快速排序的非遞歸實現

   快速排序的思想是先從一數列中找出一個樞軸點,這個樞軸點左邊的數都比它小,右邊的數都比它大,也就是說樞軸點的位置已經是有序的了。例如 5 1 7 9  2   3 8   使用一次快速排序後會變成這樣· · · 5 · · ·。

1,2,3會在5的左邊,7,8,9在5的右邊,5在第四個位置,5是樞軸點,且不會再變動了。

 如果最終排序完畢,數列會是   1 2 3  5  7 8 9    的確5也在第四i個位置。

然後樞軸點確定好位置後,數列被分成了2個部分。即樞軸點左半段和樞軸點有半段。這兩段子數列也是無序的。因此我們在對這兩個子數列使用快速排序,分別從左邊和右邊 各找到一個樞軸點。 這時新找的這兩個樞軸點加上最開始找的樞軸點已經有3個點了,3個點把數列分成了5段!!

然後在對這5個新的子數列使用快速排序.就又會·多出一些樞軸點。

這樣下去樞軸點的數量會越來越接近原始數列的長度。每個子數列的長度會越分越趨向1。

到最後不能再分的時候,樞軸點的個數就等於了原始數列的數字的個數。我們又知道樞軸點就是按有序數列的位置放的,所以這個時候整個數列也有序了。

 

 使用遞歸對這種劃分來說很好想,只要遞歸下去,加上遞歸出口,輕而易舉的就能寫出快速排序。可是如果不使用遞歸呢。遞歸我們知道,遞歸出來的函數會佔用棧空間,且存在尾遞歸重複操作導致效率底下等問題。

 

因此使用非遞歸也是一種對於快速排序的優化。

不能使用遞歸了,我們先分析下首先面臨的問題是什麼?

由快速排序的核心代碼塊 Quick(arr,int s,int  e)

來分析。我們知道。每次快排的數列段。是要給它發送2個重要的參數的,即開始下標,和結尾下標。

對於快排的普通遞歸算法來說。使用 mid=Quick(arr,int s,int e);

用mid記錄了樞軸點。

然後再使用遞歸QuickSort(arr,s,mid-1);和QuickSort(arr,mid+1,e);

就可以不必管每次給Quick()函數的s和e到底是什麼。遞歸會一層層的更新s和e。

但是如果要使用循環,我們必須手動來存儲每個樞軸點。

 

因此我們首要任務就是創建一個數據結構來存放每次遍歷後產生的樞軸點!

我們先看下這張圖



 

圖中我們可以清楚的發現,每次產生的樞軸點 都是 在上一次快排產生的兩個樞軸點之間的!

第一次只有開頭s和結尾e這兩個樞軸點。產生了一個樞軸點mid0;

第二次就有了兩個分段,s至mid0-1 和mid0+1至e。這兩個分段每個又個產生一個樞軸點,前者產生mid1,後者產生mid2.至此就用了三個樞軸點了!

。。。。

。。。。

。。。。

以此類推

到了最後,每個分段只有一個數了,返回的樞軸點就是它本身的位置了,數列就有序了。

 

因此我們一開始要創建一個數據結構存放所有樞軸點,我們知道每次更新的樞軸點都在上次的相鄰樞軸點之間出生的,因此插入的操作肯定是非常頻繁的。因此使用雙向鏈表應該非常適用。但是本着偷懶和照顧沒有學到鏈表的讀者。我使用了數組。

創建一個數組tmp存放所有的樞軸點,並定義它的有效長度tmp_len;至於tmp的大小,最少要把它設置爲待排數列的長度。因爲到了最後樞軸點的個數就是數列的長度。

初始有效長度設置爲2。因爲最開始tmp數組要存入待排數列的開始下標和結尾下標。

因此這樣定義

int tmp[N+1]={s-1,e+1};//N爲待排數列長度

int tmp_len=2;          

那爲什麼放入的是s-1和e+1呢?我來分析。既然定義了tmp來存放樞軸點,用的時候該怎麼用呢?、

我們由遞歸算法也知道 每次取     s到 mid-1   mid+1到e。mid樞軸點的值是直接跳過的。

到時候tmp存放的全是樞軸點,用的時候也是這樣使用的Quick(arr,tmp[i]+1,tmp[i+1]-1);

意思就是隻把兩個樞軸點之間的數列進行Quick排序,樞軸點本身是不用參與進去的。

那麼如果我們定義成  int tmp[N]={s,e};  第一次循環使用Quick(arr,s+1,e-1);那豈不是開頭就錯了,把開頭和結尾的兩個值忽略了,誰又能保證它倆的大小呢?

 

至於具體的循環算法。 我們先想想什麼情況下,數列還未有序,我們要繼續排序呢。

那就是tmp保存的樞軸點不夠待排數列的長度。代表還有樞軸點沒被找出來,我們就不能結束。

因此最外面的循環while(tmp_len小於len)     【題外話:不知怎麼回事,博客中只要有'<'和‘len’連一起的時候,它們及它們後面的內容就會消失。這也是爲啥我給代碼總是截圖。難道是觸發什麼敏感詞組被屏蔽了?<和len難道是什麼黃暴信息?搞不懂!】

那麼while裏面該如何實現呢。

再回想遞歸算法,是不是該一半一半的往下分。而while已經替我們做了。while裏是不是該做一件事。

那就是每次如何分。分的結果又是什麼!

我們拿第一次劃分來舉例。第一次tmp裏只有s-1和e+1。傳入參數後Quick(arr,s,e).把數列分成了2段。並返回了一個樞軸點mid0;這個mid0是關鍵,如何處理它?是不是該把它插入到tmp中!

插入到哪個位置?是不是該插入到Quick參數的s和e之間!

     for(int k=0;k
   {
        if((tmp[k]+1)<=(tmp[k+1]-1))      //這個if條件意思是2個樞軸點不相鄰,即中間還有空出的子數列
       {                                                     //比如3和4都在tmp中相鄰。3+1>4-1  它們之間沒空位了,不進入if

                                                              //再比如7和9都在tmp中相鄰,7+1=9-1 他們之間還有空位,進入if
            ttmp=Quick(arr,tmp[k]+1,tmp[k+1]-1);  //把tmp[k]+1 和tmp[k+1]-1之間的數列進行快排,返                                                      // 回的樞軸點賦值給ttmp;ttmp將用在後面插入tmp數組中
            tmp_len++;                         //讓tmp數組有效長度+1,因爲即將插入新的樞軸點
           Insert(tmp,k+1,tmp_len,ttmp);  //把ttmp插入在tmp數組中k+1的位置。原本k+1以後的數據整體後移
            k++;            //由於k+1的位置插入了一個新樞軸點,此次循環用不到它。因此k++
      }
   }

然後第一次就劃分完了!

while會持續監測到n次,第n次tmp數組的有效長度tmp_len爲數列長度時,那就跳出,數列排序完畢。


  以下爲代碼:

 




 

運行結果:



當然如果使用數組這種數據結構來當作樞軸點存放的話 ,由於每次插入新的樞軸點都需要將後面的所有點向後移位,所以時間複雜度很高。正確的方式應該使用雙向鏈表。每次插入的複雜度都是0(1),時間複雜度會有很大很大的改善。用數組只是方便寫代碼表達非遞歸的思想。如果寫非遞歸的快排目的是爲了優化效率,那千萬不可用數組,否則複雜度會比原版的遞歸還要高很多。