问题描述
编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target。该矩阵具有以下特性:
- 每行的元素从左到右升序排列。
- 每列的元素从上到下升序排列。
示例 1:
输入:matrix =
1 2 3 4 5 6 7
| [ [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 2 3 4 5 6 7
| [ [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
解题思路
这道题目提供了一个特殊性质的二维矩阵,要求我们查找一个目标值。矩阵的特性是每行从左到右升序,每列从上到下升序。根据这个特性,我们可以有多种解题思路。
方法一:角落搜索法
核心思想: 利用矩阵的排序特性,从右上角或左下角开始搜索,每次能够排除一行或一列。
从右上角(或左下角)开始搜索的原理:
- 若当前元素等于目标值,直接返回 true
- 若当前元素大于目标值,则可以排除当前列(因为当前列下方的元素更大)
- 若当前元素小于目标值,则可以排除当前行(因为当前行左侧的元素更小)
这种方法的时间复杂度是 O(m + n),其中 m 是行数,n 是列数,因为每次操作都会排除一行或一列。
实现细节
我们从矩阵的右上角开始搜索:
- 初始化指针位置为右上角坐标 (0, columns-1)
- 当指针在矩阵范围内时,比较当前元素与目标值:
- 如果相等,返回 true
- 如果当前元素大于目标值,则向左移动(列索引减一)
- 如果当前元素小于目标值,则向下移动(行索引加一)
- 如果搜索结束后仍未找到目标值,返回 false
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| func searchMatrix(matrix [][]int, target int) bool { rows := len(matrix) if rows == 0 { return false } columns := len(matrix[0]) row, col := 0, columns-1 for row < rows && col >= 0 { currentValue := matrix[row][col] if currentValue == target { return true } else if currentValue > target { col-- } else { row++ } } return false }
|
方法二:二分查找法
核心思想: 由于每一行都是排序的,我们可以对每一行使用二分查找。
实现细节
- 遍历矩阵的每一行
- 对每一行使用二分查找寻找目标值
- 如果在任一行中找到目标值,返回 true
- 否则,返回 false
这种方法的时间复杂度是 O(m log n),其中 m 是行数,n 是列数。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| func searchMatrix(matrix [][]int, target int) bool { rows := len(matrix) if rows == 0 { return false } for _, row := range matrix { if binarySearch(row, target) { return true } } return false }
func binarySearch(nums []int, target int) bool { left, right := 0, len(nums)-1 for left <= right { mid := left + (right-left)/2 if nums[mid] == target { return true } else if nums[mid] > target { right = mid - 1 } else { left = mid + 1 } } return false }
|
方法三:分治法
核心思想: 利用矩阵的特性,将矩阵划分为四个子矩阵,递归搜索。
实现细节
- 找到矩阵中心点 (midRow, midCol)
- 将矩阵分为四个区域:左上、右上、左下、右下
- 比较中心点的值与目标值:
- 如果等于目标值,返回 true
- 如果大于目标值,则目标值可能在左上区域或左下区域或右上区域
- 如果小于目标值,则目标值可能在右下区域或右上区域或左下区域
- 递归搜索可能包含目标值的子矩阵
这种方法的时间复杂度在最坏情况下可能接近 O(m*n),但在平均情况下表现较好。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| func searchMatrix(matrix [][]int, target int) bool { rows := len(matrix) if rows == 0 { return false } columns := len(matrix[0]) return searchMatrixRecursively(matrix, target, 0, 0, rows-1, columns-1) }
func searchMatrixRecursively(matrix [][]int, target int, rowStart, colStart, rowEnd, colEnd int) bool { if rowStart > rowEnd || colStart > colEnd { return false } if rowStart == rowEnd && colStart == colEnd { return matrix[rowStart][colStart] == target } rowMid := rowStart + (rowEnd-rowStart)/2 colMid := colStart + (colEnd-colStart)/2 midValue := matrix[rowMid][colMid] if midValue == target { return true } if midValue > target { return searchMatrixRecursively(matrix, target, rowStart, colStart, rowMid, colMid) || searchMatrixRecursively(matrix, target, rowStart, colMid+1, rowMid, colEnd) || searchMatrixRecursively(matrix, target, rowMid+1, colStart, rowEnd, colMid) } else { return searchMatrixRecursively(matrix, target, rowMid+1, colMid+1, rowEnd, colEnd) || searchMatrixRecursively(matrix, target, rowStart, colMid+1, rowMid, colEnd) || searchMatrixRecursively(matrix, target, rowMid+1, colStart, rowEnd, colMid) } }
|
复杂度分析
| 方法 |
时间复杂度 |
空间复杂度 |
优点 |
缺点 |
| 角落搜索法 |
O(m + n) |
O(1) |
非常高效,实现简单 |
需要理解特定的搜索策略 |
| 二分查找法 |
O(m log n) |
O(1) |
易于理解,二分查找是常见算法 |
对于大矩阵,比角落搜索法慢 |
| 分治法 |
最坏 O(m*n) |
O(log(m*n)) |
适用于更一般的搜索问题 |
实现复杂,递归调用开销大 |
综合考虑,角落搜索法(方法一)是解决此问题最高效的方法,它充分利用了矩阵的特性,时间复杂度为 O(m + n),空间复杂度为 O(1)。
关键学习点
- 利用数据结构特性:矩阵的排序特性使我们能够设计出 O(m+n) 的算法。在解决问题时,充分理解并利用数据的特殊性质往往能找到最优解。
- 搜索空间缩减:角落搜索法的核心思想是每次操作都能排除一行或一列,有效地缩小搜索空间。
- 多种解法思考:同一个问题可以有多种解法,从暴力法到优化解法,通过比较不同算法的复杂度,可以找到最适合的解决方案。
- 二维空间的搜索技巧:对于二维空间的搜索问题,可以考虑从特殊点(如角落)开始,利用排序特性引导搜索方向。