题 4:二维数组中的查找
题干
在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。——《剑指 Offer》P44
测试用例
- 二维数组中包含查找的数字(査找的数字是数组中的最大值和最小值、査找的数字介于数组中的最大值和最小值之间);
- 二维数组中没有查找的数字(查找的数字大于数组中的最大值、査找的数字小于数组中的最小值、査找的数字在数组的最大值和最小值之间但数组中没有这个数字);
- 特殊输入测试(输入空指针)。
例如有如下矩阵,给定 target = 5 返回 true,给定 target = 20,返回 false。
[
[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]
]
解题思路
这道题可以使用蛮力法来解决,也就是直接遍历二维数组,一个一个匹配需要查找的元素,这种方法的时间复杂度为 O(n^2)。不过由于传入的矩阵并不是规则的矩阵,只要是有规则的矩阵都可能找到更优的解法。其实对于矩阵的查找,也是一个解空间缩小的过程,使用蛮力法是在一个 n × n 的解空间中每次减少 1。因此总体的思路是如何更快地缩小解空间的大小,对于矩阵来说,如果可以一次性排除多个行或多个列,就可以降低解决问题的时间复杂度。
设传入的 n × n 的矩阵中的任何一个元素是 x(i, j),其中 0 ≤ i < n 表示元素所在位置的行数,其中 0 ≤ j < n 表示元素所在位置的列数。由于矩阵的特性是“每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序”,因此从行的角度看,当任何一个元素 t < x(i, j) 时,该元素 t 必定小于元素 x(i + p, j) (i ≤ i + p < n)。同理从列的角度看,当任何一个元素 t > x(i, j) 时,该元素 t 必定大于元素 x(i, j - q) (0 ≤ j - q < j)。
这么讲其实还是很抽象,这里引用 LeetCode 上这道题的第一条热门评论,来自用户 ymy1248:
如何理解?也就是说如果把矩阵当做一个树结构,右上角的结点当做树的根结点,则满足 x(i - 1, j) < x(i + p, j) < x(i, j + 1),这恰好是一棵二叉搜素树。
把这个矩阵旋转过来,就能更明显地看出来了,当在该搜索树中没下降一层,就可以减少一行或一列的解空间。值得一提的是,也可以将矩阵左下角的元素当做根结点,此时结点的左孩子是较大的数,右节点是较小的数。
解题代码
bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
int idx_x = matrix.size() - 1;
int idx_y = 0;
while(idx_x >= 0 && idx_y < matrix[0].size())
{
if(matrix[idx_x][idx_y] > target)
{
idx_x--;
}
else if(matrix[idx_x][idx_y] < target)
{
idx_y++;
}
else
{
return true;
}
}
return false;
}
时空复杂度
按照二叉搜索树的方式扫描数组时,一次可以减少一行或一列的解空间,因此最坏情况是搜索完所有的行或列之后找不到。设矩阵是 N 行 N 列,则把每一行和每一列都遍历一遍的 T(n) = 2n,得时间复杂度 O(n)。
由于没有使用辅助空间,因此空间复杂度 O(1)。
参考资料
《剑指 Offer(第2版)》,何海涛 著,电子工业出版社
面试题04. 二维数组中的查找(标志数,清晰图解)