經(jīng)典排序相關面試題

該文章總結自牛課網(wǎng)的在線算法課程(https://www.nowcoder.com/)

經(jīng)典排序算法就是前面講那幾個(可以看我之前寫的關于經(jīng)典排序的文章哦~)
經(jīng)典排序(一)
經(jīng)典排序(二)堆排序
經(jīng)典排序(三)計數(shù)排序和基數(shù)排序

一般不能說某個算法就是最快的,要看實際應用情況。工程上一般是多種算法結合使用。下面貼幾道和排序有關的經(jīng)典問題和解法,題目來自牛課網(wǎng)。

我在寫解答時候盡量把思路解釋清楚再貼代碼,并且在給出最優(yōu)解(我認為的)之前有一個優(yōu)化的過程。因為在面試或在線做題時候,對于我這樣的學渣,突然拿到一個陌生題的時候很難一下子想到最優(yōu)解,此時可以先把最先想到的方法寫出來,然后(在面試官的提醒下)再逐步優(yōu)化到滿足要求的復雜度。我覺得即使沒一下子做出來,這樣也能體現(xiàn)自己解決問題的能力,還能有一個順暢的溝通過程(滿足面試官裝逼的需求。。嘿嘿)。好吧。。。以上都是我胡扯的??,能直接做出來當然是最好的。下面直接看題吧。


一、小范圍排序問題

問題:

已知一個幾乎有序的數(shù)組,幾乎有序是指,如果把數(shù)組排好順序的話,每個元素移動的距離可以不超過k,并且k相對于數(shù)組來說比較小。請選擇一個合適的排序算法針對這個數(shù)據(jù)進行排序。
給定一個int數(shù)組A,同時給定A的大小n和題意中的k,請返回排序后的數(shù)組。
測試樣例:

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

思路:

首先,因為不知道輸入值的范圍,所以計數(shù)排序不考慮。而冒泡排序和選擇排序和輸入序列是否有序無關,也不是最好選擇。。。

其實很容易想到插入排序是個不錯的選擇,因為之前講插入排序希爾排序時候都提到過,插入排序在幾乎有序情況下很好,接近線性時間。本題說每個元素移動的距離可以不超過k,也就意味著每個元素插入過程中比較交換次數(shù)不會超過k,所以用插入排序時間復雜度可以到O(n*k)。

那么有沒有更好的解決方法呢?我們接下來考慮一下O(nlogn)級別的那幾個算法有沒有合適的。

快排歸并和輸入序列是否有序也沒有關系,那堆排序呢?其實這道題的最優(yōu)解就是利用堆排序的思想做的,算是一個修改過的堆排吧。用下面這個數(shù)組舉個例子。假設n=8,k=2,輸入為下面數(shù)組。


輸入數(shù)組

可以看出來最小元素一定在前3個位置(k+1),所以我們可以建立一個大小為3的最小堆,先把前三個元素插入,再刪除最小根并放在第一個位置,然后插入第四個元素,彈出最小根放到第二個位置,依次進行直到結束。每次插入刪除操作為O(logk),執(zhí)行n次,所以這種方法可以做到時間復雜度O(nlogk)。下面是我寫的代碼。其實應該單獨寫一個Heap類,然后把堆的各種方法封裝起來,這樣更符合java規(guī)范。如果懶得自己寫Heap類,在線題也可以直接使用現(xiàn)成的優(yōu)先隊列。畢竟我們是在用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);
        }
    }
     
}

二、重復值判斷問題

問題:

請設計一個高效算法,判斷數(shù)組中是否有重復值。必須保證額外空間復雜度為O(1)。
給定一個int數(shù)組A及它的大小n,請返回它是否有重復值。
測試樣例:
[1,2,3,4,5,5,6],7
返回:true

思路:

拿到這個題目,我首先想到的是計數(shù)排序,簡單高效。實際上如果沒有額外空間復雜度的限制,我們應該用哈希表來做這道題(如果你不清楚哈希表,上次我實現(xiàn)的計數(shù)排序其實就差不多,你可以暫時把它們當一個東西理解)。用哈希表遍歷一般數(shù)組,記錄數(shù)值出現(xiàn)次數(shù)。時間復雜度和額外空間復雜度都是O(n)。

因為這里有空間復雜度限制,然后我想到可以用二重循環(huá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(n2)

能不能優(yōu)化呢?答案是肯定的。一般看到復雜度O(n2)O(n3)這樣都是有優(yōu)化空間的。我們可以利用排序,對數(shù)組排序后再進行一次遍歷比較相鄰元素有沒有重復的。先排序,后判斷,判斷過程變成O(n),現(xiàn)在就是看在原地排序算法里那個時間復雜度最低了。現(xiàn)在我們就知道,應該選取堆排序來處理排序過程啦~(不知道的同學看我前面的文章哦)

不過需要注意的是,書上給出的堆排序經(jīng)典實現(xiàn)使用的是遞歸的方式。遞歸雖然方便理解,但是遞歸需要用棧保存函數(shù),空間大小是O(logn),所以需要自己寫非遞歸的堆排。下面是我的實現(xiàn),這個代碼只寫了接口實現(xiàn),沒寫測試。

代碼:
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;
        }
    }
}

三、相鄰兩數(shù)最大差值

問題:

有一個整形數(shù)組A,請設計一個復雜度為O(n)的算法,算出排序后相鄰兩數(shù)的最大差值。
給定一個int數(shù)組A和A的大小n,請返回最大的差值。保證數(shù)組元素多于1個。
測試樣例:
[1,2,5,4,6],5
返回:2

思路:

這道題感覺有點難度,因為他要復雜度O(n),還要完成排序,然后計算相鄰兩數(shù)最大差值。這需求提的,這不是坑爹嘛?_?。


看來常規(guī)思路是行不通了。既然如此,只好放大招了,別忘了我們還有一個可以轉換時空的超必殺技,用空間換時間之究極計數(shù)排序大法!


我首先想到的滿足它時間復雜度要求的解決方案就是使用計數(shù)排序,因為他要復雜度為O(n)嘛,而我們知道的O(n)級別的算法里也就是計數(shù)排序比較靈活了。還是像上次文章里寫的一樣,用桶排序思想實現(xiàn)。先裝桶,再遍歷一遍桶,計算非空桶序號最大差值。這個方法時間上滿足了,缺點就是計數(shù)排序通病,不知道輸入值范圍,空間需求可會非常大。如果在線做題,時間緊,其實想到這里應該也可以通過大部分測試樣例了。反正他也沒要求空間復雜度,只要內存不爆炸就可以了。。。

而我看的視頻里則給出了更好的方法,下面介紹這種方法,它可以做到時間復雜度空間復雜度都是O(n)。這種方法也是利用了桶排序的思想。我們接著我上面給出的想法往下想。其實桶排序這種思想很靈活,知道輸入值范圍的時候我們可以根據(jù)值大小創(chuàng)建桶(計數(shù)排序),而如果這樣做桶的數(shù)量不可控,我們則可以根據(jù)每一位上的數(shù)字創(chuàng)建10個桶(基數(shù)排序)。

在這個問題里,我們可以只創(chuàng)建n個桶。記輸入值最小值min,最大值,max。我們要用n個桶裝范圍是minmax的n個數(shù),那么每個桶裝的范圍就是min+ixmin+(i+1)x,x是區(qū)間長度,x=(max-min+1)/n。可能這么你不太理解,舉個例子就懂了。

假如輸入{7,9,3,4,2,1,8},7個元素。我們把最大值到最小值這個范圍等量分成7個區(qū)間。換句話說就是要7個區(qū)間要能裝下1~9的數(shù)字,所以每個區(qū)間的長度就是9/7,例如第一個區(qū)間從1開始范圍就是[1,16/7),第二個區(qū)間[16/7,25/7),以此類推。

接著遍歷一遍數(shù)組,根據(jù)元素的值對應的范圍,放到相應的桶里。首先我們知道,會有中間有空桶和沒有空桶兩種情況。

先考慮沒有空桶的情況:

  1. 這個時候n個數(shù)平均分布在n個桶里,這種情況是在輸入值增量比較平均的時候,比如輸入是{1,5,9},準備3個桶,區(qū)間分別為[1,4)[4,7)[7,10),每個桶一個元素。這種情況找相鄰兩數(shù)最大差值,只需要比較每個相鄰的桶里的元素的差值,找最大的即可。

再考慮有空桶情況:

  1. n個桶,n個數(shù),有空桶意味著有的桶里不止一個元素。我們知道同一個桶里相鄰元素差值小于區(qū)間值,而空桶兩端的桶里的相鄰元素一定大于區(qū)間值。也就是說,有空桶的話,相鄰兩數(shù)最大差值處在空桶兩端的桶里的相鄰元素。這個和我們剛才在計數(shù)排序實現(xiàn)的方法里也是一樣的,很好理解。

剛剛說了半天,其實就是為了證明,我們完全不用考慮來自同一個桶的相鄰元素差值,我們只需要考慮來自不同桶的相鄰元素即可。也就是只要比較后一個桶的最小值減去前一個桶的最大值。所以我們只需要記住每個桶里的最大值和最小值,以及后一個桶的最小值和前一個桶的最大值的差值中的最大值即可。裝桶過程O(n),遍歷桶O(n),兩個操作累加,所以時間復雜度O(n)。

寫這么久有點累了。。懶得畫圖,可能直接文字描述有點不太好理解,你可以自己舉個例子,畫個圖,會更好理解一些。下面是我的代碼實現(xià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為每個桶的區(qū)間長
        int x =(max-min)/n;
        //定義桶數(shù)組B,總共有n個桶,每個桶大小為2,存儲該區(qū)間內最大值和最小值,然后初始化
        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);
        
    }
}

四、有序數(shù)組合并

問題:

有兩個從小到大排序以后的數(shù)組A和B,其中A的末端有足夠的緩沖空容納B。請編寫一個方法,將B合并入A并排序。
給定兩個有序int數(shù)組A和B,A中的緩沖空用0填充,同時給定A和B的真實大小int n和int m,請返回合并后的數(shù)組。

思路:

這道題很簡單,就是一個歸并的過程。和歸并排序里面的歸并函數(shù)做法基本一樣,但需要注意的是,這道題是把數(shù)組B加入到數(shù)組A里。我們需要從后往前比較、加入,這樣防止覆蓋掉數(shù)組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三種元素構成的整數(shù)數(shù)組,請使用交換、原地排序而不是使用計數(shù)進行排序。
給定一個只含0,1,2的整數(shù)數(shù)組A及它的大小,請返回排序后的數(shù)組。保證數(shù)組大小小于等于500。
測試樣例:
[0,1,1,0,2,2],6
返回:[0,0,1,1,2,2]

思路:

這是一個經(jīng)典的荷蘭國旗問題,處理過程和快排的劃分過程相似,可以參考快排的劃分技巧。時間復雜度O(n),空間O(1)。過程為:

遍歷數(shù)組之前,在數(shù)組左端設立“0區(qū)”,初始大小0,在數(shù)組右端設立“2區(qū)”,初始大小0。遍歷數(shù)組,如果是1,直接跳到下一個;如果是0,把當前元素與“0區(qū)”后一位交換,“0區(qū)”大小+1,遍歷下一個元素;遇到2,把當前元素與“2區(qū)”前一位交換,“2區(qū)”大小+1,由于“2區(qū)”元素并沒有遍歷過,所以不跳到后一個位置,繼續(xù)遍歷該位置元素。

其實拿到這個問題我最先想到的是用計數(shù)排序處理,只要三個桶,幾乎可以認為是原地的,簡單多了。但這里明確說要用交換,而不是計數(shù)。在在線課程下面的評論區(qū)里,有小伙伴提出和我一樣的疑問,老師的回答是,

如果數(shù)組里面放的不是int,long這種類型,而是具體一個一個實例呢?你還能壓縮在一起嗎?比如數(shù)組里面放的是“人”這個類的實例,每個實例有一個“身高”的數(shù)據(jù)項,請把小于160放左邊,160~170放中間,170以上放右邊。荷蘭國旗問題重點介紹的是一種處理數(shù)組的技巧。這種技巧從快排中來,掌握了可以解決很多類似的問題。我并不是在強調這么做才對,只是一種技巧而已。

代碼:
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;
    }
}

六、最短子數(shù)組問題

問題:

對于一個數(shù)組,請設計一個高效算法計算需要排序的最短子數(shù)組的長度。
給定一個int數(shù)組A和數(shù)組的大小n,請返回一個二元組,代表所求序列的長度。(原序列位置從0開始標號,若原序列有序,返回0)。保證A中元素均為正整數(shù)。
測試樣例:
[1,4,6,5,9,10],6
返回:2

思路:

拿到這道題,我最直接的想法就是先排序,再比較排序后的數(shù)組有變化的位置,位置有變化的元素里的最左的一個到最右的一個,這之間的數(shù)組就是題目要求的需要排序的最短子數(shù)組。這種方法需要額外空間復雜度O(n),用來保存排序后的數(shù)組(或者保存原數(shù)組各元素下標情況,這取決于具體實現(xiàn))。時間復雜度O(nlogn)。

由上面的這個思路,我們可以想到,其實只要知道,需要調整的元素里最右的元素和最左的元素的位置,就可以得到需要排序的最短子數(shù)組的長度。我們知道,如果是有序數(shù)組,一定是越往右,數(shù)值越大,越往左,數(shù)值越小,不滿足這個條件的元素,那么就是需要調整的元素。于是可以想到下面的這種處理方法。它可以做到時間復雜度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]就是需要排序的最短子數(shù)組,它的長度是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;
    }
}

七、有序矩陣查找

問題:

現(xiàn)在有一個行和列都排好序的矩陣,請設計一個高效算法,快速查找矩陣中是否含有值x。
給定一個int矩陣mat,同時給定矩陣大小nxm及待查找的數(shù)x,請返回一個bool值,代表矩陣中是否存在x。所有矩陣中數(shù)字及x均為int范圍內整數(shù)。保證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,所以再往左走,現(xiàn)在待查值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;
    }
}

八、總結

我還有本書叫《數(shù)據(jù)結構與算法經(jīng)典問題》,上面和排序有關的題還有好多,本來打算再寫幾道,但題太多實在寫不動+_+,也不想寫了,因為題總是做不完的(其實因為我好懶。。)。我感覺有牛課網(wǎng)的算法課介紹的這七道題就足夠了,而且正如之前所提到的,最重要的并不是做了多少題,記住了多少解法,而是掌握這些處理方式的技巧和思路,并能夠解決類似的問題。


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 概述:排序有內部排序和外部排序,內部排序是數(shù)據(jù)記錄在內存中進行排序,而外部排序是因排序的數(shù)據(jù)很大,一次不能容納全部...
    每天刷兩次牙閱讀 3,744評論 0 15
  • 作者: 唐征祥 山雨欲來 螞蟻搬家 來來回回 進進出出 一張廢棄的香杉床 籠罩著這忙碌王國 我從中走出一百米 然后...
    無影樹閱讀 401評論 0 7
  • 時間,過得真快。眨眼之前,還是剛進校園的那一天。那時,我還是個懵懂的孩子。大一下加入月芽,到現(xiàn)在已經(jīng)兩個年頭了...
    王師伯閱讀 2,836評論 2 2
  • 拯救家長的教育焦慮,我們該怎么做? 前段時間,有個新聞出來直接驚了小編,兩個碩士教不了一個小學生,諸如此類的新聞此...
    思碼讀高效學習法閱讀 464評論 0 0