题目描述
给你一个满足下述两条属性的 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.length
- n == matrixi.length
- 1 <= m, n <= 100
- -10^4 <= matrixij, target <= 10^4
解题思路总览
| 方法 | 核心思想 | 时间复杂度 | 空间复杂度 | 特点 |
|---|---|---|---|---|
| 线性搜索(从左下角) | 利用矩阵单调性,每次排除一行或一列 | O(m+n) | O(1) | 最常用,面试推荐 |
| 线性搜索(从右上角) | 同上,方向镜像 | O(m+n) | O(1) | 与左下角等价 |
| 两次二分查找 | 先二分行,再二分列 | O(log m + log n) | O(1) | 利用二维有序性 |
| 两次二分查找(优化) | 虚拟数组展开成一维后二分 | O(log(m*n)) | O(1) | 思路简洁 |
方法一:线性搜索(从左下角出发)
代码实现
cpp
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m = matrix.size();
int n = matrix[0].size();
int a = 0, b = m - 1; // a: 列索引(从左向右),b: 行索引(从下向上)
while (a < n && b >= 0) {
if (matrix[b][a] == target) {
return true;
} else if (matrix[b][a] > target) {
b--; // 当前值大于 target,向上走(排除当前行)
} else {
a++; // 当前值小于 target,向右走(排除当前列)
}
}
return false;
}
};
核心思想
从左下角元素开始搜索:
- matrixba > target:说明 target 在当前行的上方(因为每列从上到下递增),b-- 向上移动
- matrixba < target:说明 target 在当前列的右方(因为每行从左到右递增),a++ 向右移动
算法流程图
以 matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 13 为例:
矩阵结构:
列0 列1 列2 列3
行0 1 3 5 7
行1 10 11 16 20
行2 23 3034 60
^
左下角 (b=2, a=0) = 23
搜索路径:
第1步:(2,0) = 23 > 13,向上 b=1
第2步:(1,0) = 10 < 13,向右 a=1
第3步:(1,1) = 11 < 13,向右 a=2
第4步:(1,2) = 16 > 13,向上 b=0
第5步:(0,2) = 5 < 13,向右 a=3
第6步:(0,3) = 7 < 13,a=4 越界
循环结束,返回 false
逐行解析
cpp
int m = matrix.size();
int n = matrix[0].size();
获取矩阵的行数 m 和列数 n。
cpp
int a = 0, b = m - 1;
初始化搜索起点为左下角:
- a = 0 表示从第 0 列(最左列)开始
- b = m - 1 表示从最后一行(最底行)开始
cpp
while (a < n && b >= 0) {
循环条件:列索引未越右边界 且 行索引未越上边界。
cpp
if (matrix[b][a] == target) {
return true;
}
找到目标值,返回 true。
cpp
} else if (matrix[b][a] > target) {
b--;
}
当前值大于目标值:由于每列从上到下递增,当前值是当前列中的最小值(但仍大于 target),说明目标不可能在当前行或更下的行,向上移动一行。
cpp
} else {
a++;
}
当前值小于目标值:由于每行从左到右递增,当前值是当前行中的最小值(但仍小于 target),说明目标不可能在当前列或更左的列,向右移动一列。
cpp
return false;
搜索完所有可能的行列组合仍未找到,返回 false。
复杂度分析
| 复杂度 | 分析 |
|---|---|
| 时间 | 最多移动 m + n 次,每次移动排除一行或一列,不会回头 |
| 空间 | 仅使用常数个变量(m, n, a, b),空间复杂度 O(1) |
方法二:线性搜索(从右上角出发)
代码实现
cpp
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m = matrix.size();
int n = matrix[0].size();
int a = n - 1, b = 0; // a: 列索引(从右向左),b: 行索引(从上向下)
while (a >= 0 && b < m) {
if (matrix[b][a] == target) {
return true;
} else if (matrix[b][a] > target) {
a--; // 当前值大于 target,向左走(排除当前列)
} else {
b++; // 当前值小于 target,向下走(排除当前行)
}
}
return false;
}
};
与方法一的对比
| 对比项 | 方法一(左下角) | 方法二(右上角) |
|---|---|---|
| 起始位置 | (m-1, 0) 左下角 | (0, n-1) 右上角 |
| 当前值 > target | b--(向上) | a--(向左) |
| 当前值 < target | a++(向右) | b++(向下) |
| 搜索路径 | 先上后右 | 先左后下 |
算法流程图
以 matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 13 为例:
矩阵结构:
列0 列1 列2 列3
行0 1 3 5 7
行1 10 11 16 20
行2 23 3034 60
^
右上角 (b=0, a=3) = 7
搜索路径:
第1步:(0,3) = 7 < 13,向下 b=1
第2步:(1,3) = 20 > 13,向左 a=2
第3步:(1,2) = 16 > 13,向左 a=1
第4步:(1,1) = 11 < 13,向下 b=2
第5步:(2,1) = 30 > 13,向左 a=0
第6步:(2,0) = 23 > 13,向上 b=1
b=1, a=0,回到之前位置,形成循环
实际在第5步会发现 a<0,循环结束
返回 false
逐行解析
cpp
int a = n - 1, b = 0;
初始化搜索起点为右上角:
- a = n - 1 表示从第 n-1 列(最右列)开始
- b = 0 表示从第 0 行(最顶行)开始
cpp
while (a >= 0 && b < m) {
循环条件:列索引未越左边界 且 行索引未越下边界。
cpp
if (matrix[b][a] == target) {
return true;
} else if (matrix[b][a] > target) {
a--;
} else {
b++;
}
核心逻辑与左下角版本对称:
- 当前值大于 target:由于每列从上到下递增,当前值是当前列中的最小值,说明目标不可能在当前列,向左移动
- 当前值小于 target:由于每行从左到右递增,当前值是当前行中的最大值,说明目标不可能在当前行,向下移动
复杂度分析
| 复杂度 | 分析 |
|---|---|
| 时间 | 最多移动 m + n 次 |
| 空间 | O(1) |
方法三:两次二分查找
代码实现
cpp
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m = matrix.size();
int n = matrix[0].size();
// 第一步:二分查找行,找到第一个大于等于 target 的行
int top = 0, bottom = m - 1;
int targetRow = -1;
while (top <= bottom) {
int mid = top + (bottom - top) / 2;
if (matrix[mid][0] <= target && target <= matrix[mid][n - 1]) {
// target 在这一行内
targetRow = mid;
break;
} else if (matrix[mid][0] > target) {
bottom = mid - 1;
} else {
top = mid + 1;
}
}
// 如果没有找到合适的行,返回 false
if (targetRow == -1) {
return false;
}
// 第二步:在找到的行内二分查找列
int l = 0, r = n - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (matrix[targetRow][mid] == target) {
return true;
} else if (matrix[targetRow][mid] < target) {
l = mid + 1;
} else {
r = mid - 1;
}
}
return false;
}
};
核心思想
利用矩阵的两个性质:
- 每行第一个元素大于前一行的最后一个元素
- 每行内部有序
先通过二分找到 target 可能所在的行,再在该行内二分查找 target。
复杂度分析
| 复杂度 | 分析 |
|---|---|
| 时间 | O(log m + log n) = O(log(m*n)) |
| 空间 | O(1) |
方法四:虚拟数组展开
代码实现
cpp
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m = matrix.size();
int n = matrix[0].size();
int l = 0, r = m * n - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
int midValue = matrix[mid / n][mid % n];
if (midValue == target) {
return true;
} else if (midValue < target) {
l = mid + 1;
} else {
r = mid - 1;
}
}
return false;
}
};
核心思想
将二维矩阵虚拟地展开成一维有序数组:
- 二维坐标 (row, col) 映射到一维索引 idx = row * n + col
- 一维索引 idx 对应的二维坐标为 (idx / n, idx % n)
- 展开后仍保持有序,可直接二分
复杂度分析
| 复杂度 | 分析 |
|---|---|
| 时间 | O(log(m*n)) |
| 空间 | O(1) |
边界情况分析
情况1:矩阵只有一行
输入: matrix = [[1,3,5,7]], target = 5
分析: 从左下角 (0,0)=1 开始,向右找到 5
结果: true
情况2:矩阵只有一列
输入: matrix = [[1],[3],[5],[7]], target = 5
分析: 从左下角 (3,0)=7 开始,向上找到 5
结果: true
情况3:target 小于所有元素
输入: matrix = [[1,3,5,7],[10,11,16,20]], target = 0
分析: 从左下角 (1,0)=10 > 0,向上 (0,0)=1 > 0
再向上 b=-1,循环结束
结果: false
情况4:target 大于所有元素
输入: matrix = [[1,3,5,7],[10,11,16,20]], target = 100
分析: 从左下角 (1,0)=10 < 100,向右到 (1,1)=11 < 100
继续向右直到越界
结果: false
情况5:target 正好在角落元素
输入: matrix = [[1,3,5,7],[10,11,16,20]], target = 1
分析: 从左下角 (1,0)=10 > 1,向上 (0,0)=1 == 1
结果: true
面试追问 FAQ
| 问题 | 回答 |
|---|---|
| 为什么从左下角或右上角开始? | 这两个位置是矩阵的"鞍点",可以保证每次移动都排除一行或一列,不会走回头路 |
| 为什么不能从左上角或右下角开始? | 左上角:右移和下移都增大,无法排除;右下角:左移和上移都减小,无法排除 |
| 线性搜索的时间复杂度是多少? | O(m+n),最多移动 m+n 步 |
| 如何处理 matrix 为空的情况? | 需要先检查 matrix.size() > 0 && matrix0.size() > 0 |
| 方法三和方法四哪个更好? | 本质相同,方法四写法更简洁;方法三思路更直观,适用于面试解释 |
| 如果每行不是严格递增怎么办? | 本题条件是"非严格递增",即允许相等。上述算法仍然有效,因为比较操作符方向不变 |
相关题目
| 题目 | 难度 | 核心区别 |
|---|---|---|
| 74. 搜索二维矩阵(本题) | 中等 | 行列均有单调性 |
| 240. 搜索二维矩阵 II | 中等 | 每行每列均递增,允许从任意角开始 |
| 35. 搜索插入位置 | 简单 | 一维数组的搜索插入位置 |
| 34. 在排序数组中查找元素的第一个和最后一个位置 | 困难 | 二分查找左边界和右边界 |
总结
| 要点 | 说明 |
|---|---|
| 核心思想 | 利用矩阵的单调性,从特定角落出发,每次排除一行或一列 |
| 推荐起点 | 左下角或右上角 |
| 时间复杂度 | O(m+n) 或 O(log(m*n)) |
| 空间复杂度 | O(1) |
| 关键性质 | 每行递增 + 每行首元素大于前一行尾元素 = 矩阵整体有序 |