剑指 Offer - 二维数组中的查找 - 二分查找法

二维数组中的查找

http://www.nowcoder.com/questionTerminal/abc3fe2ce8e146608e868a70efebf62e

二分查找法: O(logM * logN)

题目描述

在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

[
  [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]
]

这里列举两种思路,第一种就是比较常见的方法了,也很容易理解。但是我想着重介绍一下第二种使用二分查找的方法。

解法一:线性查找

观察前面例子中的矩阵,如果从左上角开始搜索,如果目标值大于左上角的数,那么后续查询方向就有两个,左边或者下边。同理,如果从右下角开始查,移动的方向也有两个。但是从另外两个角查起,就完全不同了。假设我们从右上角开始寻找,要找的目标是 9,因为 15 > 9,那么唯一的选择就是向左移动,因为 15 下面的可能大于 9,所以最右边的那一列都被排除了。11 同理,11 所在的列也完全被排除了,因为这一列肯定都大于 9。往左移动到 7,由于 7 < 9,因此只能向下寻找,而且 7 所在行可以完全排除掉了。向下寻找,很快就找到了。

查找过程就是根据情况向下或者向左寻找,一旦触及左边或者下边的边界,而且要突破边界,那就说明没有找到。同样的道理,也可以从左下角找起,道理相同,不在叙述。这个算法的时间复杂度就是 O(m+n)。

class Solution {
public:
    bool Find(const vector<vector<int>>& array, int target) {
      if(array.empty() || array[0].empty()){
        return false;
      }
      int row = array.size(), col = array[0].size();
      int x = 0, y = col-1;

      while(x < row && y >= 0){
        if(array[x][y] < target){
          x += 1;
        }else if(array[x][y] > target){
          y -= 1;
        }else{
          return true;
        }
      }

      return false;
    }
};

解法二:二分查找

再次观察这个例子,还假设我们寻找的是 9。

[
  [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]
]

可以轻易地发现,最后两列是可以排除的,因为这两列中最小的元素 10 和 18 都已经大于了 9。我们还可以发现最后两行也可以排除,因为这两行最小的元素也都大于 9。因此余下的内容就是下面这些:

[
  [1,   4,  7],
  [2,   5,  8],
  [3,   6,  9]
]

不难发现,前两列可以排除掉,因为这两列中最大的元素都小于 9。同样的,由于前两行中最大的元素都小于 9,于是前两行也可以排除。

通过上面的分析,可以总结出规律:通过观察每一列开头元素,可以排除掉大于目标值的列。通过观察每一列的结尾,可以排除小于目标值的列。相同地,观察行的开头和结尾也可以排除掉不可能存在目标值的行。因此可以不断地缩小范围,但其实存在一些特殊情况。

[
  [5, 6, 9],
  [9, 10,11],
  [11,14,18]
]

在上面这个数组中,如果查询的数是 9,由于数组中可能存在多个待查找的值,使用前面的方法缩小范围,永远也不可能把范围缩小到 1,因为去掉了最后一行之后,就再也没法排除了,可以自行检验。而此时其实待查找的值就落在余下范围的角上,因此在缩小范围的过程中,要不断地检查副对角线上的两个角的值。

基于以上分析,可以给行和列维护一个范围,交替从不同方向缩小范围,并不断检查左下角和右上角的值。这里涉及到的查找,可以使用二分查找法。在行上查找一次的时间复杂度是 O(logM),需要在行上查找多少次呢。因为待查找的列的范围平均每次减小一半,因此 logN 次后列的范围就没了,循环就退出了。因此行上的查找会进行 logN 次,由此推算时间复杂度是 O(logN * logM)。反过来从列上推算也是一样的。可以看到这种方法和前一种线性查找的方法比起来有巨大的优势。

下面是 python 的实现,因为在列上搜索需要跨越多个数组很不方便,因此使用了 numpy 库,这样得到某一列就相当简单了。但是由于牛客网当下不支持 numpy 了,以下代码跑不通(以前牛客是支持 numpy 的,不知最近为啥不支持了),可以考虑在 LeetCode - 面试题04. 二维数组中的查找 上验证结果。

import numpy as np

def upper_bound(nums, x):
    """
    find the first element great than x in a sorted list
    return the index of this element
    """
    lo, hi = 0, len(nums)

    while lo < hi:
        mid = (lo + hi) // 2
        if nums[mid] <= x:
            lo = mid + 1
        else:
            hi = mid
    return lo


def lower_bound(nums, x):
    """
    find the first element that is not less than x (i.e. greater or equal to)
    """
    lo, hi = 0, len(nums)

    while lo < hi:
        mid = (lo + hi) // 2
        if nums[mid] < x:
            lo = mid + 1
        else:
            hi = mid
    return lo


class Solution:
    def Find(self, matrix, x):
        if len(matrix) == 0 or len(matrix[0]) == 0:
            return False

        row_lo, row_hi = 0, len(matrix)
        col_lo, col_hi = 0, len(matrix[0])

        matrix = np.array(matrix)
        while (row_lo < row_hi) and (col_lo < col_hi):
            if matrix[row_hi-1][col_lo] == x or matrix[row_lo][col_hi-1] == x:
                return True

            row_hi = upper_bound(matrix[:, col_lo], x)
            col_hi = upper_bound(matrix[row_lo], x)

            row_lo = lower_bound(matrix[:, col_hi-1], x)
            col_lo = lower_bound(matrix[row_hi-1], x)

        return False

以上代码需要说明一下,lower_boundupper_bound 的作用是在一个排序数组中,寻找某个范围的上下界。比如 nums = [1,3,5,5,5,7],这里 5 出现了 3 次,构成了一个范围。lower_bound(nums, 5) 会返回第一个 5 的下标,这是范围的起始下标。而 upper_bound(nums, 5) 会返回 7 的下标,这是范围的终止下标,这里采用的是左闭右开的区间表示法。

因此在代码中,寻找下界的时候,要使用 lower_bound,寻找上界的时候会使用 upper_bound。当待查找的值不在数组中时,两者返回的结果相同,即第一个大于查询值的数的下标。

全部评论
题目没说是方阵,有可能没主对角线
3 回复 分享
发布于 2020-08-13 10:10
讲得很好。但为啥没人从右下角开始想呢。右下角最大,比它大就一定不在里面。然后看主对角线的倒数第二个,比它大就在最下面一行和最右边一列看。
2 回复 分享
发布于 2020-01-15 21:19

相关推荐

不愿透露姓名的神秘牛友
10-29 21:31
大疆 硬件工程师 30.0k*15.0
快要三方开始纠结offer的到期男大:dji和影石我理解开奖了,华为你这个15级是怎么问出来的
点赞 评论 收藏
分享
不愿透露姓名的神秘牛友
09-30 19:49
牛客798932866号:已经出薪资了嘛
投递汇川技术等公司10个岗位
点赞 评论 收藏
分享
我冲冲冲冲冲:泪目了,好想选自己想选的答案啊
点赞 评论 收藏
分享
浩鲸云计算 java开发 每月11k,总包14w
点赞 评论 收藏
分享
18 收藏 评论
分享
牛客网
牛客企业服务