剑指 Offer 04. 二维数组中的查找

题目描述:

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

示例:

现有矩阵 matrix 如下:

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

限制:

0 <= n <= 1000

0 <= m <= 1000

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/er-wei-shu-zu-zhong-de-cha-zhao-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

        ps:记下来一段为自我吐槽,可以略过到后面的分析。

        这道题算是对我自信的最大打击吧,哎,不知道说什么好,本来看到中等类型的,我还认为会没有多难(实际上确实不难),但我花了一晚上一下午才给他解决掉。。。。。这道题完完全全考在了我的弱点上,1.我一眼看过去,猜到了应该是二分查找(目前为止还没看题解,只是自我理解),但是我做过一位数组的二分查找,没有做过二维的。2.它使用的并不是普通的二维数组int a[m][n]这种类型,而是用的是vector类型的,关于vector我可以说是很不熟悉。3.里面涉及到了vector<vector<int>>& matrix;这一句,然后直击痛点,本来指针学的就不好,到现在为止,&和*我还是一看就怕,一看到这里,我又混乱了,比如他要一个引用类型,那我该传给他一个什么?我传的时候matrix加&还是加*,又或者都不加?matrix的地址是matrix总地址还是第一行的地址?如果把vector二维数组换成普通的二维数组,我想的那些还对吗?怎么通过&和*传递普通二维数组?等等一系列问题需要解决。这道题已经不仅仅是算法的问题了,而是整个C语言的底子问题,我确实该补补基础了。

        分析:

        我自己的思路是,二分查找,先用二分查找确定哪一行

然后再用二分查找target位于该行的哪一列。

二分查找target位于该行哪一列,这个很简单,没什么可说的。但是对于二分查找位于哪一行来说,我遇到了问题。对于第i行,如果target不在该行中,那么该如何做?是去0—(i-1)行查找还是去i+1—n行查找?target失去了一个比较的指标flag,他是与第i行第一个元素比,还是与第i行最后一个元素比?如果比flag小或者大又怎么办?这个数组很特殊,我们假定与第i行第一个元素进行比较,那么如果target<第i行第一个元素,我们可以断定,target一定位于0-(i-1)行中,因为在i-n行中,第i行第一个元素是最小的,比它小的元素只能出现在之前的行中。

但是,如果target>第i行第一个元素,我们能说target一定在i行之后吗?不能,因为i行之前的元素,也有可能比i行之后的要大。

所以,当target>第i行第一个元素,我只能想到遍历所有行,来判断target是否位于其中,判断某一行内部时可以用二分法。大致思路就是这样,时间复杂度最坏情况下为O(n*longn),实际上就是遍历所有行,然后每一行用二分查找。

        代码实现如下:

        

class Solution {
public:
    bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
        int row,col;
        row=matrix.size();
        // 避免出现matrix为[]的情况
        if(row!=0)
        col=matrix[0].size();
        // 避免出现matrix为[]和[[]]的情况
        if(row==0||col==0)
        return false;
        // int i=row/2;
        
        // int j=col/2;
        // 开始查找行
        return divrow(matrix,target,0,row-1,row,col);
        
        // return true;
    }
    bool divrow (vector<vector<int>>& matrix, int target,int start,int end,int row,int col)
    {
        // 不存在的条件,如果查找的起始行号比结束行号还要大,说明之前都没有找到,target不存在,返回false
        if(start>end)
        return false;
        // 确认本次查找的行号i
        int i=(start+end)/2;
        // 利用二分查找检查第i行是否存在target
        if(divcol(matrix[i],target,0,col-1))
        {
            // 如果第i行存在target,返回true
            return true;
        }
        // 如果第i行不存在target,查找别的行
        
        // 如果target比i行第1个元素小,说明target一定在i行之前(具体见分析)
        if(target<=matrix[i][0])
        {
            return divrow(matrix,target,start,i-1,row,col);
        }
        else
        // 如果target比i行第1个元素大,并不能说明target一定在i行之后(具体见分析),只能对所有行进行遍历
        {
            // 遍历所有行
            for(int t=start;t<=end;t++)
            {
                // 如果找到了t行中有target,返回true
                // 注意这里传入的是matrix[t],而不是matrix,因为divcol要求传入的是一维vector数组
                if(divcol(matrix[t],target,0,col-1))
                return true;
            }
            如果遍历所有行都没有找到target,则本轮查找为false
            return false;
        }
        // 啥也没找到,返回false
        return false;
    }
    // 二分查找某一行
    bool divcol (vector<int>& matrix, int target,int start,int end)
    {
        // 普通的二分查找,不再分析
        if(start>end)
        return false;
        int j=(start+end)/2;
        

        if(matrix[j]==target)
        return true;
        if(target > matrix[j])
        {
            return divcol(matrix,target,j+1,end);
        }
        if(target<matrix[j])
        {
            return divcol(matrix,target,start,j-1);

        }
        return false;
    }
};

正确的思路,我看完题解再来分享

在评论区看到了别人的二分法,虽然也是O(n*logn),但是比我的高级和易懂一些。

附上代码:

class Solution {
    public boolean findNumberIn2DArray(int[][] matrix, int target) {
        int m = matrix.length;
        if(m == 0) return false;
        int n = matrix[0].length;
        int start = 0 , end = m-1;
        int l = 0 , r = m-1;
        while(l < r){
            int mid = (l+r) >>> 1;
            //System.out.println("mid is "+mid);
            //System.out.println("l is "+l);

            //System.out.println("r is "+r);

            int j = n-1;
            if(matrix[mid][j] == target) return true;
            if(matrix[mid][j] > target){
                r = mid;
            }else{

                l = mid+1;
            }
        }

        start = r;
        //System.out.println("start is "+start);

        l = 0;
        r = m-1;
        while(l < r){
            int mid = (l+r) >>> 1;
            //System.out.println("mid is "+mid);
            //System.out.println("l is "+l);

            //System.out.println("r is "+r);

            if(matrix[mid][0] == target) return true;
            if(matrix[mid][0] > target){

                r = mid-1;
            }else{

                l = mid+1;
            }
        }
        end = r;
        // System.out.println(start + "  " + r);
        //System.out.println("end is " + end);
        return Helper(m,n,matrix,target,start,end);
        
    }

    public boolean Helper(int m , int n , int[][] matrix, int target , int start , int end){
        for(int i = start ; i <= end ; i++){
            int l = 0 , r = n-1;
            while(l <= r){
                int mid = (l+r) >>> 1;
                //System.out.println(matrix[i][mid]);
                if(matrix[i][mid] == target) return true;
                if(matrix[i][mid] > target){
                    r = mid-1;
                }else{
                    l = mid+1;
                }
            }
        }
        return false;
    }
}

代码出自:leetcode用户:FalseBlank

        其思想与我类似,确认某行之后,对该行进行二分查找,这很简单,大家都想得到。然后就是如何确定行的问题。他做了以下两步:

        1.把target与每行的最后一个元素进行比较,设行号为i,如果target<i行最后一个元素,则把搜索的行号起始点start压缩到i行及之前(因为如果target<i行最后一个元素,说明target可能存在于i行及i行之前,也可能target在i行之后,但是没有办法保证i行之前就没有target,因此必须把所有可能涉及到的行都搜索),假如判断到了i-3行,此时target>matrix[i-3][m-1](假定m-1为最后一个元素),说明了什么?说明了target必不可能在i-3行之前了(matrix[i-3][m-1]已经是0——(i-3)行中最大的元素了,而target比matrix[i-3][m-1]还要大,说明target肯定在i-3——n行中了),因此start卡在了i-3行,直到第一次while循环结束,我们得到了target的起始搜索行start。

        2.把target与每行的第一个元素进行比较,设行号为i,如果target>matrix[i][0],则把搜索的行号终点end压缩到i行及之后,因为如果target>matrix[i][0],说明target可能存在于i行及i行之后,也可能target在i行之前,但是没有办法保证i行之后就没有target,因此必须把所有可能涉及到的行都搜索)。假如判断到了i+3行,此时target<matrix[i+3][0],说明了什么?说明了target必不可能在i+3行之后了(matrix[i+3][0]已经是i+3——n行中最小的元素了,而target比matrix[i-3][m-1]还要小,说明target肯定在0——i+3行中了),因此end卡在了i+3行,直到第一次while循环结束,我们得到了target的终止搜索行end。

        故此,我们到了target的分布范围start和end,在start和end行之间进行遍历,对每一行进行二分查找,就可以判断target是否存在。因为无法对行号进行准确的二分查找,故行号的时间复杂度仍为O(n),行内的二分查找时间复杂度为O(logn),故总的时间复杂度为O(n*logn)。

最后,就是题解了,看完题解感觉自己的智商受到了侮辱。。。。

题解的思路很简单,选取最右上角的数,开始与target比较,如果比target小,则向左移动,如果比target大,则向右移动。

思路简单,但是如何想到这一点,几乎是无法跨越的天堑,评论区大佬

Marina

给了思考问题的思路,很受启发,分享一下:

选择一个点进行比较然后缩小搜索范围,关键是这个点如何选择最优。

  1. 如果是数组中除顶点以外任意点,以该点为二维坐标原点,左上象限全部和左下象限部分比该点值小,右下象限全部和右上象限部分比它大,并不能有效地缩小范围。
  2. 四个顶点: 左上顶点,其他所有点都比它大; 右下顶点,其他所有点都比它小; 右上顶点,如果该点值比target大,则可以排除当前这一列,向前一列移动缩小搜索范围,如果比target小,则可以排除当前一行,向下一行移动缩小搜索范围。 左下顶点和右上类似。

所以最优起始点是左下顶点和右上顶点。

以后还是要多培养这种思考问题的方式。

此题历时两天,终于算是结束了,可以开启下一个征程了。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值