該文章總結自牛課網(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ù)組。
可以看出來最小元素一定在前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ù)元素的值對應的范圍,放到相應的桶里。首先我們知道,會有中間有空桶和沒有空桶兩種情況。
先考慮沒有空桶的情況:
- 這個時候n個數(shù)平均分布在n個桶里,這種情況是在輸入值增量比較平均的時候,比如輸入是{1,5,9},準備3個桶,區(qū)間分別為[1,4)[4,7)[7,10),每個桶一個元素。這種情況找相鄰兩數(shù)最大差值,只需要比較每個相鄰的桶里的元素的差值,找最大的即可。
再考慮有空桶情況:
- 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)。處理過程大致為:
- 先向右遍歷,記住遍歷過的元素中的最大值max。如果遍歷的當前元素i的值A[i]小于max,說明i是需要向左調整的,記住它。向右遍歷,只記錄需要向左調整的元素的最右的一個,記為R。
- 再從右至左遍歷一次,這次記住遍歷過的元素中的最小值min。同理,如果遍歷的當前元素i的值A[i]大于min,說明i是需要向右調整的,記住它。遍歷過程只記錄要調整的最左的一個元素,記為L。
- 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)的算法課介紹的這七道題就足夠了,而且正如之前所提到的,最重要的并不是做了多少題,記住了多少解法,而是掌握這些處理方式的技巧和思路,并能夠解決類似的問題。