📌 题目链接:74. 搜索二维矩阵 - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:数组、二分查找
⏱️ 目标时间复杂度:O(log(mn))
💾 空间复杂度:O(1)
🎯 题目分析
本题给出一个 m × n 的整数矩阵,具有两个关键性质:
- 每行从左到右非严格递增(即升序) ;
- 每行的第一个元素 > 前一行的最后一个元素。
这意味着:整个矩阵按行展开后是一个严格升序的一维数组!
✅ 举例:
[[1,3,5,7],[10,11,16,20],[23,30,34,60]]
展开为:[1,3,5,7,10,11,16,20,23,30,34,60]→ 完全有序!
因此,虽然数据结构是二维的,但逻辑上是一维有序数组。这为我们使用二分查找提供了天然条件。
面试中常考:如何将高维问题降维处理? 本题就是经典案例。
🔍 核心算法及代码讲解:二分查找(一次 vs 两次)
📌 方法一:两次二分查找(先找行,再找列)
思路拆解:
-
第一轮二分:在
matrix[i][0](每行首元素)中找 最后一个 ≤ target 的行。- 因为
matrix[i][0]是严格递增的(由题设保证),可二分。 - 使用
upper_bound找第一个 > target 的行,然后前移一行即为目标行。
- 因为
-
第二轮二分:在该行内进行标准二分查找。
C++ 代码(带详细行注释):
class Solution {
public:
bool searchMatrix(vector<vector<int>> matrix, int target) {
// 使用 upper_bound 在行首元素中查找第一个 > target 的行
// 自定义比较函数:target < row[0] ?
auto row = upper_bound(matrix.begin(), matrix.end(), target, [](const int b, const vector<int> &a) {
return b < a[0]; // 注意:upper_bound 要求 comp(value, element)
});
// 如果所有行首都 > target,则 target 不可能存在于矩阵中
if (row == matrix.begin()) {
return false;
}
--row; // 回退到最后一行满足 row[0] <= target 的行
// 在该行内进行标准二分查找
return binary_search(row->begin(), row->end(), target);
}
};
💡 面试重点:
upper_bound的比较函数写法容易出错!标准形式是comp(value, element)。- 此方法鲁棒性更强:即使每行列数不同(如锯齿数组),只要行首递增仍可工作。
📌 方法二:一次二分查找(虚拟一维化)
思路拆解:
-
将
m × n矩阵视为长度为m*n的一维数组。 -
下标
idx映射到二维坐标:- 行号:
idx / n - 列号:
idx % n
- 行号:
-
直接对
[0, m*n - 1]进行标准二分。
C++ 代码(带详细行注释):
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m = matrix.size(), n = matrix[0].size();
int low = 0, high = m * n - 1; // 虚拟一维数组的左右边界
while (low <= high) {
int mid = (high - low) / 2 + low; // 防止溢出的写法
int x = matrix[mid / n][mid % n]; // 将一维下标映射回二维
if (x < target) {
low = mid + 1;
} else if (x > target) {
high = mid - 1;
} else {
return true; // 找到目标值
}
}
return false; // 未找到
}
};
💡 面试重点:
- 坐标映射公式必须熟记:
row = idx / n,col = idx % n。- 此方法简洁高效,但依赖矩阵是“矩形”且每行列数相同。若为锯齿数组则失效。
- 时间复杂度更优常数因子(仅一次循环 vs 两次函数调用)。
🧩 解题思路(分步详解)
-
理解矩阵结构
- 题设保证全局有序 → 可视为一维升序数组。
- 这是解题的关键洞察!很多同学卡在“二维”思维上。
-
选择策略
- 若追求代码简洁 & 性能 → 选方法二(一次二分)。
- 若考虑通用性 & 鲁棒性(如后续题目变为不规则矩阵)→ 选方法一。
-
实现细节
- 方法一注意
upper_bound的比较函数方向。 - 方法二注意整数除法与取模的映射关系。
- 方法一注意
-
边界处理
target小于最小值或大于最大值时应快速返回false。- 两种方法均天然处理了这些情况。
⏱️ 算法分析
| 方法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 两次二分 | O(log m + log n) = O(log mn) | O(1) | 通用性强,适用于不规则矩阵 | 多一次函数调用开销 |
| 一次二分 | O(log mn) | O(1) | 代码简洁,常数更小 | 依赖矩阵为矩形 |
✅ 两者时间复杂度渐近等价,实际性能差异微小。
🎯 面试推荐:优先写方法二(体现降维思想),再补充方法一展示全面性。
💻 代码(完整可运行)
C++ 版本
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m = matrix.size(), n = matrix[0].size();
int low = 0, high = m * n - 1;
while (low <= high) {
int mid = (high - low) / 2 + low;
int x = matrix[mid / n][mid % n];
if (x < target) {
low = mid + 1;
} else if (x > target) {
high = mid - 1;
} else {
return true;
}
}
return false;
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
vector<vector<int>> matrix1 = {{1,3,5,7},{10,11,16,20},{23,30,34,60}};
cout << sol.searchMatrix(matrix1, 3) << "\n"; // 输出: 1 (true)
cout << sol.searchMatrix(matrix1, 13) << "\n"; // 输出: 0 (false)
vector<vector<int>> matrix2 = {{1}};
cout << sol.searchMatrix(matrix2, 1) << "\n"; // 输出: 1
cout << sol.searchMatrix(matrix2, 2) << "\n"; // 输出: 0
return 0;
}
JavaScript 版本
/**
* @param {number[][]} matrix
* @param {number} target
* @return {boolean}
*/
var searchMatrix = function(matrix, target) {
const m = matrix.length;
const n = matrix[0].length;
let low = 0, high = m * n - 1;
while (low <= high) {
const mid = Math.floor((high - low) / 2) + low;
const x = matrix[Math.floor(mid / n)][mid % n];
if (x < target) {
low = mid + 1;
} else if (x > target) {
high = mid - 1;
} else {
return true;
}
}
return false;
};
// 测试
console.log(searchMatrix([[1,3,5,7],[10,11,16,20],[23,30,34,60]], 3)); // true
console.log(searchMatrix([[1,3,5,7],[10,11,16,20],[23,30,34,60]], 13)); // false
console.log(searchMatrix(, 1)); // true
console.log(searchMatrix(, 2)); // false
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!