LeetCode 240. 搜索二维矩阵 II - 完整解法详解
一、问题理解
问题描述
编写一个高效的算法来搜索 m × n 矩阵 matrix 中的一个目标值 target。该矩阵具有以下特性:
-
每行的元素从左到右升序排列
-
每列的元素从上到下升序排列
示例
python
输入: 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
解释: 目标值 5 存在于矩阵中
输入: 同样的矩阵, target = 20
输出: false
解释: 目标值 20 不存在于矩阵中
要求
-
时间复杂度尽可能低
-
不使用额外空间
-
矩阵不为空
二、核心思路:排除法(Z字形搜索)
基本思想
利用矩阵的行列有序特性,从矩阵的右上角(或左下角)开始搜索:
-
从右上角开始:如果当前值等于 target,返回 true
-
如果当前值小于 target:说明当前行所有值都小于 target(因为当前值是当前行最大的),排除当前行,向下移动
-
如果当前值大于 target:说明当前列所有值都大于 target(因为当前值是当前列最小的),排除当前列,向左移动
为什么选择右上角?
-
右上角的元素:是所在行的最大值,所在列的最小值
-
左下角的元素:是所在行的最小值,所在列的最大值
-
这两个位置都具有特殊的比较意义,可以用于快速排除行或列
三、代码逐行解析
方法一:右上角开始搜索(最优解)
Python 解法
python
from typing import List
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
# 获取矩阵的行数和列数
m, n = len(matrix), len(matrix[0])
# 从右上角开始搜索
i, j = 0, n - 1 # i: 行索引, j: 列索引
# 当索引在矩阵范围内时继续搜索
while i < m and j >= 0:
if matrix[i][j] == target:
return True # 找到目标值
if matrix[i][j] < target:
# 当前值小于目标值,说明这一行所有值都小于目标值
# 排除当前行,向下移动
i += 1
else:
# 当前值大于目标值,说明这一列所有值都大于目标值
# 排除当前列,向左移动
j -= 1
# 搜索完整个矩阵都没有找到
return False
Java 解法
java
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
// 获取矩阵的行数和列数
int m = matrix.length, n = matrix[0].length;
// 从右上角开始
int i = 0, j = n - 1;
// 在矩阵范围内搜索
while (i < m && j >= 0) {
if (matrix[i][j] == target) {
return true; // 找到目标值
}
if (matrix[i][j] < target) {
// 当前值小于目标值,排除当前行
i++;
} else {
// 当前值大于目标值,排除当前列
j--;
}
}
// 未找到
return false;
}
}
方法二:左下角开始搜索(同样有效)
Python 解法
python
from typing import List
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
m, n = len(matrix), len(matrix[0])
# 从左下角开始搜索
i, j = m - 1, 0 # i: 行索引, j: 列索引
while i >= 0 and j < n:
if matrix[i][j] == target:
return True
if matrix[i][j] < target:
# 当前值小于目标值,说明这一列所有值都小于目标值
# 排除当前列,向右移动
j += 1
else:
# 当前值大于目标值,说明这一行所有值都大于目标值
# 排除当前行,向上移动
i -= 1
return False
Java 解法
java
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
int m = matrix.length, n = matrix[0].length;
// 从左下角开始
int i = m - 1, j = 0;
while (i >= 0 && j < n) {
if (matrix[i][j] == target) {
return true;
}
if (matrix[i][j] < target) {
// 排除当前列
j++;
} else {
// 排除当前行
i--;
}
}
return false;
}
}
四、Java 与 Python 语法对比
1. 矩阵维度获取
| 操作 | Java | Python |
|---|---|---|
| 获取行数 | matrix.length |
len(matrix) |
| 获取列数 | matrix[0].length |
len(matrix[0]) |
2. 循环控制
| 操作 | Java | Python |
|---|---|---|
| while 循环 | while (i < m && j >= 0) |
while i < m and j >= 0: |
| 自增/自减 | i++, j-- |
i += 1, j -= 1 |
3. 条件判断
| 操作 | Java | Python |
|---|---|---|
| 相等比较 | matrix[i][j] == target |
matrix[i][j] == target |
| 小于比较 | matrix[i][j] < target |
matrix[i][j] < target |
五、实例演示
示例矩阵:
text
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
方法一(右上角开始)步骤:
-
初始位置 :
i=0, j=4,值为matrix[0][4] = 15 -
比较 :
15 > 5,所以当前列(第5列)所有值都大于5,排除第5列,j=3 -
新位置 :
i=0, j=3,值为matrix[0][3] = 11 -
比较 :
11 > 5,排除第4列,j=2 -
新位置 :
i=0, j=2,值为matrix[0][2] = 7 -
比较 :
7 > 5,排除第3列,j=1 -
新位置 :
i=0, j=1,值为matrix[0][1] = 4 -
比较 :
4 < 5,说明第1行所有值都小于5,排除第1行,i=1 -
新位置 :
i=1, j=1,值为matrix[1][1] = 5 -
比较 :
5 == 5,找到目标值,返回True
搜索路径可视化:
text
初始: (0,4)=15 > 5 → 排除第5列
(0,3)=11 > 5 → 排除第4列
(0,2)=7 > 5 → 排除第3列
(0,1)=4 < 5 → 排除第1行
(1,1)=5 == 5 → 找到!
方法二(左下角开始)步骤:
-
初始位置 :
i=4, j=0,值为matrix[4][0] = 18 -
比较 :
18 > 5,说明第5行所有值都大于5,排除第5行,i=3 -
新位置 :
i=3, j=0,值为matrix[3][0] = 10 -
比较 :
10 > 5,排除第4行,i=2 -
新位置 :
i=2, j=0,值为matrix[2][0] = 3 -
比较 :
3 < 5,说明第1列所有值都小于5,排除第1列,j=1 -
新位置 :
i=2, j=1,值为matrix[2][1] = 6 -
比较 :
6 > 5,排除第3行,i=1 -
新位置 :
i=1, j=1,值为matrix[1][1] = 5 -
比较 :
5 == 5,找到目标值,返回True
六、关键细节解析
1. 为什么这个方法有效?
-
利用矩阵的行列有序特性
-
每次比较都能排除一行或一列
-
搜索路径类似于"楼梯形"或"Z字形"
2. 时间复杂度是多少?
-
最坏情况下:从右上角到左下角,需要移动 m+n 步
-
时间复杂度:O(m+n)
-
这比暴力搜索的 O(m×n) 快得多
3. 空间复杂度是多少?
-
只使用了常数个额外变量
-
空间复杂度:O(1)
4. 为什么不能从左上角或右下角开始?
-
左上角:是最小值,如果 target 更大,不知道应该向下还是向右
-
右下角:是最大值,如果 target 更小,不知道应该向上还是向左
-
右上角和左下角具有"一边最大/最小"的特性,可以明确排除方向
5. 如何处理空矩阵?
-
题目保证矩阵不为空
-
但实际编程中可以添加边界检查:
python
if not matrix or not matrix[0]:
return False
七、复杂度分析
方法一/方法二(Z字形搜索)
-
时间复杂度:O(m + n)
-
最坏情况下需要遍历一行和一列的所有元素
-
每一步排除一行或一列
-
-
空间复杂度:O(1)
- 只使用了常数个额外变量
对比其他方法:
| 方法 | 时间复杂度 | 空间复杂度 | 备注 |
|---|---|---|---|
| 暴力搜索 | O(m×n) | O(1) | 遍历所有元素 |
| 二分查找每行 | O(m log n) | O(1) | 对每行进行二分查找 |
| Z字形搜索 | O(m+n) | O(1) | 最优解 |
八、其他解法
解法一:二分查找每行
python
from typing import List
import bisect
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
# 对每一行进行二分查找
for row in matrix:
# 使用bisect模块进行二分查找
idx = bisect.bisect_left(row, target)
if idx < len(row) and row[idx] == target:
return True
return False
解法二:分治法(递归)
python
from typing import List
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
def search_submatrix(top, bottom, left, right):
# 递归终止条件
if top > bottom or left > right:
return False
# 选择中间行和中间列
mid_row = (top + bottom) // 2
mid_col = (left + right) // 2
mid_val = matrix[mid_row][mid_col]
if mid_val == target:
return True
elif mid_val < target:
# 目标值可能在右下部分或右上部分或左下部分
return (search_submatrix(mid_row + 1, bottom, left, right) or
search_submatrix(top, mid_row, mid_col + 1, right))
else:
# 目标值可能在左上部分或右上部分或左下部分
return (search_submatrix(top, mid_row - 1, left, right) or
search_submatrix(top, bottom, left, mid_col - 1))
m, n = len(matrix), len(matrix[0])
return search_submatrix(0, m - 1, 0, n - 1)
解法三:暴力搜索(不推荐)
python
from typing import List
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
# 遍历所有元素
for row in matrix:
for val in row:
if val == target:
return True
return False
九、常见问题与解答
Q1: 如果矩阵非常大怎么办?
A1: Z字形搜索的时间复杂度是 O(m+n),即使对于非常大的矩阵也非常高效。例如 1000×1000 的矩阵最多只需要 2000 次比较。
Q2: 这个方法适用于所有有序矩阵吗?
A2: 这个方法适用于题目描述的特定有序矩阵(每行从左到右升序,每列从上到下升序)。对于其他类型的排序矩阵可能需要不同的方法。
Q3: 如果我想返回目标值的位置怎么办?
A3: 可以修改算法,在找到目标值时返回其坐标:
python
def searchMatrixWithPosition(self, matrix: List[List[int]], target: int) -> tuple:
m, n = len(matrix), len(matrix[0])
i, j = 0, n - 1
while i < m and j >= 0:
if matrix[i][j] == target:
return (i, j) # 返回坐标
elif matrix[i][j] < target:
i += 1
else:
j -= 1
return (-1, -1) # 未找到
Q4: 如何处理有重复元素的矩阵?
A4: 算法仍然有效。如果存在重复元素,找到其中一个就会返回 true。如果需要找到所有出现位置,可以在找到后继续搜索,但这样时间复杂度会变高。
Q5: 为什么这个方法比二分查找每行更好?
A5:
-
二分查找每行:O(m log n)
-
Z字形搜索:O(m + n)
-
当 m 和 n 都很大时,O(m+n) 比 O(m log n) 更优
-
例如:1000×1000 矩阵,二分查找需要约 1000×10=10000 次比较,而 Z字形搜索最多需要 2000 次比较
十、相关题目
1. LeetCode 74. 搜索二维矩阵
python
from typing import List
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
# 这个矩阵的特点是:每行有序,且下一行第一个大于上一行最后一个
# 可以将其视为一个一维有序数组进行二分查找
if not matrix or not matrix[0]:
return False
m, n = len(matrix), len(matrix[0])
left, right = 0, m * n - 1
while left <= right:
mid = (left + right) // 2
# 将一维索引转换为二维坐标
row = mid // n
col = mid % n
if matrix[row][col] == target:
return True
elif matrix[row][col] < target:
left = mid + 1
else:
right = mid - 1
return False
2. LeetCode 378. 有序矩阵中第K小的元素
python
from typing import List
import heapq
class Solution:
def kthSmallest(self, matrix: List[List[int]], k: int) -> int:
# 使用最小堆,归并排序的思想
n = len(matrix)
heap = []
# 将每行的第一个元素放入堆中
for i in range(min(k, n)):
heapq.heappush(heap, (matrix[i][0], i, 0))
# 弹出k-1次,第k次弹出的就是第k小的元素
for _ in range(k - 1):
val, row, col = heapq.heappop(heap)
# 如果当前行还有下一个元素,将其加入堆中
if col + 1 < n:
heapq.heappush(heap, (matrix[row][col + 1], row, col + 1))
return heap[0][0]
十一、总结
核心要点
-
利用有序特性:矩阵的行和列都有序,可以用于快速排除
-
选择合适的起点:右上角或左下角具有特殊的比较意义
-
排除法:每次比较可以排除一整行或一整列
算法步骤(右上角法)
-
初始化指针到右上角
(0, n-1) -
当指针在矩阵范围内:
-
如果当前值等于目标值,返回 true
-
如果当前值小于目标值,向下移动(排除当前行)
-
如果当前值大于目标值,向左移动(排除当前列)
-
-
如果搜索结束未找到,返回 false
时间复杂度与空间复杂度
-
时间复杂度:O(m + n),最坏情况下遍历一行和一列
-
空间复杂度:O(1),只使用常数个额外变量
适用场景
-
行列都有序的矩阵搜索问题
-
需要高效搜索的大型矩阵
-
不能使用额外空间的场景
扩展思考
Z字形搜索法展示了如何利用数据结构的特殊性质设计高效算法。这种思想可以应用于:
-
其他类型的二维搜索问题
-
多维数据的搜索
-
利用数据特性优化算法的问题
掌握这种搜索技巧不仅能够解决特定的矩阵搜索问题,还能提高对算法优化的理解,是面试中常见的高级算法题目。