力扣240——搜索二维矩阵

这道题主要是利用搜索二维矩阵本身的特性,找到其中的规律,就可以解决了。

原题

编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target。该矩阵具有以下特性:

  • 每行的元素从左到右升序排列。
  • 每列的元素从上到下升序排列。

示例:

现有矩阵 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。

给定 target = 20,返回 false。

原题url:https://leetcode-cn.com/problems/search-a-2d-matrix-ii/

解题

这道题相比之前的二维矩阵,可能有序性没有之前那么强,所以没法直接拉成一个一维数组利用二分法查找,需要结合其特性,进行查找。

暴力解法

就是一行行、一列列慢慢找。假设是一个m * n的二维数组,那么时间复杂度就是O(mn),这个方法没什么好说的,贴个代码看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[0].length; j++) {
if (matrix[i][j] == target) {
return true;
}
}
}

return false;
}
}

行列同时寻找

这是我自己想的方法,就是每次查找行列,只查每一行每一列最大值和最小值。根据这个二维数组的特性,找出可能存在 target 的行列的范围,然后逐渐缩小,如果行列相同时,则开始遍历寻找。

让我们看看代码:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
if (matrix.length == 0) {
return false;
}
return search(0, matrix.length - 1, 0, matrix[0].length - 1, matrix, target);
}

private boolean search(
int rowStart, int rowEnd,
int colStart, int colEnd,
int[][] matrix, int target) {

if (rowStart == rowEnd || colStart == colEnd) {
if (rowStart == rowEnd) {
for (int i = colStart; i <= colEnd; i++) {
if (matrix[rowStart][i] == target) {
return true;
}
}
} else {
for (int i = rowStart; i <= rowEnd; i++) {
if (matrix[i][colStart] == target) {
return true;
}
}
}
return false;
}

// 是否有合适的行,默认没有
boolean has = false;
// 新的rowStart、rowEnd
int newRowStart = 0, newRowEnd = 0;
// 筛选行
for (int i = rowStart; i <= rowEnd; i++) {
// 如果相等,说明找到了
if (matrix[i][colStart] == target || matrix[i][colEnd] == target) {
return true;
}

// target大于这一行的最大值,直接跳过
if (target > matrix[i][colEnd]) {
continue;
}

// target小于这一行的最小值,说明之后的就不用找了
if (target < matrix[i][colStart]) {
break;
}

// 如果是第一次找到
if (!has) {
has = true;
newRowStart = i;
}

newRowEnd = i;
}
// 如果没有找到
if (!has) {
return false;
}

has = false;
int newColStart = 0, newColEnd = 0;
// 筛选列
for (int i = colStart; i <= colEnd; i++) {
// 如果相等,说明找到了
if (matrix[newRowStart][i] == target || matrix[newRowEnd][i] == target) {
return true;
}

// target大于这一列的最大值,直接跳过
if (target > matrix[newRowEnd][i]) {
continue;
}

// target小于这一列的最小值,说明之后的就不用找了
if (target < matrix[newRowStart][i]) {
break;
}

// 如果是第一次找到
if (!has) {
has = true;
newColStart = i;
}

newColEnd = i;
}
// 如果没有找到
if (!has) {
return false;
}

return search(newRowStart, newRowEnd, newColStart, newColEnd, matrix, target);
}
}

时间复杂度上,应该是比最基础的暴力法好一些,但应该也是没有本质区别。

行列同时二分查找

以行列总数中较小的那个数,选择构成正方形的正对角线,每一次按照二分法,查找相应的行列,可以参考下面这张图:

每次都会对行和列各用一次二分法,逐步排查。让我们看看代码:

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
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
if (matrix == null || matrix.length == 0) {
return false;
}

// 利用二分法搜索
// 寻找出行数、列数中较小的那个值,作为迭代次数
int count = Math.min(matrix.length, matrix[0].length);

int low, high, middle;
// 迭代循环
for (int i = 0; i < count; i++) {
// 从(i,i)开始,寻找这一行是否有target
low = i;
high = matrix[0].length - 1;
while (low <= high) {
middle = (low + high) / 2;
if (matrix[i][middle] == target) {
return true;
}
if (matrix[i][middle] > target) {
high = middle - 1;
} else {
low = middle + 1;
}
}

// 从(i,i)开始,寻找这一列是否有target
low = i;
high = matrix.length - 1;
while (low <= high) {
middle = (low + high) / 2;
if (matrix[middle][i] == target) {
return true;
}
if (matrix[middle][i] > target) {
high = middle - 1;
} else {
low = middle + 1;
}
}
}

return false;
}
}

这个方法的时间复杂度为O(lg(n!))

1
2
3
4
5
6
7
8
9
10
11
12
这个算法产生的时间复杂度并不是特别明显的是 O(lg(n!)) ,所以让我们一步一步地分析它。
在主循环中执行的工作量逐渐最大,它运行 min(m,n)次迭代,其中 m 表示行数,n 表示列数。
在每次迭代中,我们对长度为 m-i 和 n-i 的数组执行两次二分查找。因此,循环的每一次迭代都以 O(lg(m-i)+lg(n-i)) 时间运行,其中 i 表示当前迭代。
我们可以将其简化为 O(2 lg(n-i))= O(lg(n-i)) ,在最坏的情况是 n≈m 。当 n≪m 时,n 将在渐近分析中占主导地位。通过汇总所有迭代的运行时间,我们得到以下表达式:
O(lg(n)+lg(n−1)+lg(n−2)+…+lg(1))

然后,我们可以利用对数乘法规则(lg(a)+lg(b)=lg(ab))将复杂度改写为:

O(lg(n)+lg(n−1)+lg(n−2)+…+lg(1))
=O(lg(n⋅(n−1)⋅(n−2)⋅…⋅1))
=O(lg(1⋅…⋅(n−2)⋅(n−1)⋅n))
=O(lg(n!))

划分为四个二维数组

这是一种递归查找,同样也是利用了这个二维搜索数组的特性。我们在当前矩阵中间的列上进行比较,如果小于 target 就继续向下比较,直到找到比 target 大的数,此时递归查找左上和右上的矩阵。

以下面这张图为例,我们假设target = 9,那么中间列找到 10 以后,发现比 9 大,因此寻找左上和右上两个标红的矩阵:

接下来我们看看代码:

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
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
if (matrix == null || matrix.length == 0) {
return false;
}

// 利用搜索二维矩阵的特性,划分为2个搜索矩阵进行递归搜索
return searchRecursive(0, 0, matrix[0].length - 1, matrix.length - 1, matrix, target);
}

/**
* 左上角和右下角的坐标,在这个二维矩阵中进行搜索
*/
private boolean searchRecursive(
int left, int up,
int right, int down,
int[][] matrix, int target) {
// 此时矩阵的长度或者宽度为0,那么就没有必要搜索了
if (left > right || up > down) {
return false;
}
// 如果target小于该矩阵的最小值(左上角)或者大于该矩阵的最大值(右下角),也没有必要搜索了
if (target < matrix[up][left] || target > matrix[down][right]) {
return false;
}

// 利用列进行拆分
int mid = (left + right) / 2;
// 从上到下开始寻找
int row = up;
for (; row <= down; row++) {
if (matrix[row][mid] == target) {
return true;
}

// matrix[row][mid]作为一个标准点,如果小于target,则继续往下一行寻找
if (matrix[row][mid] < target) {
continue;
}

// 如果matrix[row][mid]大于target,则停止增加
break;
}

return searchRecursive(left, row, mid - 1, down, matrix, target) ||
searchRecursive(mid + 1, up, right, row - 1, matrix, target);
}
}

时间复杂度:O(nlgn)。分析如下:

单向寻找

结合该二维数组的特性,我们希望在进行比较的时候,只往一个方向寻找,这样可以简化查询步骤。如果从左上(最小)开始寻找时,如果 target 比当前值大,我们不知道是该往右移还是往下移,右下(最大)同理。

此时我们可以换一种思路,从左下开始,因为比它小的数一定在它右边,比它大的数一定在它右边,这样寻找的时候就简单多了。

接下来我们看看代码:

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
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
if (matrix == null || matrix.length == 0) {
return false;
}

// 从左下角开始寻找,如果当前值大于target,则向上移动一行;如果当前值小于target,则向右移动一列。
// 直到找到target,或者超出矩阵边界

// 列的最大值
int colMax = matrix[0].length - 1;
// 行列开始的下标
int row = matrix.length - 1;
int col = 0;

while (row >= 0 && col <= colMax) {
if (matrix[row][col] == target) {
return true;
}

// 如果大于target,则向上移一行
if (matrix[row][col] > target) {
row--;
}
// 如果小于target,则向右一列
else {
col++;
}
}

return false;
}
}

时间复杂度:O(n+m)。如果和上面一样,假设 n << m,那么就是 O(m),总的来说,还是比较高效的。

总结

以上就是这道题目我的解答过程了,不知道大家是否理解了。这道题目主要还是在于利用二维搜索数组的特性,完成解题。

有兴趣的话可以访问我的博客或者关注我的公众号、头条号,说不定会有意外的惊喜。

https://death00.github.io/

公众号:健程之道

健健 wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
如果您感觉文章不错,也愿意支持一下作者的话