Java版LeetCode热题100之搜索二维矩阵:从基础到进阶的全面解析
本文将带你深入剖析 LeetCode 第74题「搜索二维矩阵」,通过多种解法、复杂度分析、面试技巧与实际应用,帮助你彻底掌握这道经典算法题。
一、原题回顾
题目描述(LeetCode 74. 搜索二维矩阵)
给你一个满足下述两条属性的m x n整数矩阵:
- 每行中的整数从左到右按非严格递增顺序排列(即升序,允许相等)。
- 每行的第一个整数大于前一行的最后一个整数。
给你一个整数target,如果target在矩阵中,返回true;否则,返回false。
示例
示例 1:
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3 输出:true示例 2:
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 13 输出:false约束条件
m == matrix.lengthn == matrix[i].length1 <= m, n <= 100-10^4 <= matrix[i][j], target <= 10^4
二、原题分析
这道题的关键在于理解矩阵的两个结构性质:
- 行内有序:每一行都是升序排列;
- 行间有序:第
i行的首元素 > 第i-1行的尾元素。
这意味着整个矩阵可以被看作是一个全局升序的一维数组。例如:
[[1, 3, 5, 7], [10,11,16,20], [23,30,34,60]]可视为一维数组:[1,3,5,7,10,11,16,20,23,30,34,60],完全升序。
因此,我们可以利用**二分查找(Binary Search)**这一高效算法来解决问题。
但如何在二维结构上进行二分?这是本题的核心挑战。
三、答案构思
思路一:两次二分查找(Two Binary Searches)
- 第一步:在第一列中二分查找,确定
target可能所在的行。- 因为每行首元素 > 上一行末元素 ⇒ 第一列也是升序。
- 找到最后一个 ≤ target 的行首元素所在的行。
- 第二步:在该行中进行标准二分查找,判断
target是否存在。
✅ 优点:逻辑清晰,易于理解,适用于“行长度不一致”的变种题(虽然本题行长度一致)。
思路二:一次二分查找(Flatten Matrix)
- 将整个
m x n矩阵虚拟展开为长度为m*n的一维升序数组。 - 对下标
[0, m*n - 1]进行二分。 - 利用数学映射:
- 一维下标
idx→ 二维坐标:row = idx / n,col = idx % n
- 一维下标
- 直接比较
matrix[row][col]与target。
✅ 优点:代码简洁,效率高,充分利用了全局有序性。
两种方法时间复杂度相同,但实现思路不同,各有适用场景。
四、完整答案(Java 实现)
方法一:两次二分查找
classSolution{publicbooleansearchMatrix(int[][]matrix,inttarget){// Step 1: Find the candidate row using binary search on first columnintrowIndex=binarySearchFirstColumn(matrix,target);if(rowIndex<0){returnfalse;// No valid row found}// Step 2: Search within that rowreturnbinarySearchRow(matrix[rowIndex],target);}/** * 在矩阵的第一列中查找最后一个 <= target 的行索引 * 使用「右边界」二分模板(找 <= target 的最大值) */privateintbinarySearchFirstColumn(int[][]matrix,inttarget){intlow=-1;// 哨兵,表示无有效行inthigh=matrix.length-1;while(low<high){// 防止死循环:向上取整intmid=(high-low+1)/2+low;if(matrix[mid][0]<=target){low=mid;// 当前 mid 可能是答案,保留}else{high=mid-1;// 排除 mid}}returnlow;// 返回 [-1, m-1]}/** * 在指定行中进行标准二分查找 */privatebooleanbinarySearchRow(int[]row,inttarget){intlow=0,high=row.length-1;while(low<=high){intmid=(high-low)/2+low;if(row[mid]==target){returntrue;}elseif(row[mid]>target){high=mid-1;}else{low=mid+1;}}returnfalse;}}方法二:一次二分查找(推荐)
classSolution{publicbooleansearchMatrix(int[][]matrix,inttarget){intm=matrix.length;intn=matrix[0].length;intlow=0;inthigh=m*n-1;while(low<=high){intmid=(high-low)/2+low;// 将一维索引映射回二维坐标introw=mid/n;intcol=mid%n;intvalue=matrix[row][col];if(value==target){returntrue;}elseif(value<target){low=mid+1;}else{high=mid-1;}}returnfalse;}}💡 提示:方法二更简洁,且在 LeetCode 上通常运行更快(常数因子更小)。
五、代码分析
方法一详解
binarySearchFirstColumn:- 使用的是寻找右边界的二分模板。
- 初始化
low = -1是为了处理target < matrix[0][0]的情况(此时应返回false)。 mid = (high - low + 1) / 2 + low是向上取整,避免low = mid导致死循环。- 最终
low是满足matrix[low][0] <= target的最大行号。
binarySearchRow:- 标准二分查找,无特殊处理。
方法二详解
- 关键在于坐标映射:
- 一维下标
idx ∈ [0, m*n-1] - 行号:
idx / n(整除) - 列号:
idx % n(取模)
- 一维下标
- 例如
m=3, n=4,则:idx=5→row=5/4=1,col=5%4=1→matrix[1][1]
- 这种映射在内存连续存储(如 C/C++)中天然成立,在 Java 中虽为引用数组,但逻辑上完全等价。
六、时间复杂度和空间复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 方法一(两次二分) | O(log m + log n) = O(log(mn)) | O(1) | 先查行O(log m),再查列O(log n) |
| 方法二(一次二分) | O(log(mn)) | O(1) | 直接在mn个元素上二分 |
📌 注意:
log m + log n = log(mn),所以两者渐近复杂度相同。
但由于方法二只有一次循环,常数更小,实际运行更快。
七、问题解答(常见疑问)
Q1:为什么方法一要找“最后一个 ≤ target 的行”?
因为矩阵性质保证:若target存在,它一定在第一个首元素 > target 的行的上一行中。
例如target=13,第一列为[1,10,23],10 ≤ 13 < 23,所以应在第1行(索引1)查找。
Q2:方法二是否要求每行长度相同?
是的!方法二依赖n = matrix[0].length作为固定列数进行坐标映射。
如果各行长度不同(如锯齿数组),则无法用idx / n计算行号。
⚠️ 但本题明确
n == matrix[i].length,所以安全。
Q3:能否用线性搜索?
可以,但效率低:
- 时间复杂度
O(m + n)(从右上角开始走)或O(mn)(暴力遍历)。 - 不符合“高效查找”的要求,面试中会被扣分。
八、优化思路
优化1:提前终止
在方法一中,若target < matrix[0][0]或target > matrix[m-1][n-1],可直接返回false。
if(target<matrix[0][0]||target>matrix[m-1][n-1]){returnfalse;}但这在二分中已隐含处理,收益有限。
优化2:使用库函数(不推荐面试)
Java 的Arrays.binarySearch()可用于行内查找,但第一列仍需自定义二分。
intpos=Arrays.binarySearch(matrix[row],target);returnpos>=0;但面试官更希望看到手写二分能力。
优化3:位运算加速(微优化)
mid = (low + high) >>> 1可防止溢出,但本题m,n ≤ 100,无需考虑。
九、数据结构与算法基础知识点回顾
1. 二分查找(Binary Search)
- 前提:数组必须有序。
- 核心思想:每次排除一半搜索空间。
- 模板:
- 查找目标值:
while (low <= high) - 查找边界:
while (low < high)+ 向上/向下取整
- 查找目标值:
2. 二维数组的线性化
- 将
matrix[i][j]映射为一维下标:index = i * n + j - 反向映射:
i = index / n,j = index % n - 应用于图像处理、矩阵压缩、内存布局等
3. 单调性与有序结构
- 本题的“全局有序”是解题关键。
- 类似结构:杨氏矩阵(Young Tableau),但本题更强(完全有序)。
4. 时间复杂度对数级别
O(log N)意味着每步问题规模减半。- 100万数据只需约20次比较!
十、面试官提问环节(模拟对话)
面试官:你用了两次二分,能不能只用一次?
✅回答:可以。因为整个矩阵是全局升序的,我们可以将其视为一维数组,通过idx → (row, col)映射,在[0, m*n-1]上做一次二分即可。
面试官:如果矩阵不是完全有序,只是每行每列分别有序(如左上到右下递增),还能用二分吗?
✅回答:不能直接用全局二分。但可以从右上角开始:
- 若
matrix[i][j] > target,则排除当前列(j--) - 若
matrix[i][j] < target,则排除当前行(i++) - 时间复杂度
O(m + n)
面试官:你的方法二在什么情况下会失效?
✅回答:当矩阵的行长度不一致时(如matrix = [[1,2], [3]]),无法用固定n进行坐标映射。此时方法一更鲁棒。
面试官:空间复杂度真的是 O(1) 吗?
✅回答:是的。我们只使用了几个整型变量(low,high,mid等),没有使用额外数组或递归栈,因此空间复杂度为常数。
十一、这道算法题在实际开发中的应用
1. 数据库存储引擎
- B+树、LSM树等索引结构常将数据按页(Page)组织,每页内部有序,页间也有序。
- 查找某条记录时,先定位页(类似找行),再在页内二分查找(类似找列)。
2. 分页缓存系统
- 缓存按时间或ID分段存储,每段有序。
- 快速判断某ID是否在缓存中,可用类似“两次二分”策略。
3. 图像/视频处理
- 像素矩阵可视为二维数组,某些压缩算法依赖局部有序性。
- 快速查找特定像素值的位置。
4. 游戏开发中的地图系统
- 大型地图分块加载,每块内坐标有序。
- 快速判断玩家是否进入某区域。
5. 日志分析
- 日志按时间分文件,每个文件内按时间戳排序。
- 查找某时间点的日志:先二分文件,再二分行。
十二、相关题目推荐
| 题目 | 难度 | 关联点 |
|---|---|---|
| 240. 搜索二维矩阵 II | 中等 | 每行每列有序,但非全局有序 |
| 378. 有序矩阵中第 K 小的元素 | 中等 | 利用堆或二分答案 |
| 74. 搜索二维矩阵 | 中等 | 本题 |
| 1351. 统计有序矩阵中的负数 | 简单 | 利用有序性优化遍历 |
| 1428. 至少在两个数组中出现的值 | 中等 | 多数组二分 |
🔔 建议:先掌握本题,再挑战 240 题(更难,无全局有序性)。
十三、总结与延伸
核心收获
- 结构决定算法:矩阵的“全局有序”性质是使用二分查找的前提。
- 二维转一维:通过数学映射,将复杂结构简化为熟悉模型。
- 二分查找的灵活性:不仅可用于一维数组,还可用于“虚拟有序序列”。
- 鲁棒性 vs 简洁性:方法一更通用,方法二更高效,需根据场景选择。
延伸思考
- 如果矩阵非常大(无法全加载到内存)?
- 可结合外部排序思想,分块读取 + 二分定位块。
- 如果允许重复元素?
- 本题已允许(非严格递增),不影响解法。
- 能否用分治或递归?
- 可以,但不如二分高效,且空间复杂度可能上升。
最佳实践建议
- 优先考虑方法二:代码短、效率高、易维护。
- 掌握二分模板:尤其是边界处理(
low < highvslow <= high)。 - 面试时先说思路:强调“全局有序 → 虚拟一维数组 → 二分”。
结语
“搜索二维矩阵”看似简单,却蕴含了数据结构特性识别、算法模型转换、边界处理等多重考察点。掌握它,不仅能拿下 LeetCode 热题,更能培养你在面对复杂数据结构时的抽象与简化能力。
正如计算机科学家 Edsger Dijkstra 所言:“简单是可靠的先决条件。” 通过将二维问题降维到一维,我们不仅找到了最优解,也体会到了算法之美。
✨练习建议:手写两遍代码,确保无 bug;尝试修改条件(如行长度不等),看解法是否仍适用。