经典排序相关面试题

作者: 锅与盆 | 来源:发表于2017-03-07 16:43 被阅读1905次

    该文章总结自牛课网的在线算法课程(https://www.nowcoder.com/)

    经典排序算法就是前面讲那几个(可以看我之前写的关于经典排序的文章哦~)
    经典排序(一)
    经典排序(二)堆排序
    经典排序(三)计数排序和基数排序

    一般不能说某个算法就是最快的,要看实际应用情况。工程上一般是多种算法结合使用。下面贴几道和排序有关的经典问题和解法,题目来自牛课网。

    我在写解答时候尽量把思路解释清楚再贴代码,并且在给出最优解(我认为的)之前有一个优化的过程。因为在面试或在线做题时候,对于我这样的学渣,突然拿到一个陌生题的时候很难一下子想到最优解,此时可以先把最先想到的方法写出来,然后(在面试官的提醒下)再逐步优化到满足要求的复杂度。我觉得即使没一下子做出来,这样也能体现自己解决问题的能力,还能有一个顺畅的沟通过程(满足面试官装逼的需求。。嘿嘿)。好吧。。。以上都是我胡扯的😂,能直接做出来当然是最好的。下面直接看题吧。


    一、小范围排序问题

    问题:

    已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以不超过k,并且k相对于数组来说比较小。请选择一个合适的排序算法针对这个数据进行排序。
    给定一个int数组A,同时给定A的大小n和题意中的k,请返回排序后的数组。
    测试样例:

    [2,1,4,3,6,5,8,7,10,9],10,2
    返回:[1,2,3,4,5,6,7,8,9,10]

    思路:

    首先,因为不知道输入值的范围,所以计数排序不考虑。而冒泡排序和选择排序和输入序列是否有序无关,也不是最好选择。。。

    其实很容易想到插入排序是个不错的选择,因为之前讲插入排序希尔排序时候都提到过,插入排序在几乎有序情况下很好,接近线性时间。本题说每个元素移动的距离可以不超过k,也就意味着每个元素插入过程中比较交换次数不会超过k,所以用插入排序时间复杂度可以到O(n*k)。

    那么有没有更好的解决方法呢?我们接下来考虑一下O(nlogn)级别的那几个算法有没有合适的。

    快排归并和输入序列是否有序也没有关系,那堆排序呢?其实这道题的最优解就是利用堆排序的思想做的,算是一个修改过的堆排吧。用下面这个数组举个例子。假设n=8,k=2,输入为下面数组。


    输入数组

    可以看出来最小元素一定在前3个位置(k+1),所以我们可以建立一个大小为3的最小堆,先把前三个元素插入,再删除最小根并放在第一个位置,然后插入第四个元素,弹出最小根放到第二个位置,依次进行直到结束。每次插入删除操作为O(logk),执行n次,所以这种方法可以做到时间复杂度O(nlogk)。下面是我写的代码。其实应该单独写一个Heap类,然后把堆的各种方法封装起来,这样更符合java规范。如果懒得自己写Heap类,在线题也可以直接使用现成的优先队列。毕竟我们是在用java,别弄得好像还在用c做题一样。我这个代码是偷懒把之前的代码拷贝过来改了改,写的很乱,大家看看就好。

    解答:
    package fuckingtest;
    
    import java.util.*;
     
    public class ScaleSort {
        public int[] sortElement(int[] A, int n, int k) {
            int[] heap=new int[k+1];
            for(int i=0;i<k+1;i++)
                insert(heap,i,A[i]);
             
            for(int i=0,count=k;i<n;i++){
                A[i]=delete(heap,count--);
                 
                if(i+k+1<n){                
                    insert(heap,++count,A[i+k+1]);
                }
            }
            return A;
        }
         
        void insert(int[] A, int count, int data){
            int i,temp;
            i=count;
            A[i]=data;
            while(i!=0&&(i-1)/2>=0&&A[(i-1)/2]>data){
                temp=A[i];
                A[i]=A[(i-1)/2];
                A[(i-1)/2]=temp;
                i=(i-1)/2;
            }
        }
         
        void down(int[] A,int count,int i){
            int r,l;
            int maxson=-1;
            for(;;){
                l=2*i+1;
                r=2*i+2;
                if(r<=count){
                    if(A[l]<A[r])
                        maxson=l;
                    else
                        maxson=r;
                }
                else if(l<=count&&r>count)
                    maxson=l;
                else if(l>count)
                    break;
                if(A[maxson]<A[i]){
                    int temp=A[i];
                    A[i]=A[maxson];
                    A[maxson]=temp;
                    i=maxson;
                }
                else
                    break;
            }
        }
         
        int delete(int[] A, int count){
            if(count==-1)
                return -1;//当堆为空时,报错
            int data = A[0];
            A[0]=A[count];
            A[count--]=0;
            down(A,count,0);
            return data;//返回删除的值
        }
         
      public static void main(String[] args) {
            int[] r=new int[10];
            int[] a=new int[]{3,1,2,6,5,4,7,8,10,9};
            ScaleSort h = new ScaleSort();
            r = h.sortElement(a,10,2);
            for(int t:r){
                System.out.println(t);
            }
        }
         
    }
    

    二、重复值判断问题

    问题:

    请设计一个高效算法,判断数组中是否有重复值。必须保证额外空间复杂度为O(1)。
    给定一个int数组A及它的大小n,请返回它是否有重复值。
    测试样例:
    [1,2,3,4,5,5,6],7
    返回:true

    思路:

    拿到这个题目,我首先想到的是计数排序,简单高效。实际上如果没有额外空间复杂度的限制,我们应该用哈希表来做这道题(如果你不清楚哈希表,上次我实现的计数排序其实就差不多,你可以暂时把它们当一个东西理解)。用哈希表遍历一般数组,记录数值出现次数。时间复杂度和额外空间复杂度都是O(n)。

    因为这里有空间复杂度限制,然后我想到可以用二重循环,逐个检查各元素是否有重复值。

    boolean cheak(int A[] ,int n ){
      for(int i=0; i<n ;i++)
        for(int j=i+1; j<n; j++)
          if(A[i]==A[j])
            return ture;
       return false;
    }
    

    额外空间复杂度O(1),时间复杂度O(n²)

    能不能优化呢?答案是肯定的。一般看到复杂度O(n²)O(n³)这样都是有优化空间的。我们可以利用排序,对数组排序后再进行一次遍历比较相邻元素有没有重复的。先排序,后判断,判断过程变成O(n),现在就是看在原地排序算法里那个时间复杂度最低了。现在我们就知道,应该选取堆排序来处理排序过程啦~(不知道的同学看我前面的文章哦)

    不过需要注意的是,书上给出的堆排序经典实现使用的是递归的方式。递归虽然方便理解,但是递归需要用栈保存函数,空间大小是O(logn),所以需要自己写非递归的堆排。下面是我的实现,这个代码只写了接口实现,没写测试。

    代码:
    import java.util.*;
     
    public class Checker {
        public boolean checkDuplicate(int[] a, int n) {
            heapSort(a,n);
            for(int i =0;i<n-1;i++){
                if(a[i]==a[i+1])
                    return true;
            }
            return false;
        }
         
        public void heapSort(int[] A, int n) {
            int count=n-1;
            build(A,count);
            for(;count>0;){
                int temp = A[0];
                A[0]=A[count--];
                down(A, count,0);
                A[count+1]=temp;
            }
            
           
                 
        }
         
        void build(int[] A,int count){
            for(int i=(count-1)/2;i>=0;i--){
                down(A,count,i);
                System.out.println(i);
            }
            System.out.println("建堆ok:");
            for(int t:A){
                System.out.println(t);
            }
            System.out.println(" ");
        }
         
        void down(int[] A,int count,int i){
            int r,l;
            int maxson=-1;
            for(;;){
                l=2*i+1;
                r=2*i+2;
                if(r<=count){
                    if(A[l]>A[r])
                        maxson=l;
                    else
                        maxson=r;
                }
                else if(l<=count&&r>count)
                    maxson=l;
                else if(l>count)
                    break;
                if(A[maxson]>A[i]){
                    int temp=A[i];
                    A[i]=A[maxson];
                    A[maxson]=temp;
                    i=maxson;
                }
                else
                    break;
            }
        }
    }
    

    三、相邻两数最大差值

    问题:

    有一个整形数组A,请设计一个复杂度为O(n)的算法,算出排序后相邻两数的最大差值。
    给定一个int数组A和A的大小n,请返回最大的差值。保证数组元素多于1个。
    测试样例:
    [1,2,5,4,6],5
    返回:2

    思路:

    这道题感觉有点难度,因为他要复杂度O(n),还要完成排序,然后计算相邻两数最大差值。这需求提的,这不是坑爹嘛¬_¬。


    看来常规思路是行不通了。既然如此,只好放大招了,别忘了我们还有一个可以转换时空的超必杀技,用空间换时间之究极计数排序大法!


    我首先想到的满足它时间复杂度要求的解决方案就是使用计数排序,因为他要复杂度为O(n)嘛,而我们知道的O(n)级别的算法里也就是计数排序比较灵活了。还是像上次文章里写的一样,用桶排序思想实现。先装桶,再遍历一遍桶,计算非空桶序号最大差值。这个方法时间上满足了,缺点就是计数排序通病,不知道输入值范围,空间需求可会非常大。如果在线做题,时间紧,其实想到这里应该也可以通过大部分测试样例了。反正他也没要求空间复杂度,只要内存不爆炸就可以了。。。

    而我看的视频里则给出了更好的方法,下面介绍这种方法,它可以做到时间复杂度空间复杂度都是O(n)。这种方法也是利用了桶排序的思想。我们接着我上面给出的想法往下想。其实桶排序这种思想很灵活,知道输入值范围的时候我们可以根据值大小创建桶(计数排序),而如果这样做桶的数量不可控,我们则可以根据每一位上的数字创建10个桶(基数排序)。

    在这个问题里,我们可以只创建n个桶。记输入值最小值min,最大值,max。我们要用n个桶装范围是minmax的n个数,那么每个桶装的范围就是min+ixmin+(i+1)x,x是区间长度,x=(max-min+1)/n。可能这么你不太理解,举个例子就懂了。

    假如输入{7,9,3,4,2,1,8},7个元素。我们把最大值到最小值这个范围等量分成7个区间。换句话说就是要7个区间要能装下1~9的数字,所以每个区间的长度就是9/7,例如第一个区间从1开始范围就是[1,16/7),第二个区间[16/7,25/7),以此类推。

    接着遍历一遍数组,根据元素的值对应的范围,放到相应的桶里。首先我们知道,会有中间有空桶和没有空桶两种情况。

    先考虑没有空桶的情况:

    1. 这个时候n个数平均分布在n个桶里,这种情况是在输入值增量比较平均的时候,比如输入是{1,5,9},准备3个桶,区间分别为[1,4)[4,7)[7,10),每个桶一个元素。这种情况找相邻两数最大差值,只需要比较每个相邻的桶里的元素的差值,找最大的即可。

    再考虑有空桶情况:

    1. n个桶,n个数,有空桶意味着有的桶里不止一个元素。我们知道同一个桶里相邻元素差值小于区间值,而空桶两端的桶里的相邻元素一定大于区间值。也就是说,有空桶的话,相邻两数最大差值处在空桶两端的桶里的相邻元素。这个和我们刚才在计数排序实现的方法里也是一样的,很好理解。

    刚刚说了半天,其实就是为了证明,我们完全不用考虑来自同一个桶的相邻元素差值,我们只需要考虑来自不同桶的相邻元素即可。也就是只要比较后一个桶的最小值减去前一个桶的最大值。所以我们只需要记住每个桶里的最大值和最小值,以及后一个桶的最小值和前一个桶的最大值的差值中的最大值即可。装桶过程O(n),遍历桶O(n),两个操作累加,所以时间复杂度O(n)。

    写这么久有点累了。。懒得画图,可能直接文字描述有点不太好理解,你可以自己举个例子,画个图,会更好理解一些。下面是我的代码实现。

    代码:
    package fuckingtest;
    
    import java.util.*;
    
    public class Gap {
        public int maxGap(int[] A, int n) {
            // write code here
            //先找最大值最小值,确定桶范围
            int min=A[0];
            int max=A[0];
            for(int i=0;i<n;i++){
                if(A[i]<min)
                    min=A[i];
                if(A[i]>max)
                    max=A[i];
            }
            if(max==min)
                return 0;
            //设x为每个桶的区间长
            int x =(max-min)/n;
            //定义桶数组B,总共有n个桶,每个桶大小为2,存储该区间内最大值和最小值,然后初始化
            int B[][]=new int[n][2];
            for(int i=0;i<n;i++){
                B[i][0]=-99999;
                B[i][1]=-99999;
            }
            //装桶
            for(int i=0;i<n;i++){
                int k=getkey(A[i],min,max,n);
                if(B[k][0]==B[k][1]&&B[k][0]==-99999)
                    B[k][1]=B[k][0]=A[i];
                if(A[i]>B[k][1])
                    B[k][1]=A[i];
                if(A[i]<B[k][0])
                    B[k][0]=A[i];
            }
            for(int i=0;i<n;i++){
                System.out.println("min:"+B[i][0]+" max:"+B[i][1]);
            }
                
            //比较后一个桶最小值和前一个桶最大值的差值
            int r=0;
            int temp;
            for(int i=0,j=1,f=1;i<n-1;i=i+f){
                System.out.println("i:"+i);
                j=1;
                f=1;
                while(B[i+j][0]==-99999){
                    j++;
                    f++;
                }
                System.out.println("j:"+j);
                temp=B[i+j][0]-B[i][1];
                if(temp>r)
                    r=temp;
            }
            return r;
        }
        
        int getkey(int v,int min,int max,int n){
            
            return (int)(v-min)*n/(max-min+1);
        }
        
        
        
        public static void main(String[] args) {
            int r;
            int[] a=new int[]{1,2,3,6,7,8};
            Gap h = new Gap();
            r = h.maxGap(a,6);
        
                System.out.println(r);
            
        }
    }
    

    四、有序数组合并

    问题:

    有两个从小到大排序以后的数组A和B,其中A的末端有足够的缓冲空容纳B。请编写一个方法,将B合并入A并排序。
    给定两个有序int数组A和B,A中的缓冲空用0填充,同时给定A和B的真实大小int n和int m,请返回合并后的数组。

    思路:

    这道题很简单,就是一个归并的过程。和归并排序里面的归并函数做法基本一样,但需要注意的是,这道题是把数组B加入到数组A里。我们需要从后往前比较、加入,这样防止覆盖掉数组A前面的有用部分。过程大致为我们每次从两个列表后面元素选取较大的一个,放入A最后,直到某一个列表到达头部,再将另一个剩下部分逆序取出。时间复杂度O(n+m),空间O(1)。

    代码:
    import java.util.*;
     
    public class Merge {
        public int[] mergeAB(int[] A, int[] B, int n, int m) {
            // write code here
            int i=n-1;
            int j=m-1;
            int k=m+n-1;
            for(;i>=0&&j>=0;){
                if(A[i]>B[j])
                    A[k--]=A[i--];
                else
                    A[k--]=B[j--];
            }
            while(i>=0){
                A[k--]=A[i--];
            }
            while(j>=0){
                A[k--]=B[j--];
            }
            return A;
        }
    }
    

    五、三色排序

    问题:

    有一个只由0,1,2三种元素构成的整数数组,请使用交换、原地排序而不是使用计数进行排序。
    给定一个只含0,1,2的整数数组A及它的大小,请返回排序后的数组。保证数组大小小于等于500。
    测试样例:
    [0,1,1,0,2,2],6
    返回:[0,0,1,1,2,2]

    思路:

    这是一个经典的荷兰国旗问题,处理过程和快排的划分过程相似,可以参考快排的划分技巧。时间复杂度O(n),空间O(1)。过程为:

    遍历数组之前,在数组左端设立“0区”,初始大小0,在数组右端设立“2区”,初始大小0。遍历数组,如果是1,直接跳到下一个;如果是0,把当前元素与“0区”后一位交换,“0区”大小+1,遍历下一个元素;遇到2,把当前元素与“2区”前一位交换,“2区”大小+1,由于“2区”元素并没有遍历过,所以不跳到后一个位置,继续遍历该位置元素。

    其实拿到这个问题我最先想到的是用计数排序处理,只要三个桶,几乎可以认为是原地的,简单多了。但这里明确说要用交换,而不是计数。在在线课程下面的评论区里,有小伙伴提出和我一样的疑问,老师的回答是,

    如果数组里面放的不是int,long这种类型,而是具体一个一个实例呢?你还能压缩在一起吗?比如数组里面放的是“人”这个类的实例,每个实例有一个“身高”的数据项,请把小于160放左边,160~170放中间,170以上放右边。荷兰国旗问题重点介绍的是一种处理数组的技巧。这种技巧从快排中来,掌握了可以解决很多类似的问题。我并不是在强调这么做才对,只是一种技巧而已。

    代码:
    public class ThreeColor {
        public int[] sortThreeColor(int[] A, int n) {
            // write code here
            int i=-1;
            int j=n;
            int temp;
            for(int k=0;k<j;){
                if(A[k]==0){
                    swap(A,++i,k++); 
                }
                else if(A[k]==2){
                    swap(A,--j,k);
                }
                else
                    k++;
            }
            return A;
        }
         
        void swap(int A[],int a,int b){
            int temp=A[a];
            A[a]=A[b];
            A[b]=temp;
        }
    }
    

    六、最短子数组问题

    问题:

    对于一个数组,请设计一个高效算法计算需要排序的最短子数组的长度。
    给定一个int数组A和数组的大小n,请返回一个二元组,代表所求序列的长度。(原序列位置从0开始标号,若原序列有序,返回0)。保证A中元素均为正整数。
    测试样例:
    [1,4,6,5,9,10],6
    返回:2

    思路:

    拿到这道题,我最直接的想法就是先排序,再比较排序后的数组有变化的位置,位置有变化的元素里的最左的一个到最右的一个,这之间的数组就是题目要求的需要排序的最短子数组。这种方法需要额外空间复杂度O(n),用来保存排序后的数组(或者保存原数组各元素下标情况,这取决于具体实现)。时间复杂度O(nlogn)。

    由上面的这个思路,我们可以想到,其实只要知道,需要调整的元素里最右的元素和最左的元素的位置,就可以得到需要排序的最短子数组的长度。我们知道,如果是有序数组,一定是越往右,数值越大,越往左,数值越小,不满足这个条件的元素,那么就是需要调整的元素。于是可以想到下面的这种处理方法。它可以做到时间复杂度O(n),额外空间复杂度O(1)。处理过程大致为:

    1. 先向右遍历,记住遍历过的元素中的最大值max。如果遍历的当前元素i的值A[i]小于max,说明i是需要向左调整的,记住它。向右遍历,只记录需要向左调整的元素的最右的一个,记为R。
    2. 再从右至左遍历一次,这次记住遍历过的元素中的最小值min。同理,如果遍历的当前元素i的值A[i]大于min,说明i是需要向右调整的,记住它。遍历过程只记录要调整的最左的一个元素,记为L。
    3. A[l]~A[R]就是需要排序的最短子数组,它的长度是R-L+1.
    代码:
    public class Subsequence {
        public int shortestSubsequence(int[] A, int n) {
            // write code here
            int r=-1;
            int l=0;
            //从左至右遍历,记录最右的当前值小于最大值情况
            for(int i=0,max=A[0];i<n;i++){
                if(A[i]>=max)
                    max=A[i];
                else
                    r=i;
            }
            //从右至左遍历,记录最左的当前值大于最小值情况
            for(int i=n-1,min=A[n-1];i>-1;i--){
                if(A[i]<=min)
                    min=A[i];
                else
                    l=i;
            }
            return r-l+1;
        }
    }
    

    七、有序矩阵查找

    问题:

    现在有一个行和列都排好序的矩阵,请设计一个高效算法,快速查找矩阵中是否含有值x。
    给定一个int矩阵mat,同时给定矩阵大小nxm及待查找的数x,请返回一个bool值,代表矩阵中是否存在x。所有矩阵中数字及x均为int范围内整数。保证n和m均小于等于1000。
    测试样例:
    [[1,2,3],[4,5,6],[7,8,9]],3,3,10
    返回:false

    思路:

    这道题可以做到时间复杂度O(m+n),额外空间复杂度O(1)。用下面这个矩阵举例说明。


    我们从右上角或左下角作为起始位置开始遍历。这么做是因为矩阵行列都是有序的,右上角是行最小,列最大,左下角相反。我们这里选择从右上角开始,假设待查值是3。当前值是5,如果待查值比当前值大,那么往下走一步,因为我们知道这一行当前位置是最大的,左面所有元素都小于该值,就不用考虑;如果待查值更小,那么往左走一步,理由同上;如果相等,返回true。待查值3<当前值5,往左走一步,当前值变成2。重复上面过程,当前值=4。3<4,所以再往左走,现在待查值3=当前值3,返回true。如果直到越界都没找到,则返回false。

    代码:
    public class Finder {
        public boolean findX(int[][] mat, int n, int m, int x) {
            // write code here
            int i=0;
            int j=m-1;
            for(;i<n&&j>-1;){
                if(mat[i][j]<x)
                    i++;
                else if(mat[i][j]>x)
                    j--;
                else
                    return true;
            }
            return false;
        }
    }
    

    八、总结

    我还有本书叫《数据结构与算法经典问题》,上面和排序有关的题还有好多,本来打算再写几道,但题太多实在写不动+_+,也不想写了,因为题总是做不完的(其实因为我好懒。。)。我感觉有牛课网的算法课介绍的这七道题就足够了,而且正如之前所提到的,最重要的并不是做了多少题,记住了多少解法,而是掌握这些处理方式的技巧和思路,并能够解决类似的问题。


    相关文章

      网友评论

      • RenesmeeLiu:这个很厉害,好像你以前给我讲题的样子哈哈!
        RenesmeeLiu:@锅与盆 哈哈,保持创作吧,我来看
        锅与盆: @RenesmeeLiu 还是那时候好,现在也没人需要我讲题了,只好写在这里。。哈哈
        锅与盆: @RenesmeeLiu 一起床就看到你的评论,好开心😁😁

      本文标题:经典排序相关面试题

      本文链接:https://www.haomeiwen.com/subject/zacmgttx.html