一、题目描述
编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target。该矩阵具有以下特性:
- 每行的元素从左到右升序排列
- 每列的元素从上到下升序排列
示例 1:
输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]]
target = 5
输出:true
示例 2:
输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]]
target = 20
输出:false
提示:
- m == matrix.length
- n == matrix[i].length
- 1 <= n, m <= 300
- -10^9 <= matrix[i][j] <= 10^9
- 每行的所有元素从左到右升序排列
- 每列的所有元素从上到下升序排列
- -10^9 <= target <= 10^9
二、解题思路总览
核心思想:从右上角开始,利用有序性单向剪枝
从矩阵的右上角开始搜索,每次比较后根据大小关系排除一行或一列:
- 如果当前元素大于 target,说明目标不可能在当前列的下方,往左走
- 如果当前元素小于 target,说明目标不可能在当前行的上方,往下走
为什么从右上角或左下角开始?
因为这两个角具有「单向有序」的特性:
- 右上角:同行左边更小,同列下边更大
- 左下角:同行右边更大,同列上边更小
从其他角落开始无法保证单向剪枝。
| 方法 | 核心思路 | 时间复杂度 | 空间复杂度 |
|---|---|---|---|
| 搜索窗口法(本题) | 从右上角开始,每次排除一行或一列 | O(m + n) | O(1) |
| 二分搜索 | 对每行进行二分 | O(m log n) | O(1) |
| 暴力搜索 | 遍历整个矩阵 | O(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; // 当前行,从 0 开始
int b = n - 1; // 当前列,从最后一列开始(右上角)
while (l < m && b >= 0) {
if (matrix[l][b] == target) {
return true;
}
if (matrix[l][b] > target) {
b--; // 当前元素太大,目标在左边,列减一
} else if (matrix[l][b] <= target) {
l++; // 当前元素太小,目标在下边,行加一
}
}
return false;
}
};
四、算法流程图
4.1 整体搜索流程
输入:matrix, target
[Step 1] 初始化起点
l = 0(从第一行开始)
b = n - 1(从最后一列开始,即右上角)
|
v
[Step 2] 进入主循环
|
v
l < m && b >= 0 ?
|否
v
【返回 false】 ← 循环结束,未找到
|
|是
v
[Step 3] 获取当前元素
current = matrix[l][b]
|
v
current == target ?
|是
v
【返回 true】 ← 找到目标
|
|否
v
current > target ?
|是 |否
v v
[Step 4] 列左移 [Step 5] 行下移
| |
v v
b-- l++
| |
v v
回到 Step 2 回到 Step 2
4.2 具体示例执行流程(找到目标)
输入:matrix = [[1,4,7,11,15],
[2,5,8,12,19],
[3,6,9,16,22],
[10,13,14,17,24],
[18,21,23,26,30]]
target = 5
初始:l=0, b=4
第一轮:
matrix[0][4] = 15 > 5 → b-- → b=3
第二轮:
matrix[0][3] = 11 > 5 → b-- → b=2
第三轮:
matrix[0][2] = 7 > 5 → b-- → b=1
第四轮:
matrix[0][1] = 4 < 5 → l++ → l=1
第五轮:
matrix[1][1] = 5 == 5 → 返回 true
找到目标!共执行 5 步。
4.3 具体示例执行流程(未找到目标)
输入:target = 20
初始:l=0, b=4
第一轮:matrix[0][4] = 15 < 20 → l++ → l=1
第二轮:matrix[1][4] = 19 < 20 → l++ → l=2
第三轮:matrix[2][4] = 22 > 20 → b-- → b=3
第四轮:matrix[2][3] = 16 < 20 → l++ → l=3
第五轮:matrix[3][3] = 17 < 20 → l++ → l=4
第六轮:matrix[4][3] = 26 > 20 → b-- → b=2
第七轮:matrix[4][2] = 23 > 20 → b-- → b=1
第八轮:matrix[4][1] = 21 > 20 → b-- → b=0
第九轮:matrix[4][0] = 18 < 20 → l++ → l=5
第十轮:l=5 不满足 l < m,循环退出
返回 false(未找到)
4.4 为什么从右上角开始
以 4x4 矩阵为例:
col=0 col=1 col=2 col=3
row=0 1 2 3 4
row=1 5 6 7 8
row=2 9 10 11 12
row=3 13 14 15 16
右上角 (l=0, b=3) = 4
特性:
- 往左走(b--):值变小(同行左边元素更小)
- 往下走(l++):值变大(同列下方元素更大)
这保证了每次比较后只能往一个方向走,不会丢失任何可能性。
如果从左上角开始 (l=0, b=0) = 1:
- 往右走(b++):值变大
- 往下走(l++):值变大
两个方向都可能导致 target,无法剪枝。
五、逐行解析
5.1 初始化起点
cpp
int l = 0; // 当前行
int b = n - 1; // 当前列(右上角)
选择右上角的原因:
- 同行左侧都是比它小的元素(往左走值变小)
- 同列下方都是比它大的元素(往下走值变大)
- 形成天然的搜索剪枝条件
5.2 主循环逻辑
cpp
while (l < m && b >= 0) {
if (matrix[l][b] == target) {
return true;
}
if (matrix[l][b] > target) {
b--;
} else if (matrix[l][b] <= target) {
l++;
}
}
三种情况:
| 情况 | 条件 | 动作 | 原因 |
|---|---|---|---|
| 找到目标 | matrix[l][b] == target | return true | 搜索成功 |
| 当前太大 | matrix[l][b] > target | b-- | 同行左边更小,目标可能在左边 |
| 当前太小 | matrix[l][b] < target | l++ | 同列下方更大,目标可能在下方 |
5.3 循环结束条件
cpp
return false;
何时返回 false:
- l >= m:已经越过最后一行(第 m 行不存在)
- b < 0:已经越过第一列(第 -1 列不存在)
- 两种情况都说明已经搜索完所有可能的路径
六、复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(m + n) | 每次循环要么 l++,要么 b--,最多走 m+n 步 |
| 空间复杂度 | O(1) | 只用了两个指针变量 |
最坏情况演示:
m=5, n=5 的矩阵,最多走 5+5-1=9 步:
l 从 0 到 4(5 步)
b 从 4 到 0(4 步)
七、面试追问
| 问题 | 回答要点 |
|---|---|
| 为什么从右上角开始而不是左上角? | 左上角往右往下都变大,无法单向剪枝;右上角同行往左更小,同列往下更大 |
| 可以从左下角开始吗? | 可以,逻辑相同:当前太大往上走(行减),当前太小往右走(列加) |
| 时间复杂度为什么是 O(m+n)? | 每次循环 l++ 或 b--,最多 m+n 步 |
| 如果 target 小于矩阵右上角? | 从右上角开始,直接 b-- 向左搜索,直到找到或越界 |
| 为什么不用二分搜索? | 每行每列虽然有序,但不是全局有序,二分只能作用在单行或单列上 |
| 最坏情况下搜索多少次? | m+n-1 次,即沿着对角线走完整个矩阵 |
| 如果 target 大于矩阵右下角? | 从右上角开始,一路 l++ 向下搜索,直到找到或越界 |
| 为什么是 <= 而不是 <? | 当 matrix[l][b] < target 时行加一,等于 target 时在第一个 if 已经返回,else if 用 <= 或 < 等价 |
八、相关题目
| 题号 | 题目 | 关键点 |
|---|---|---|
| 240 | 搜索二维矩阵 II | 本题 |
| 74 | 搜索二维矩阵 | 每行每列升序,但第一行最后一个元素的右边不再有更小的值 |
| 378 | 有序矩阵中第 K 小的元素 | 堆或二分搜索 |
| 54 | 螺旋矩阵 | 矩阵遍历 |