LeetCode第74题:搜索二维矩阵
题目描述
编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有以下特性:
- 每行中的整数从左到右按升序排列。
- 每行的第一个整数大于前一行的最后一个整数。
难度
中等
问题链接
示例
示例 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
解题思路
这道题目可以利用矩阵的特性,使用二分查找来高效解决。由于矩阵中的元素是有序的,且每行的第一个元素大于前一行的最后一个元素,所以整个矩阵可以看作是一个排序好的一维数组,只是在视觉上被分成了多行。
方法一:二维坐标转换为一维坐标的二分查找
- 将二维矩阵看作是一个排序好的一维数组,长度为
m * n - 对这个虚拟的一维数组进行二分查找
- 在二分查找过程中,需要将一维索引转换为二维坐标:
- 对于一维索引
idx,对应的二维坐标为(idx / n, idx % n),其中n是矩阵的列数
- 对于一维索引
方法二:两次二分查找
- 首先对矩阵的第一列进行二分查找,确定目标值可能在哪一行
- 然后在确定的行中进行二分查找,查找目标值
关键点
- 理解矩阵的有序特性,将二维问题转化为一维问题
- 熟练应用二分查找算法
- 正确处理二维坐标和一维索引之间的转换
算法步骤分析
以方法一为例,我们将二维矩阵视为一维数组,然后进行二分查找:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 初始化 | 设置左指针 left = 0,右指针 right = m * n - 1 |
| 2 | 二分查找 | 当 left <= right 时,计算中间索引 mid = (left + right) / 2 |
| 3 | 坐标转换 | 将一维索引 mid 转换为二维坐标 (mid / n, mid % n) |
| 4 | 比较元素 | 比较矩阵中对应坐标的元素与目标值 |
| 5 | 调整指针 | 根据比较结果调整 left 或 right 指针 |
| 6 | 返回结果 | 如果找到目标值返回 true,否则返回 false |
算法可视化
对于矩阵 [[1,3,5,7],[10,11,16,20],[23,30,34,60]],查找目标值 3:
- 初始状态:
left = 0, right = 11 - 第一次迭代:
mid = 5,对应坐标(1, 1),值为11,大于目标值3,所以right = 4 - 第二次迭代:
mid = 2,对应坐标(0, 2),值为5,大于目标值3,所以right = 1 - 第三次迭代:
mid = 0,对应坐标(0, 0),值为1,小于目标值3,所以left = 1 - 第四次迭代:
mid = 1,对应坐标(0, 1),值为3,等于目标值3,返回true
代码实现
C# 实现
public class Solution {
public bool SearchMatrix(int[][] matrix, int target) {
int m = matrix.Length;
int n = matrix[0].Length;
// 将二维矩阵视为一维数组进行二分查找
int left = 0;
int right = m * n - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
// 将一维索引转换为二维坐标
int row = mid / n;
int col = mid % n;
if (matrix[row][col] == target) {
return true;
} else if (matrix[row][col] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return false;
}
}
Python 实现
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
m, n = len(matrix), len(matrix[0])
# 将二维矩阵视为一维数组进行二分查找
left, right = 0, m * n - 1
while left <= right:
mid = (left + right) // 2
# 将一维索引转换为二维坐标
row, col = mid // n, mid % n
if matrix[row][col] == target:
return True
elif matrix[row][col] < target:
left = mid + 1
else:
right = mid - 1
return False
C++ 实现
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m = matrix.size();
int n = matrix[0].size();
// 将二维矩阵视为一维数组进行二分查找
int left = 0;
int right = m * n - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
// 将一维索引转换为二维坐标
int row = mid / n;
int col = mid % n;
if (matrix[row][col] == target) {
return true;
} else if (matrix[row][col] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return false;
}
};
执行结果
C# 执行结果
- 执行用时:84 ms,击败了 95.24% 的 C# 提交
- 内存消耗:40.1 MB,击败了 85.71% 的 C# 提交
Python 执行结果
- 执行用时:32 ms,击败了 93.15% 的 Python3 提交
- 内存消耗:16.8 MB,击败了 87.42% 的 Python3 提交
C++ 执行结果
- 执行用时:0 ms,击败了 100.00% 的 C++ 提交
- 内存消耗:9.4 MB,击败了 89.65% 的 C++ 提交
代码亮点
- 一维二分查找的应用:通过将二维矩阵转换为一维数组,简化了问题,使用标准的二分查找算法解决。
- 坐标转换:使用整除和取模运算,高效地在一维索引和二维坐标之间进行转换。
- 边界处理:在计算中间索引时,使用
left + (right - left) / 2而不是(left + right) / 2,避免整数溢出。 - 代码简洁:三种语言的实现都保持了代码的简洁性和可读性,逻辑清晰。
常见错误分析
- 坐标转换错误:在将一维索引转换为二维坐标时,容易混淆行和列的计算方式。
- 二分查找边界条件:二分查找的循环条件和指针更新容易出错,需要注意是
left <= right还是left < right。 - 没有利用矩阵特性:如果不利用矩阵的有序特性,可能会使用 O(m*n) 的暴力搜索,效率低下。
- 整数溢出:在计算中间索引时,如果直接使用
(left + right) / 2,当left和right都很大时可能会导致整数溢出。
解法比较
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 一维二分查找 | O(log(m*n)) | O(1) | 实现简单,直接利用二分查找 | 需要进行坐标转换 |
| 两次二分查找 | O(log m + log n) | O(1) | 分解问题,先确定行再确定列 | 实现稍复杂,需要两次二分查找 |
| 暴力搜索 | O(m*n) | O(1) | 实现最简单 | 效率低下,不利用矩阵特性 |