1. 程式人生 > >資料結構與演算法分析筆記與總結(java實現)--排序5:快速排序練習題

資料結構與演算法分析筆記與總結(java實現)--排序5:快速排序練習題

題目:對於一個int陣列,請編寫一個快速排序演算法,對陣列元素排序。給定一個int陣列A及陣列的大小n,請返回排序後的陣列。測試樣例:[1,2,3,5,2,3],6 [1,2,2,3,3,5]

思路:

快速排序是使用二分思想,通過遞迴來實現排序的。對於一個數組,它先隨機選擇一個元素A將陣列分成兩個部分,將小於元素A的元素放到陣列的A的左邊,將大於A的元素放到A的右邊,然後再對左右兩側的子陣列分別進行遞迴的分割排序,遞迴的邊界條件是當最終分割得到的陣列只有一個元素,即被元素A分割得到的某個陣列大小是1,那麼這個大小為1的陣列就直接就是結果,不用再進行遞迴了,對於元素數目多於1的陣列繼續進行分割遞迴排序。快速排序的關鍵①隨機元素的選取,直接決定了排序的複雜度,但是通常選取陣列的中間元素作為分界元素;②已知陣列array和其中的分界元素array[k],如何將陣列中小於等於array[k]的元素放在其左邊而將大於array[k]的元素放在其右邊?這裡採取的策略是,建立一個抽象的小於等於區間{}。先將array[k]與陣列的最後一個元素array[length-1]進行交換,使得分界元素位於最後面,然後假設初始的小於等於array[k]的陣列smallArray[]在array[0]位置的左側,初始為空,即為{};然後使用兩個指標p1,p2對陣列array[]和smallArray[]進行遍歷,p1初始值是0,p2初始值是-1,將array[p1]與array[k]=A進行比較,如果array[p1]>A,那麼p2保持不變,p1++,表示這個元素大於A,不能放入到左側的{}中;如果array[p1]<A,那麼說明A元素小於分界值A,可以放入到左側的{}中,此時將p2++,表示{}要開始容納一個元素,然後將array[p2]與array[p1]進行交換,此時得到{0}5641723;然後再p1++即開始遍歷下一個元素,即當遇到一個元素array[p1]<A時總是先對p2進行p2++,再將其array[p2]與array[p1]進行交換,再對p1進行p1++。即快速排序也是基於交換的,需要進行nlogn次交換。

快速排序的時間複雜度O(nlogn),空間複雜度O(1),不穩定。

採用該思路失敗的程式碼:

import java.util.*;

//快速排序:藉助二分法的思想,使用遞迴,選定中間元素,將小於該值的元素放到左邊,大於等於的放到右邊

public class QuickSort {

    public int[]quickSort(int[] A, int n) {

        //特殊輸入

        if(A==null||A.length<=0)return A;

        //呼叫遞迴方法進行二分和元素的移動,完成排序

        this.adjust(A,0,A.length-1);

        return A;

}

//寫一個遞迴的分割方法,將陣列二分,然後以此元素為界將陣列進行移動,小於等於的位於左邊,大於等於的位於右邊

    /*public voiddivide(int[] array,int start,int end){

        //遞迴一定要寫終止的邊界條件

        if(start==end)return;

        //找任意一個元素作為分界點,但是通常選擇中間元素作為分界點

        int middle=(start+end)/2;

        //對當前的陣列根據中間點進行分界,使得小於等於的位於左邊,大於的位於右邊

        this.adjust(array,start,end);

        //繼續對2個子陣列進行二分

       this.divide(array,start,middle);

        this.divide(array,middle+1,end);

        //呼叫adjust()方法將[start,end]範圍內的陣列以middle為界進行移動使之按照大小位於2邊

        this.adjust(array,start,end);

    }

   */

    //寫一個方法adjust(),將[start,end]範圍內的元素按照中間值分成2個部分並調整大小,小於等於的在左,大的在右

    public voidadjust(int[] array,int start,int end){

         //遞迴一定要寫終止的邊界條件,這裡的邊界條件是需要分界的陣列只有一個元素

        if(start==end)return;

        //顯然分界元素是middle

        intmiddle=(start+end)/2;

        //核心邏輯,將小於middle的元素放到左邊,大於middle的元素放到右邊

        //①先將分界元素與最後的元素交換使得分界元素位於最後面

       this.swap(array,middle,end);

        //②設定指標p指向抽象的小於等於middle的陣列smallArray{}456123;指標i用來遍歷原始陣列array,每次調整都是從0開始進行遍歷調整

        int p=start-1;

        for(inti=start;i<=end;i++){

           if(array[i]<=array[end]){

                //如果元素i小於分界元素,那麼就與小於等於陣列區間{}的下一個元素進行交換

               swap(array,++p,i);

            }

        }

        /*

        ③本次調整結束,小於等於array[middle]值的元素全部位於陣列的前列,大於的位於後面,注意,middle只是位置上面的中間,並不是大小的中間值(中位數),於是調整後的陣列原來的分界值並不在中間位置,只是確保該值前面的都是小於該值的,該值後面的都是大於該值的而已。於是在此之後要繼續對分界後的2個子陣列進行分界排序,即對2個子陣列呼叫遞迴過程。注意此時的2個子陣列的分界位置是調整後的分界值所在的新位置,即上面迴圈過後的p的位置。於是對[start,p]和[p+1,end]進行調整

 *///假設執行adjust()方法後就完成了調整功能,即將[start,end]範圍的元素進行了分界,不要考慮遞迴細節

       this.adjust(array,start,p);

       this.adjust(array,p+1,end);

    }

    //專門寫一個輔助函式用來交換陣列中的2個元素

    public voidswap(int[] array,int p1,int p2){

        inttemp=array[p1];

       array[p1]=array[p2];

        array[p2]=temp;

    }

}

對於快速排序,換成這種思路:

快速排序和歸併排序一樣,都採用分治的思想,先分再合,寫一個divide(array,start,end)方法來對陣列array中從start到end範圍的陣列進行分界;顯然需要遞迴的劃分陣列,要想遞迴的劃分陣列需要求得分界元素,於是在遞迴呼叫divide()之前,寫一個分界函式partition()來確定[start,end]範圍的分界值—將陣列元素進行分界—返回分界後分界元素位於的新的位置p1。即在divide()方法中通過先呼叫partitiom()方法返回分界後的新的分界元素下標mid然後遞迴的呼叫divide(start,mid);divide(mid+1,end)來進行新的分界。

即核心的邏輯是寫一個partition(A,start,end)方法,選擇以中間位置的元素作為分界點,middle=(start+end)/2,先將這個元素與最後的元素array[end]交換位置,使得該分界元素位於陣列的末尾,然後將小於等於分界值的元素移動到陣列的前面,將大於等於分界值的元素移動到陣列的後面部分,使用的交換策略是設定2個指標p1,p2分別從陣列的start和end-1開始進行遍歷,p1逐步向後面移動,將元素逐個與分界值進行比較,如果小於分界值就不交換,直接p1++,直到遇到array[p1]>=分界值為止;p2從陣列的end-1開始向前遍歷,如果array[p2]>分界值則不交換,p2--,直到array[p2]<=分界值為止,此時array[p1]<=分界值;array[p2]>=分界值;此時將array[p1]與array[p2]進行交換,依次進行,直到p1>=p2,即p1和p2交錯時停止,此時p1所在位置是第一個大於等於分界值的元素,將array[p1]與分界值array[end]進行交換,此時完成一輪分割,於是小於等於分界值的元素都在前面部分,大於等於分界值的元素都在後面部分,分界值的新的下標是p1,即此時[start,end]陣列的分界值在p1位置(注意,這裡採取的交換策略中,對於左邊的指標p1,認為大於等於分界值的元素都應該移到右邊;對於右邊的指標p2,認為小於等於分界值的元素都應該移動到左邊,即都包含等於的情況,這樣可以使得結果均衡,避免出現最壞情況。)在完成了這一輪的分界之後,應該對分界後的2個子陣列進行遞迴的分界,即已經得到了分界值p1,於是對於[start,p1]和[p1+1,end]要分別呼叫partition()方法進行分界。

與歸併排序不同的是,歸併排序是先遞迴呼叫divide(),再呼叫非遞迴方法merge()方法進行合併;

this.divide(array,tempArray,start,middle);

this.divide(array,tempArray,middle+1,end);

this.merge(array,tempArray,start,end);

快速排序是先呼叫非遞迴方法partition()確定分界值,再遞迴呼叫divide()進行進一步分界。

int mid = partition(A, start, end);

quick(A, start , mid);

quick(A, mid+1, end); 

注意對於遞迴方法,一定要有遞迴結束的邊界條件。

注意:快速排序非常容易出錯,不僅要理解,對於易出錯的點還要記住解決方案,直接按照規範的操作來,不要隨便寫,直接避免出錯就行了。

①例如如果對於區間[6,7]進行快排,那麼(6+7)/2=6;p1=6,p2=6,將44與44進行交換,之後p1=7,p2=5,結束迴圈,將array[p1]與array[end]進行交換,即array[end]與array[end]自身進行交換。相當於沒有交換,於是程式陷入死迴圈,死遞迴最終出現棧溢位的錯誤。

這裡快速排序採用的分組方式其實很簡單,不需要找到之間元素後與最後的元素進行交換,在對i、j進行遍歷交換最後再將最後的元素更換回來並記錄分界點新的位置。採用的分組策略是這樣的:partition(array,start,end)方法用於對陣列array中[start,end]區間內的元素進行分界,注意partition()方法不是遞迴方法,它先找到[start,end]陣列中間位置的元素值,注意時值middleValue,不需要將其與最後的元素交換,然後使用2個指標從頭和尾開始向後和向前進行遍歷,這裡指標可以直接使用start和end,比較的邏輯還是一樣的,如果array[start]<middleValue則start繼續向後移動,如果array[end]>middleValue則end繼續向前移動,當遇到array[end]<=middleValue,array[start]>=middleValue時將array[start]與array[end]交換然後start++,end--;直到start>=end即交錯時結束交換並返回此時的start值到quick()方法中進行繼續的分界,此時這個返回的start作為待分界陣列的分界點,之後分別對2個子陣列進行分界即可,但是這裡千萬千萬注意,有一個麻煩的細節,在得到int middle=this.partition(array,start,end)即陣列的分界點後,通過遞迴呼叫quick()方法對兩個子陣列進行分界,此時採取的分界方式是[start,middle-1]和[middle,end即為:

this.quick(array, start, middle-1);

this.quick(array, middle , end);

為什麼不是用:

this.quick(array, start, middle);

this.quick(array, middle+1 , end);

進行分界:如果使用[start,middle]和[middle+1,end]進行分界,那麼存在一種情況:對於quick(0,2)即


對於[0][1][2]3個元素,是順序排列的,交換時在start=end=1之後start=2,end=0;此時返回的middle=2,即以[2]作為陣列[0,2]的分界,相當於沒有進行分界,於是遞迴呼叫quick(array,0,2)一直陷入死迴圈,死遞迴,最終導致棧溢位。而採用[start,middle-1]和[middle,end]可以避免這個問題。記住這個問題直接避免即可。

quick()是一個遞迴方法,它的結束的邊界條件還是if(start>=end) return;

總結:快速排序方法邏輯還是很清楚直接的,和歸併排序一樣,需要寫2個方法,quickSort(array,n)是呼叫者方法;寫一個quick(array,start,end)方法,這是一個遞迴方法,用來計算intmiddle=partition(array,start,end);即陣列[start,end]的分界點;然後遞迴呼叫quick(start,middle-1)方法和quick(array,middle,end)方法來對子陣列進行分界;關鍵是寫一個partition(array,start,end)方法,用來先找位置中間值middleVlaue,然後將陣列元素分界到middleValue的2邊,然後返回新的分界點位置,即start的位置,將其返回到quick()方法中作為int middle即陣列分界的分界點。

import java.util.*;

//快速排序,使用分治思想,通過遞迴來分割地解決問題,關鍵是返回分界之後分界點的位置,以便進行下一次的遞迴分界

public class QuickSort {

          public int[] quickSort(int[] A, int n) {

      //特殊輸入

     if(A==null||A.length<=0) return A;

      //呼叫一個遞迴的quick()方法來實現快速排序

     this.quick(A,0,A.length-1);

      return A;

  }

  //寫一個遞迴方法quick()通過遞迴呼叫自己來不斷分割給定的區間,假設執行quick(array,start,end)方法後陣列就完成排序

  public voidquick(int[] array,int start,int end){

      //遞迴方法一定要有遞迴結束的邊界條件,本題結束的邊界條件是要分割的區域只有一個元素

      if(start>=end)return;

      //呼叫partition()方法來對[start,end]範圍的陣列進行分界,並返回分界元素的位置下標

      intmiddle=this.partition(array,start,end);

      //遞迴呼叫divide()方法對已經得到的2個子陣列進行分界,假設呼叫divide()方法後陣列就可以對該範圍完成分界

//if (middle > start + 1) {不需要寫,遞迴終止條件已經可以終止遞迴

                            this.quick(array,start, middle-1);

                   //}

                   //if(middle<end) {不需要寫,遞迴終止條件已經可以終止遞迴

                            this.quick(array,middle, end);

                   //}

  }

  //核心方法partition(),用來對[start,end]範圍的陣列進行分界並且返回分界值的新下標

  public intpartition(int[] array,int start,int end){

           //先找出分界值

           int middleValue=array[(start+end)/2];

           //startend作為2個指標,分別從陣列的開頭和結尾向後和向前遍歷陣列,符合交換條件時就進行交換,不符合就繼續移動,直到2個指標交錯或者重合(重合時交換與不交換等價,於是是否包含=號不影響結果)

           while(start<=end){

                     //當陣列有序排列時是startend移動可能導致越界,但可以在後面交換條件時再進行判斷

                     while(array[start]<middleValue){

                              start++;

                     }

                     while(array[end]>middleValue){

                              end--;

                     }

                     //可以防止越界的情況

                     if(start<=end){

                             //交換2個元素的位置

                              this.swap(array,start,end);

                              start++;

                              end--;

                     }

           }

           //start是大於等於分界值的第一個元素,下一次就在以此分界點形成的2個子陣列中進行遞迴分界

           return start;

  }

  //輔助函式,專門用來交換陣列中的2個元素

  public voidswap(int[] array,int p1,int p2){

      inttemp=array[p1];

     array[p1]=array[p2];

      array[p2]=temp;

  }

}

3.堆排序

所謂堆就是優先佇列,就是先進先出的佇列,即兩端開口的序列。先將陣列建立成為大小為n的大根堆,堆頂是最大的元素,將堆頂元素與堆末尾的元素進行交換,並讓這個最大元素脫離陣列,再對剩下的堆進行排序,通過對堆進行調整,使得最大元素調整到堆頂的位置,然後再將堆頂元素與最後的元素進行交換。

4.希爾排序(shell排序)

希爾排序是插入排序的改良版本,插入排序中前面是有序序列,每次將元素array[i]插入到前面有序序列中的合適位置,直接插入排序在插入時是逐個與前面的元素進行比較,即比較的步長為1,而希爾排序中,步長是一個逐漸變小的過程,對於陣列6 5 3 1 8 7 2 4。例如第一次插入時步長為3,於是對於下標為0,1,2的元素不需要排序,從i=3即第4個元素開始進行插入,此時比較array[3]與array[0]的大小,如果array[3]<array[0],那麼就將array[3]與array[0]進行交換,說明array[3]應該插入到前面去,然後i++在比較array[4]與array[1]的大小,進行相同的交換……例如對於i=7的最後一個元素,它先與array[4]=8進行交換,此時array[4]=4,然後再與array[4-3=1]=5進行比較交換,於是array[1]=4,再往前就越界了。即對於某一個步長k,在遍歷元素時總是與array[i-k],array[i-2*k],array[i-3*k]進行比較和交換,即在步長k的情況下,對於一個元素array[i],只需要考慮array[i-k],array[i-2*3],array[i-3*k],array[i-4*k]……(直到向前越界)

這個序列即可,即抽取這幾個元素組成一個新的當前待排序的子序列陣列。比較大小決定是否進行交換,注意,一個元素array[i]要與之前的所有元素進行比較和交換,直到再往前跳躍時越界,不能僅僅比較和交換1次。對於步長k=3遍歷比較交換完成後對k進行調整,通常是k--;按照相同的過程進行遍歷比較交換,此時從元素i=k=2進行向前的比較,前面的2個元素不用考慮順序,比較array[i-2],array[i-2*2],array[i-3*2]……直到向前越界。不管步長的大小如何調整,最終步長k一定要調整為k=1,即對所有元素進行逐一比較交換,使得整個陣列完全有序。當步長k=1時的排序就是一個直接插入排序,直接插入排序其實和任意步長k的插入排序思想都是一樣的,就是逐個比較array[i-k],array[i-2*k],array[i-3*k],進行比較交換,只是當步長為k=1時的交換就是兩個相鄰元素之間的交換,前面插入排序中所將的將array[i]插入到前面有序序列中的j位置,其實就是通過對array[i-1],array[i-2],array[i-3]……逐一進行比較交換得到的,並不是找到位置後再將目標位置後面的元素統一向後移動一位,即還是基於比較交換的。

希爾排序進行了好幾輪的插入排序,看似麻煩,但是當k值較大時,排序的粒度較粗,交換的元素較少,當k逐漸減小時,當前陣列已經排序的程度逐漸提高,需要進行交換的次數變少,當最後k=1時只需要對很少的幾個元素進行交換即可。根據統計規律可以得出結論,當步長k選擇恰當時可以使得時間複雜度減少,最優時間複雜度為O(n),最劣時間複雜度為O(n^2),平均的時間複雜度為O(n^1.5),空間複雜度為O(1).

其實對於氣泡排序、插入排序、選擇排序、歸併排序、希爾排序、堆排序、快速排序,都是基於元素交換來實現的。

在寫程式碼時,步長總是從int feet=length/2開始,逐步減小為一半(常識,除以2用>>來實現),直到feet>0不再滿足,即最後一次遍歷的步長總是1;對於每一個步長feet,從i=feet(注意對於步長為1時就從第2個元素即i=1,因為總是與前面的元素進行比較,開始遍歷陣列)開始遍歷陣列,對於每個i,比較array[i]與array[i-feet]、array[i-feet-feet](直到向前越界)進行比較。如果array[i]<前面某個元素就與它進行交換,直到找到在該步長陣列中的合理的位置,即希爾排序是步長為feet的插入排序,插入的原理是不變的。希爾排序程式中有3層迴圈,最內層是對於元素array[i]遍歷前面的元素找到合適的插入位置;中間層迴圈時對每個元素進行遍歷和向前插入,外層迴圈時feet的遍歷,由於feet是有限的,所以外層迴圈複雜度是常數C而不是n,對於內層的2層迴圈,最壞情形複雜度為O(n^2),即等於直接插入排序,但是一般複雜度為O(n^1.5)。在寫程式碼時對於不同步長feet的遍歷可以使用for迴圈、while迴圈也可以使用遞迴,顯然這裡使用的是尾遞迴,很容易的,就是while迴圈的遞迴形式而已。

importjava.util.*;

//希爾排序,對步長feet進行迴圈或者遞迴地縮短,直到收斂為步長為1的直接插入排序

publicclass ShellSort{

         public int[] shellSort(int[] A, int n){

                   //特殊輸入

                   if (A == null || A.length<= 0)

                            return A;

                   //呼叫遞迴方法(尾遞迴)sort()來完成希爾排序

       //注意習慣,題目中的陣列通常以A給出,而自己喜歡用array表示陣列,因此在呼叫函式時要記住傳入的是A

                   sort(A, A.length >> 1);

                   //記得要返回排序後的結果

                   return A;

         }

         //寫一個排序方法sort(array,feet)用來使用步長feet對陣列進行插入排序,內部呼叫的方法最好寫成private方法

         private void sort(int[] array, intfeet) {

                   //遞迴一定要有停止遞迴的邊間條件

                   if (feet <= 0)

                            return;

                   //按照步長feet對陣列array進行插入排序

                   //初始位置為i=feet;初始的比較位置是index=index-feet

                   for (int i = feet; i <array.length; i++) {

// 要與前一個元素進行比較需要設定一個指標index,總是比較2個相鄰的元素array[index]array[index-feet],第一個元素是array[i]

                            int index = i;

                   //如果index-feet<0說明index不要再往前交換了,本元素已經找到了合適的位置,停止迴圈

                            while (index - feet>= 0) {

                                     if(array[index] < array[index - feet]) {

                                               //如果後一個元素比前一個元素要小,應該交換元素

                                               this.swap(array,index, index - feet);

                                               index-= feet;

                                     } else {

// 注意,還要有else,如果後面的元素大於等於前面的元素,不需要交換,說明元素array[index]之前已經找到合適的位置於是不需要再往前遍歷了,結束本元素array[i]的插入,開始下一個i的向前插入

                                               break;

                                     }

                            }

                   }

// 本步長的插入結束,此時需要更換步長feet,再次進行插入,於是遞迴呼叫sort()傳入新的步長即可

                   this.sort(array, feet>> 1);

         }

         //寫一個輔助函式用來交換2個元素

         private void swap(int[] array, int p1,int p2) {

                   int temp = array[p1];

                   array[p1] = array[p2];

                   array[p2] = temp;

         }

}