从2个有序数组中找第k小那个数

系列文章目录

提示:AC==accepted,即LeetCode上提交代码通过,我刷题的代码用的是java,但是C++一个道理,算法思想一样,而且c++和java非常类似,python需要自己写,但是算法的根本思想仍然一样。
还有,很多算法代码不需要背,只需要理解清楚原理,面试场上自己推都给它把代码边界推出来了
本文的思想来源于左神,我很敬仰他!


文章目录

  • 系列文章目录
  • 算法题目
  • 一、审题
  • 二、笔试AC普通解法
    • 1.双指针移动计数法
    • 2.二分查找法
      • 2.1一维有序数组arr中查找第k小的数——二分查找算法原型
      • 2.2利用二分查找算法原型来解决这个题
  • 二、面试优化解法
    • 1.先学一个重要的算法原型:如何求等长数组A,B中的中位数k
    • 2.利用二中1.的算法原型,求解本题
  • 总结
    • 测试和验证代码:


算法题目

提示:面试官有可能这样提问:

数组A,B分别长度为N,M,均为升序数组,请你从A,B中找到第k小的数。


提示:以下是本审题过程和解题过程

一、审题

eg1:
A=[1,3,4,5,7,13,19]
B=[2,6,7,9,13,15,20]
k=6
啥意思呢?
请你找到第k小的数是多少?返回的结果是A/B中的某一个值,这个值,是AB融合之后再排序的数组C中,第k小那个数。
AB融合排序:1,2,3,4,5,6,7,7,9,13,13,19,20;
显然,第6小就是6,返回6即可。

二、笔试AC普通解法

1.双指针移动计数法

先别急着融合,我们不需要将整体数组全部融合,再去遍历求,那样速度将是o(N+M),太慢。
笔试为了简单编写代码,还能快速AC,怎么办呢?
设立2个指针,p1指着A的0位置,p2指着B的0位置;
然后领count==0做计时器,在指针滑动过程中,count计数,count计数到k就停止,说明找到了第k小的数。
p1,p2怎么滑动呢?

比较A[p1]和B[p2]的大小,谁小谁滑动,说明小的先排前面,做的工作就相当于融合AB数组为C。
但是count计数到了k就停

代码如下:

public static int findKthMinNum(int[] A, int[] B, int k){
        int p1 = 0;
        int p2 = 0;

        int m = A.length;
        int n = B.length;
        int count = 0;//计数器
        int ans = A[p1] <= B[p2] ? A[p1++] : B[p2++];//先定为这个
        if (k == 1) return ans;
        count++;//刚刚已经排好了一个了

        while (p1 < m && p2 < n){
            //同时不越界,其中一个越界,就只会剩另一个,之前有讲过merge代码就这样
            ans = A[p1] <= B[p2] ? A[p1++] : B[p2++];//谁小谁移动
            count++;
            if (count == k) return ans;
        }
        while (p1 < m){
            //如果A还没有搞定全
            ans = A[p1++];//谁小谁移动
            count++;
            if (count == k) return ans;
        }
        while (p1 < m){
            //如果B还没有搞定全
            ans = B[p2++];//谁小谁移动
            count++;
            if (count == k) return ans;
        }
        //全部都越界了还没有搞定,那只能是k过大
        return -1;
    }

测试与验证的代码:

public static void test(){
        int[] A = {1,3,5,7,9};
        int[] B = {2,4,6,8,10};

        System.out.println(getUpMedian(A, 0, A.length - 1, B, 0, B.length - 1));

    }

    public static void main(String[] args) {
        test();
    }

笔试的话,求AC,这个题,就上面这个代码就过了;
但是面试时,如果你这么写,只能得0分,因为时间复杂度最次为o(N+M)
而你得清楚什么叫面试?它目的是为了什么?面试是为了招优秀的能做算法优化的学生,而不是随便写一个算法的普通人,不可能这么写就要你的。

2.二分查找法

2.1一维有序数组arr中查找第k小的数——二分查找算法原型

上面那个方法,来不来你就从头到尾遍历数组,最次就把俩数组都遍历完了,那既然是有序数组的查找,咱们在一维有序数组arr中查找aim时,就有一个算法原型:
怎么做呢?
一维有序数组arr=[1,2,3,4,5,6,7,8,9],长度为N=9,不妨设aim=5,你要用二分查找法,快速找到aim?返回ans==5;
定义函数int f(arr,L,R),从数组L–R范围上二分查找一个数aim,使其算法时间复杂度为o(log(N))
每次直接找中点mid=L+((R-L)>>1);判断中点arr[mid]与aim的大小?看看找到了没有,没有找到的话,需要调整下一次L,R,方便继续找。

这里必须秀一下,计算机中位运算,远比四则运算速度快!
x>>1 即 x/2,x<<1 即 2x,1<
另外,本题中,为啥中点mid要写L+((R-L)>>1),而不是写(L+R)>>1,是因为当L+R超过int表示的范围时,你得到了一个负数,再除2没啥意义,mid是个负数,会溢出报错的,故提前让R-L先除2,加到L上,就代表了L--R的中点了。

继续说,怎么调整下一次L,R,三个分支:
从2个有序数组中找第k小那个数_第1张图片

如果arr[mid] == aim,则返回arr[mid];
如果arr[mid] > aim,意味着aim还在mid的左边,下一次,需要去数组的左半边找,即调整R=mid-1,继续调用int f(arr,L,R);
如果arr[mid] < aim,意味着aim还在mid的右边,下一次,需要去数组的右半边找,即调整L=mid+1,继续调用int f(arr,L,R);
当然,注意 base case:
如果L==R了,说明你只有一个数了,不妨设aim一定在arr中,则aim一定是arr[L]==arr[R]
啥时候停止查找呢?L>R就越界了,直接return即可;

代码如下:

//二分查找一个有序数组arr的aim
    public static int findAimOfAscendingArr(int[] arr, int aim){
        if (arr == null || arr.length == 0) return -1;
        return findAscending(arr, aim, 0, arr.length - 1);
    }

    public static int findAscending(int[] arr, int aim, int L, int R){
        if (L == R) return arr[L];//仅剩下一个了,我们设定aim一定在其中的

        while (L < R){
            int mid = L +((R - L) >> 1);
            if (arr[mid] == aim) return arr[mid];
            else if (arr[mid] > aim) R = mid - 1;//还得去左边找
            else L = mid + 1;//arr[mid] < aim,还得去右边找
        }
        return -1;//没找到必然是-1
    }

    public static void test2(){
        int[] arr = {1,2,3,4,5,6,7,8,9};
        int aim = 5;
        System.out.println(findAimOfAscendingArr(arr, aim));
    }

2.2利用二分查找算法原型来解决这个题

现在,如何利用上面的二分查找原型算法解决本题呢?
这么想:
从2个有序数组中找第k小那个数_第2张图片
你先找A的中点A[mid],将其作为aim,去B中二分搜索<=aim的最右那个点(不妨设其位置为j),这样的话,A的mid极其左边有a个,B的j极其左边就有b个,你看看:
a+b==k吗?相等,说明你找到了第k小那个数
如果,a+b>k,说明mid过大,还得去A的左边二分找新的mid
如果,a+b 如果在A中找不到,则还需要去B中找mid,想法,跟上面一样
代码就不写了,思路就这样,但这不是本文的重点,即使这样做,算法复杂度也是仅仅是o(log(N)*log(M)),仍然不是最好的解,但是比上面那个好点。

本文的重点在如何优化算法复杂度为o( min( log(N), log(M) ) ),这是最优解!


二、面试优化解法

1.先学一个重要的算法原型:如何求等长数组A,B中的中位数k

如何优化使得本题算法复杂度极其低呢?达到最优解,o( min( log(N), log(M) ) )
既然是log,显然也是规模每次减半的,因而,咱的目标肯定是每次并行地让A,B都找中点mid,然后类似于单数组二分查找一样,咱们需要排除另一半,然后递归其中一半,由于是并行二分查找,那就是o( min( log(N), log(M) ) )

难点在于,你怎么并行控制哪一半淘汰呢?
这个题考验我们的coding能力,尤其是抠边界和细节,捣鼓清楚数组的索引范围,才能完整地把代码搞出来。

我们要把难点破解开,就需要先学一个重要的算法原型:如何求 等长数组 A,B中的中点第k小那个数。
这里要关注,等长,都是N长度,那AB任意一个数组的中点那个数mid就是N/2呗,因此,在俩数组中找到mid1和mid2,我们这么想,说不定,答案就在mid1和mid2中(比如,俩数组中的值一样的话),比如下图,俩长度都是4,一共8个,则你分别找到2和2’,如果22’,则2或者2’就是我们要的答案,因为左边4个值,右边4个值,k4必然是2或者2’。
从2个有序数组中找第k小那个数_第3张图片
但是如果2!=2’呢?
如果2>2’,显然,结果不可能是34(因为升序的,34比2大,2比2’还大,你要划分4个数出来作为右半边,必然包含34,它们就不可能是mid),同样1’,2’也不可能是结果,因为1,1’,2’都比2小,只能在左边,现在就要看1,2和3’,4’谁是mid了,于是我们下一次递归时,规模已经把1’,2’与3,4淘汰了。
如果2<2’,显然,结果不可能是3’4’,也不可能是12,淘汰掉这一般,下次去递归另外两部分即,1’2’,34。
当然,这是N为偶数的时候,当N为奇数时,情况还不太一样呢
先说,代码如何判断一个数是奇数呢还是偶数呢?用位运算,取出x的最低位,它是0,就是偶数,它是1就是奇数:

boolean odd = (x & 1) == 1;//长度奇数判断1就是奇数,odd为true0就是偶数,odd为false

我们再来看看N是奇数时情况如何?
从2个有序数组中找第k小那个数_第4张图片
长度为N=5,故,找k=5小的数,你会发现mid1和mid2它是正中点,而不是上中点,当3==3’时,mid在5,6小位置,所以3和3’是结果,但是,
1)如果3>3’时,345和1’2’绝不会是结果,你需要去12和3’4’5’中找,这可不对了,下一次递归已经不是等长的数组了,因此我们需要手动排除一个,将下一次递归数组AB搞出等长来,才能继续重复迭代。
现在比较2和3’谁大?
如果3’更大,则3’必然是结果,因为这样就让12,1’2’成为左边部分4个,而3>3’,刚刚好3’就是第5小
如果比较2和3’,2更大,那就不清楚了,还需要去12和4’5’中等长寻找mid。
2)如果3<3’时,与1)相反的,需要捣鼓清楚下一次迭代应该用谁?

下面代码这么设计,从A的s1到e1,从B的s2到e2,中找到融合数组中点位置那个数
代码如下:

//从A的s1到e1,从B的s2到e2,找到两者融合升序的上中位数
    public static int getUpMedian(int[] A, int s1, int e1, int[] B, int s2, int e2){
        int mid1 = 0;
        int mid2 = 0;

        while (s1 < e1){
            boolean odd = ((e1 - s1 + 1) & 1) == 1;//长度奇数判断
            //s1!=e1
            mid1 = (s1 + e1) / 2;
            mid2 = (s2 + e2) / 2;//取中点
            //分为两种状况,一个是偶数长度
            if (!odd){
                //偶数长度
                //三种情况:A相等,大于,小于B
                if (A[mid1] == B[mid2]) return A[mid1];
                if (A[mid1] > B[mid2]){
                    //情况2,A大,那取A的左边俩,B的右边俩,mid在上中位数
                    e1 = mid1;
                    s2 = mid2 + 1;
                }else {
                    //情况3,B大,那取A的右边俩,B的左边俩,mid在上中位数
                    s1 = mid1 + 1;
                    e2 = mid2;
                }
            }else {
                //奇数长度--大致与A类似,多了一个手动判断
                //三种情况:A相等,大于,小于B
                if (A[mid1] == B[mid2]) return A[mid1];
                if (A[mid1] > B[mid2]){
                    //先手动判断B[mid2]是否大于A[mid1-1]
                    if (B[mid2] > A[mid1 - 1]) return B[mid2];
                    //情况2,A大,那取A的左边俩,B的右边俩,mid在上中位数
                    e1 = mid1 - 1;
                    s2 = mid2 + 1;
                }else {
                    //先手动判断A[mid1]是否大于B[mid2-1]
                    if (A[mid1] > B[mid2 - 1]) return A[mid1];
                    //情况3,B大,那取A的右边俩,B的左边俩,mid在上中位数
                    s1 = mid1 + 1;
                    e2 = mid2 - 1;
                }
            }
        }
        //如果跑完while,没有结果,说明s1==e1,现在已经二分干到只有一个数了,俩数组谁小返回谁
        return Math.min(A[s1], B[s2]);
    }

2.利用二中1.的算法原型,求解本题

首先,我想说,这个解体过程,不需要你背,你只需要理解,并熟悉它,在面试场上,你自己画个图,就能把代码边界推导出来,最重要的是上面的算法原型,而应用它的话,想清楚下面的逻辑,你就能轻易套用了。

这是左神的最优解,用于面试的:O( log(min(m, n)) ) 的复杂度,非常经典,非常经典的
----下面的代码中,展示了如何利用等长数组找他们的中位数的算法原型
解决本题,不一定等长的N,M的数组A,B,找到他们俩的中位数第k小的数!!!!!先copy一下,N和M哪个小,把数组长度小的给shorts数组,令其长度为s,长度长的给longs数组,令其长度为l,方便一会操作。

分为三段,每一段,都是找A和B中的某一等长长度的数组,进算法原型去求中位数,绝不可能超过O( log(min(m, n)) ):

——第一段:你要求的第k小的数,1<=k<=s小数组的长度,那直接把AB前k个放入算法原型中求k那个数,为啥呢?
不妨设s=10,l=17,k=7你那假如所有的最小数都在A中,也不会超过7,或者全在B中,你也不会超过7’,所以呢,直接把A的0–k-1和B的0–k-1放入算法原型中,直接求完事。
从2个有序数组中找第k小那个数_第5张图片

——第二段:你要求的第k小的数,如果有k:小数组组的长度s< k <=l大数组的长度,需要手动验证长数组中k-s-1那个是不是大于短数组中s-1那个,为啥这么说呢?
不妨设k==15,我们可以淘汰掉1’2’3’4’和16’17’,他们绝不可能是结果,怎么说?
假如你A10个全部在左边,则B中最次答案也是5’,这样的话,1’2’3’4’绝不可能是答案。
假如你B15个全在左边,刚刚好,15’就是答案,而显然,16’17’他们绝不可能是答案。
而逆淘汰了一共4+2=6个之后,省下的两个数组,A中10个,B中还有11个,不等长,而你还需要剩余俩部分中第9个出来,由于不等长,我们没法将他们输入算法原型
现在这样搞,手动判定长数组中k-s-1(15-10-1=4位置,即5’)那个是不是大于短数组中s-1(即10),如果5’大于10,则5’必然是答案,都不用继续找了。

从2个有序数组中找第k小那个数_第6张图片
否则砍掉长数组中k-s-1那个,取短数组全部,长数组的k-s – k-1范围全部去算法原型中找最终的结果。

——第三段:你要求的第k小的数,大数组的长度l< k <= N + M,比如k==23
需要手动验证短数组中k-l-1那个是不是大于长数组中l-1那个
需要手动验证长数组中k-s-1那个是不是大于短数组中s-1那个
为啥呢?
看下面这个图,如果你结果左边部分包含所有A,那干掉了10个,还有13个只能从B中找,最次结果是13’,因此1‘‘–12’绝不可能是结果,排除掉12个
再类似,结果左边部分全部B,则干掉了17个,还要从A中补6个,最次也是6,那1–5他们都不可能是结果,所以淘汰掉5个
咦,注意到了么,逆淘汰的12+5=17个
余下的6–10和13’–17’找第5小的那个数,还不够长,因为你淘汰的17+5=22,它不等于k=23,所以我们还是跟第二段一样,需要手动判一下,如果13’大于等于10,则13’即答案,如果6大于等于17’,则6就是答案,排除了这两个之后,等于排除了19个,然后还从省下的8个中找第4小即可,就等价于19+4=23那个数。
从2个有序数组中找第k小那个数_第7张图片

是,返回,否则砍掉短数组中k-l-1那个,砍掉长数组中k-s-1那个,然后从短数组的k-l – s-1与长数组中k-s – l-1中放入算法原型去找结果。

代码如下:

public static int findKthMinNumBest(int[] A, int[] B, int k){
        if (A == null || B == null) return -1;
        if (k < 1 || k > A.length + B.length) return -1;

        int n = A.length;
        int m = B.length;
        //将长数组赋值给longs,短数组赋值给shorts
        int[] longs = n >= m ? A : B;
        int[] shorts = n < m ? A : B;

        int l = longs.length;
        int s = shorts.length;//重新定位长短数组的长度

        //第一段:你要求的第k小的数,1<=k<=小数组的长度,那直接把AB前k个放入算法原型
        if (k <= s){
            return getUpMedian(shorts, 0, k - 1, longs, 0,   k - 1);
        }

        //第二段:你要求的第k小的数,小数组的长度< k <=大数组的长度,需要手动验证长数组中k-s-1那个是不是大于短数组中s-1那个
        //是返回,否则砍掉长数组中k-s-1那个,取段数组全部,长数组的k-s--k-1范围全部去算法原型中找
        if (k > s && k <= l){
            if (longs[k - s - 1] > shorts[s - 1]) return longs[k - s - 1];
            return getUpMedian(shorts, 0, s - 1, longs, k - s, k - 1);
        }


        //第三段:你要求的第k小的数,大数组的长度< k <= N + M,
        // 需要手动验证短数组中k-l-1那个是不是大于长数组中l-1那个
        // 需要手动验证长数组中k-s-1那个是不是大于短数组中s-1那个
        //是,返回,否则砍掉短数组中k-l-1那个,砍掉长数组中k-s-1那个,然后从短数组的k-l--s-1与长数组中k-s--l-1中放入算法原型去找
        if (shorts[k - l - 1] > longs[l - 1]) return shorts[k - l - 1];
        if (longs[k - s - 1] > shorts[s - 1]) return longs[k - s - 1];
        return getUpMedian(shorts, k - l, s - 1, longs, k - s, l - 1);
    }

再次说明,代码,解体过程,不需要背诵,只需要自己画图,理解清楚,懂了这么做是为了搞出等长数组,放入算法原型中用即可,面试场上只需要自己再画图,自然就知道代码的边界是啥了。


总结

测试和验证代码:

public static void test(){
        int[] A = {1,2,2,3,5,7};
        int[] B = {1,4,6,8,10,12};
        int k = 5;//第5小

        System.out.println(findKthMinNum(A, B, k));
        System.out.println(findKthMinNumBest(A, B, k));
    }


    public static void main(String[] args) {
        test();
    }

提示:本算法的重要考点和知识点:

1)等长数组AB中找中点那个数的算法原型,必须理解和学会。
2)分析不等长数组,怎么分为三段,手动判别淘汰掉那些不可能的结果,然后套用等长数组的算法原型。
3)算法写出来,没有多少是根据天赋直接写的,靠的都是勤奋练习,动手,coding出来的。

你可能感兴趣的:(大厂面试高频题之数据结构与算法,java,数据结构,算法,面试,leetcode)