快速排序的兩種寫法(站在巨人的肩膀上加深理解)

前言

這是面試總結,好多要求手寫快排,堆排序,思路簡單,但是手寫還是有一定難度的,所以總結下。由於本人太渣,不足之處請各位不吝賜教,在下方留言。

在網上搜索了下,發現有兩種寫法很受歡迎,且兩種方法實現思路還是有點區別的,瞭解過區別之後,對快排應該可以更加熟悉。無論哪種寫法,本質沒變:

  • 分治
  • 將所有比temp大的移動到右邊,比temp小的移動到左邊。

分治,分而治之,在遞歸中用的尤其多,這篇博文就不會講解這個問題了,不然文章就太長了。

首先提出一個問題,快排一定要從右往左排嗎?難道不能從左往右排嗎?這個問題可以自己先想想,後面會給出答案。

這裏快排的講解按照默認的從小往大排,選取當前治理的數組序列中第一個位置。
兩種寫法中,最經典的,就是下面這種

1 挖坑填數

經典講解在這裏

這是我自己臨時手擼的,沒有驗證,主要是把自己的思想記錄下來,真正代碼可以看上面這篇博客(一下講解中,temp代表選取的比較標準)

public void quick_sort(int l,int r){
	if(l>=r)
		return ;
//算法核心:以temp爲基礎,比temp小的全部移動到左邊,比temp大的全部移動到右邊
	int i = l,j=r,temp=a[l];
	while(i<j){
		while(i<j&&a[j]>=temp){//從右往左找
			j--;
		}
		if(i<j){//說明找到了比temp小的值,把這個值填到i的坑
			a[i++] = a[j];
		}
//a[j]給a[i]了,i的坑j來填,那麼j的坑也要有人填,所以從i往右找到第一個比temp大的值,把這個值填到j的坑
		while(i<j&&a[i]<=temp){
			i++;
		}
		if(i<j){
			a[j--] = a[i];
		}
	}
	//i==j,所有坑填完了,該考慮temp這個數填哪個坑了。
	a[i]=temp;
	quick_sort(l,i-1);
	quick_sort(i+1,r);	
}

很多算法代碼的思路過程可能現在記得很清楚,但是長時間不使用,我們很快便忘記了。所以記住這個算法的這個寫法的一些特性,是回憶代碼,加深理解,以及運用的關鍵。因此我們敲完後需要多想想。
這個排序法,每次排完序後,數組中都有兩個位置的數是相同的,在一次循環完成之前,看不到temp的值,當一次循環全部完了之後,才把i == j這個坑填上temp。注意,一次循環後,所有元素相對於temp的位置就不會再變了,也就是說,此時比temp小的,最終結果仍然在temp的左邊,比temp大的仍然在temp右邊(因爲這裏遞歸左右兩邊不會相互影響。)

這裏提出個問題,如果從左往右開始找,會怎麼樣?

如果從左開始找,找到第一個比temp大的值得時候,填誰的坑?temp的坑嗎?i的坑嗎?j的坑嗎?都不行。
這裏我們可以記住,從左找,還是從右找,是以你的選取標準(這裏我們默認的選取標準是l,也就是數組的第一個元素)來的,你選取第一個位置作爲標準,那麼不管你從大到小排序,還是從小到大排序,都需要從右開始往左找。原因很簡單:從左開始找,找到的第一個比temp大(小)的數會先和0位置交換,再和後面交換,即數組兩邊都會出現比比temp大(小)的數。
例如
4,2,6,3,1,10,12,0
如果從左開始找,在6的位置找到了比temp大的,佔了4的位置,變成
6,2,6,3,1,10,12,0
那麼從右往左找的時候,找到比temp小的放到6位置,變爲
6,2,0,3,1,10,12,0
明顯看出不對。
如果你選取的標準是最後一個位置,那麼就要從左邊開始找。
理解了

while(i < j && a[j] > temp) 
	j--;

中a[j] > temp的作用了吧?保證了j右邊都數都比temp大,這樣從左邊找到比temp大的數時,才能放心放到j這裏來,同理,保證了i左邊的數都比temp小…)
這個a[j]>temp就是快排中的數能夠交換的保證。

參考這個博客:快排爲什麼從右往左排


2 直接替換 不佔坑

這個算法的詳解見

連接戳這裏

void quicksort(int left,int right) 
{ 
    int i,j,t,temp; 
    if(left>right) 
       return;               
    temp=a[left]; //temp中存的就是基準數 
    i=left; 
    j=right; 
    while(i!=j) 
    { 
       //順序很重要,要先從右邊開始找 
         while(a[j]>=temp && i<j) 
                  j--; 
         //再找左邊的 
         while(a[i]<=temp && i<j) 
                  i++; 
         //交換兩個數在數組中的位置 
         if(i<j) 
         { 
                  t=a[i]; 
                  a[i]=a[j]; 
                  a[j]=t; 
         } 
    } 
    //最終將基準數歸位 
    a[left]=a[i]; 
    a[i]=temp; 
               
    quicksort(left,i-1);//繼續處理左邊的,這裏是一個遞歸的過程 
    quicksort(i+1,right);//繼續處理右邊的 ,這裏是一個遞歸的過程 
} 

這個算法就沒有佔坑,而是直接一次性找到比他大和比他小的兩個位置,兩個位置直接替換。這是另一種寫法,這裏主要說一下剛開始看這個形式的快排算法會出現的疑問。
如上面代碼,最後一定是把temp和 i == j 這個位置替換。如果i == j 的位置的數比temp大呢?那麼也會替換嗎?如果替換,那明顯就不滿足所有左邊的數都比temp小了
仔細思考後發現,不存在這種情況: 在i==j的位置的數大於temp。看看作者在文中說的:

順序很重要,要先從右邊開始找

如果從左邊開始找會怎麼樣?
首先需要明白,找坑的時候(交換之前),讓i 停留的位置,一定是比temp大的,讓j 停留的位置,一定是比temp小的。

  • 如果從左邊開始找,i最後會停在比temp大的值的位置,這時候再找j,會發現i==j停留在比temp大的位置,最總會把會將temp和a[i]替換,也就是比temp大的放在了temp左邊。
  • 如果從右邊開始找,j會停在比temp小的值的位置,這時候在i==j的時候,仍然可以保證temp和比他小的值交換。

綜上,從右往左找很重要(這個從右開始的原因就和上面不一樣了)。

以上這兩種算法均可,個人認爲第二種更好理解和實現。


應用

這裏說一下快速排序算法的應用(排序就不說了),他的核心思想是把比temp小的數都往左移動,比temp大的數都往右移動,也就是說,假設temp的位置是i,那麼temp一定是第(i+1)小的數,第(n-i)個大的數(數組從0位置開始存儲),這個就是快排的一個經典應用,面試中經常出現——求出數組中第k個最大的數,或者求出數組的前k個大的數

int k;
public int quick_sort(int l,int r){
	if(l>=r)
		return -1;
//算法核心:以temp爲基礎,比temp小的全部移動到左邊,比temp大的全部移動到右邊
	int i = l,j=r,temp=a[l];
	while(i<j){
		while(i<j&&a[j]>=temp){//從右往左找
			j--;
		}
		if(i<j){//說明找到了比temp小的值,把這個值填到i的坑
			a[i++] = a[j];
		}
//a[j]給a[i]了,i的坑j來填,那麼j的坑也要有人填,所以從i往右找到第一個比temp大的值,把這個值填到j的坑
		while(i<j&&a[i]<=temp){
			i++;
		}
		if(i<j){
			a[j--] = a[i];
		}
	}
	//i==j,所有坑填完了,該考慮temp這個數填哪個坑了。
	a[i]=temp;
	int ans=-1;
	if(i==k)
		return a[i];
	else if(i>k)//這個數一定在左邊
		ans = quick_sort(l,i-1);
	else//這個數一定在右邊
		ans = quick_sort(i+1,r);	
	return ans;
}

###應用的應用(爲了面試方便,放在一起)
再來說一下這個求出數組的前k個大的數,這個題還有一個解法,就是堆排,堆排序我個人認爲比快排難一點,因爲涉及到一種類似遞歸的思想,建樹,刪樹,插入,不斷替換等,完全理解起來比快排難度大點。

思路:針對此題來說,可以維護一個大小爲k的最小堆,然後拿後面的點與最小堆的根節點來進行對比,如果大於根節點,就把根節點的值替換,並調整爲最小堆。

記住,前k個大的數,維護最小堆,每次比較如果比根節點大,那就替換,再調整爲最小堆(原因:我們的目標是找到前k個最大的值,如果維護最大堆,你怎麼知道子節點能不能替換?最小堆的根結點root一定是堆中最小的,如果新數比root大,說明當前最小堆不是前k個最大值組成的最小堆)。同理,前k個小的數,維護最大堆。這個最小堆的根節點一定是第k個最大的數。

##最後
再來談一下輔助空間和穩定性的問題。這裏留個空間吧,後面有時間再補上。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章