七大排序算法


  排序算法種類繁多根據處理的數據規模與存儲特點,可分為內部排序和外部排序:前者處理的數據規模不大,內存足以容納;后者處理的數據規模較大,必須將數據存放於外部存儲器中,每次排序的時候需要訪問外存。根據輸入的不同形式,分為脫機算法和在線算法:前者待排序的數據是以批處理的形式給出的;而在雲計算之類的環境中,待排序的數據是實時生成的,在排序算法開始運行時,數據並未完全就緒,而是隨着排序算法本身的進行而逐步給出的。另外,針對不同的體系結構,又分為串行和並行兩大類排序算法。根據算法是否采用隨機策略,還有確定式和隨機式之分。

冒泡排序(O(n^2))

冒泡排序是比較相鄰兩數的大小來完成排序的。這里定義比較邊界,也就是進行大小比較的邊界。對於長度為n的數組,第一趟的比較邊界為[0,n-1],也就是說從a[0]開始,相鄰元素兩兩比較大小,如果滿足條件就進行交換,否則繼續比較,一直到最后一個比較的元素為a[n-1]為止,此時第一趟排序完成。以升序排序為例,每趟排序完成之后,比較邊界中的最大值就沉入底部,比較邊界就向前移動一個位置。所以,第二趟排序開始時,比較邊界是[0,n-2]。對於長度為n的序列,最多需要n趟完成排序,所以冒泡排序就由兩層循環構成,最外層循環用於控制排序的趟數,最內層循環用於比較相鄰數字的大小並在本趟排序完成時更新比較邊界。

具體代碼如下:

 1  //冒泡排序
 2     public static void bubbleSort(int[] arr,int len){
 3         int temp=0;
 4         int compareRange=len-1;//冒泡排序中,參與比較的數字的邊界。
 5         //冒泡排序主要是比較相鄰兩個數字的大小,以升序排列為例,如果前側數字大於后側數字,就進行交換,一直到比較邊界。
 6         for (int i = 0; i <len ; i++) {//n個數使用冒泡排序,最多需要n趟完成排序。最外層循環用於控制排序趟數
 7             for (int j = 1; j <=compareRange ; j++) {
 8                 if(arr[j-1]>arr[j]){
 9                     temp=arr[j-1];
10                     arr[j-1]=arr[j];
11                     arr[j]=temp;
12                 }
13             }
14             compareRange--;//每進行一趟排序,序列中最大數字就沉到底部,比較邊界就向前移動一個位置。
15         }
16         System.out.println("排序后數組"+Arrays.toString(arr));
17     }
View Code

  在排序后期可能數組已經有序了而算法卻還在一趟趟的比較數組元素大小,可以引入一個標記,如果在一趟排序中,數組元素沒有發生過交換說明數組已經有序,跳出循環即可。優化后的代碼如下:

 1 public static void bubbleSort2(int[] arr,int len){
 2         int temp=0;
 3         int compareRange=len-1;//冒泡排序中,參與比較的數字的邊界。
 4         boolean flag=true;//標記排序時候已經提前完成
 5         int compareCounter=0;
 6         //冒泡排序主要是比較相鄰兩個數字的大小,以升序排列為例,如果前側數字大於后側數字,就進行交換,一直到比較邊界。
 7        while(flag) {
 8            flag=false;
 9             for (int j = 1; j <=compareRange ; j++) {
10                 if(arr[j-1]>arr[j]){
11                     temp=arr[j-1];
12                     arr[j-1]=arr[j];
13                     arr[j]=temp;
14                     flag=true;
15                 }
16             }
17            compareCounter++;
18            compareRange--;//每進行一趟排序,序列中最大數字就沉到底部,比較邊界就向前移動一個位置。
19         }
20         System.out.println("優化后排序次數:"+(compareCounter-1));
21         System.out.println("排序后數組"+Arrays.toString(arr));
22     }
View Code

  還可以利用這種標記的方法還可以檢測數組是否有序,遍歷一個數組比較其大小,對於滿足要求的元素進行交換,如果不會發生交換則數組就是有序的,否則是無序的。

  兩種方法的排序結果如下所示:

插入排序(O(n^2))

將待排序的數組划分為局部有序子數組subSorted和無序子數組subUnSorted,每次排序時從subUnSorted中挑出第一個元素,從后向前將其與subSorted各元素比較大小,按照大小插入合適的位置,插入完成后將此元素從subUnSorted中移除,重復這個過程直至subUnSorted中沒有元素,總之就時從后向前,一邊比較一邊移動。

對應代碼如下: 

 1  //直接插入排序
 2     public static void straightInsertSort(int[] arr,int len){
 3         int temp=0;
 4         int j=0;
 5         for (int i = 1; i <len ; i++) {//待插入的數字
 6             for (j =i-1; j>=0; j--) {//有序區間
 7                 if(arr[i]>arr[j]){
 8                     break;
 9                 }
10             }
11                 if(j!=i-1){
12                     temp=arr[i];
13                     for (int k =i-1; k >j ; k--) {
14                         arr[k+1]=arr[k];//從后向前移動數組
15                     }
16                     arr[j+1]=temp;
17                 }
18            // System.out.println("直接插入排序后數組" + Arrays.toString(arr));
19 
20         }
21         System.out.println("直接插入排序后數組" + Arrays.toString(arr));
22 
23     }
24     //直接插入排序簡潔版
25     public static void straightInsertSort2(int[] arr,int len){
26         int temp=0;
27         int j=0;
28         for (int i = 1; i <len ; i++) {
29             if(arr[i]<arr[i-1]){
30                 temp=arr[i];
31                 for (j = i-1; j>=0&&temp<arr[j] ; j--) {
32                     arr[j+1]=arr[j];//從后向前移動數組
33                 }
34                 arr[j+1]=temp;
35             }
36         }
37         System.out.println("直接插入排序后數組" + Arrays.toString(arr));
38     }
View Code

添加一個易於理解的版本:

 1  //插入排序易理解版
 2     public static void straightInsertSort3(int[] arr,int len){
 3         int high=0;//有序區間的上界(包括)
 4         int insertValue=0,i=0,j=0;
 5         while (high<len-1){
 6             i=high;
 7             insertValue=arr[i+1];
 8             while (i>=0&&insertValue<arr[i]){
 9                 --i;
10             }
11             for (j =high; j>=i+1 ; j--) {
12                 arr[j+1]=arr[j];
13             }
14             arr[i+1]=insertValue;
15             ++high;
16         }
17         System.out.println("直接插入排序后數組" + Arrays.toString(arr));
18     }
View Code

新版本: 

 1  public static void insertSort(int arr[],int len){
 2         int tmp=-1;
 3         int soretdIndex=0;
 4         for (int i = 1; i <len ; i++) {
 5             tmp=arr[i];
 6             soretdIndex=i-1;
 7             while (soretdIndex>=0&&arr[soretdIndex]>tmp){
 8                 arr[soretdIndex+1]=arr[soretdIndex];
 9                 soretdIndex--;
10             }
11             arr[soretdIndex+1]=tmp;
12         }
13     }
View Code

 哨兵版本:

 1 public static void insertSort2(int[] arr,int len){//arr[0]作為哨兵,arr[1...len]是待排序元素,len是其個數
 2         for (int i = 2; i <=len ; i++) {
 3             arr[0]=arr[i];
 4             int j;
 5             for(j=i-1;arr[0]<arr[j];--j){
 6                 arr[j+1]=arr[j];
 7             }
 8             arr[j+1]=arr[0];
 9         }
10     }
View Code

希爾排序(O(n*(log n)^2)

由希爾在1959年提出,基於插入排序發展而來。希爾排序的思想基於兩個原因:

1)當數據項數量不多的時候,插入排序可以很好的完成工作。

2)當數據項基本有序的時候,插入排序具有很高的效率。

基於以上的兩個原因就有了希爾排序的步驟:

a.將待排序序列依據步長(增量)划分為若干組,對每組分別進行插入排序。初始時,step=len/2,此時的增量最大,因此每個分組內數據項個數相對較少,插入排序可以很好的完成排序工作(對應1)。

b.以上只是完成了一次排序,更新步長step=step/2,每個分組內數據項個數相對增加,不過由於已經進行了一次排序,數據項基本有序,此時插入排序具有更好的排序效率(對應2)。直至增量為1時,此時的排序就是對這個序列使用插入排序,此次排序完成就表明排序已經完成。

  可以看出,每次排序的步長逐漸縮小,新的一輪排序就是在上輪已排好序的分組中,添加一個新元素,然后對這個已基本有序的序列使用插入排序,這種條件下,插入排序具有最高的排序效率。實現代碼如下:

 1 //希爾排序
 2     public static void shellSort(int[] arr,int len){
 3         int step=len/2;//step既是組數又是步長。
 4         int temp=0;
 5         int k=0;
 6         while (step>0){
 7             for (int i = 0; i <step ; i++) {//將待排序序列分組
 8 
 9                 for (int j = i+step; j <len;j+=step) {//每個分組使用直接插入排序
10                     if(arr[j]<arr[j-step]){
11                         temp=arr[j];//待插入元素
12                         for (k =j-step;k>=0&&temp<arr[k];k-=step) {//后移較大的元素
13                             arr[k+step]=arr[k];
14                         }
15                         arr[k+step]=temp;
16                     }
17                 }
18             }
19             step/=2;//更新步長。
20         }
21         //System.out.println("希爾排序后的數組為:"+Arrays.toString(arr));
22     }
View Code

  以上代碼不夠簡潔,還可以進一步改進。事先不必分組,可以從第step個元素開始,從左向右掃描余下的序列,與索引值相差step的元素比較大小,也就是說將[step,2*step-1]與[0,step-1]區間內對應的元素比較,較大就保持不動,較小就移動至相關位置。然后再將[2*step,3*step-1]與[step,2*step-1]相比,依此類推,制止掃描到最后一個元素。實現代碼如下:

 1  public static void shellSort2(int[] arr,int len){
 2         int temp=0;
 3         int step=len/2;
 4         int j=0;
 5         while (step>0){
 6             for (int i = step; i <len ; i++) {//從第setp個元素開始,將其與之前的元素相比
 7                 if (arr[i]<arr[i-step]){//使用直接插入排序
 8                     temp=arr[i];
 9                     j=i-step;
10                     while (j>=0&&arr[j]>temp){
11                         arr[j+step]=arr[j];
12                         j-=step;
13                     }
14                     arr[j+step]=temp;
15                 }
16             }
17             step/=2;
18         }
19         //System.out.println("希爾排序2后的數組為:"+Arrays.toString(arr));
20     }
View Code

新版本: 

 1 public static void shellSort3(int[] arr,int len){
 2         int j=0,tmp=0;
 3         for (int d = len/2; d >0 ; d/=2) {//d是增量,也是排序時的分組數。
 4             for (int i = d; i <len; i++) {//0~d-1是各分組的第一個元素,作為初始時插入排序的有序序列。
 5                 j=i-d;      //得到i所在的分組中,其前一個元素(有序的)
 6                 tmp=arr[i];
 7                 while (j>=0&&arr[j]>tmp){
 8                     arr[j+d]=arr[j];
 9                     j-=d;
10                 }
11                 arr[j+d]=tmp;
12             }
13         }
14     }
View Code

   希爾排序中等大小規模表現良好,對規模非常大的數據排序不是最優選擇。但是比O(n^2)復雜度的算法快得多。並且希爾排序非常容易實現,算法代碼短而簡單。 此外,希爾算法在最壞的情況下和平均情況下執行效率相差不是很多,與此同時快速排序在最壞的情況下執行的效率會非常差。幾乎任何排序工作在開始時都可以用希爾排序,若在實際使用中證明它不夠快,再改成快速排序這樣更高級的排序算法

選擇排序(O(n^2))

像插入排序那樣,將待排序序列划分為有序區和無序區(整個待排序序列)。

1)不過不同的是,初始時,有序區為空,無序區是整個待排序序列。

2)通過比較在無序區中得到最小的記錄值,將其與無序區第一個位置的元素交換,有序區就增加了一個元素,同時無序區減少了一個元素。

3)重復上述操作,直至無序區中元素個數為0。

實現代碼如下:

 1 public static void selectSort(int[] arr,int len){
 2         int temp=0;
 3         int minIndex=-1;
 4         for (int i = 0; i <len ; i++) {//i是有序區最后一個位置的右側
 5             minIndex=i;
 6             for (int j =i; j <len-1 ; j++) {//無序區
 7                 if(arr[minIndex]>arr[j+1]){
 8                     minIndex=j+1;
 9                 }
10             }
11             temp=arr[i];//有序區的最后一個位置的右側
12             arr[i]=arr[minIndex];//將最小值放至有序區的最后一個位置上的右側,覆蓋原先值。
13             arr[minIndex]=temp;//將有序區最后一個位置的右側的原先值賦值給無序區的最小值處。
14         }
15         System.out.println("選擇排序后的數組為:"+Arrays.toString(arr));
16     }
View Code

  這里補充不使用臨時變量對兩個數值進行交換的方法。實現代碼如下:

 1     //使用加減法來完成不使用臨時變量進行交換的目的,不過當a、b很大時,可能會溢出。
 2     public int[] swap1(int a,int b){
 3         a=a+b;
 4         b=a-b;
 5         a=a-b;
 6         return new int[]{a,b};
 7     }
 8     //使用異或運算完成不使用臨時變量進行交換,使用異或進行兩數交換時,兩數不能相等
 9     public int[] swap2(int a,int b){
10         if(a!=b) {//使用異或進行兩數交換時,兩數不能相等
11             a ^= b;
12             b ^= a;
13             a ^= b;
14         }
15         return new int[]{a,b};
16     }
View Code

  解釋下使用異或進行交換的原理。異或位運算,當兩位相同時,結果為1,否則為0。使用異或進行交換的原理如下圖所示:

 

堆排序(O(n*log n))

堆的概述

堆排序是基於選擇排序的改進,目的是較少比較次數。一趟選擇排序中,僅保留了最小值,而堆排序排序不僅保留最小值,還把較小值保留下來,減少了比較小次數。

堆是這樣一種完全二叉樹:根節點的值大於等於左右孩子節點的值(最大堆)或者根節點的值小於等於左右孩子節點的值(最小堆)。堆也是遞歸定義的,即堆的孩子節點本身也是堆。使用數組存儲堆。

堆具有以下性質:

1)完全二叉樹A[0:n-1]中的任意節點,索引為i的節點,其左右孩子節點是2*i+1和2*i+2。

2)非葉子節點最大索引是⌊n/2⌋-1,葉子節點最小索引是⌊n/2⌋。

3)最大(最小)堆的左右子樹也是最大(小)堆。

如果是升序排列,就使用最大堆,反之使用最小堆。以下假設是升序排列。

排序方法

堆排序可以分為兩個過程:構建初始堆和重建堆。

構建初始堆

由於每個葉子節點本身就是以這個葉節點作為根節點的堆,而構建堆的目的就是使以每個節點作為根節點的樹都滿足堆的定義,因此從堆(完全二叉樹)的最下側非葉子節點開始構建初始堆,根據堆的性質,這個節點的索引是⌊n/2⌋-1。從下向上,一直到堆頂節點也滿足堆的定義,表示完成堆的初始化。把以某個節點為根節點的樹調整為堆的方法如下:

1)設這個節點為i,其左孩子為j(完全二叉樹中某個節點如果只有一個子節點,那么一定是左節點)。

2)如果arr[j]<arr[j+1],那么++j(指向右孩子)。

3)如果arr[i]>arr[j],說明這個以節點為根的樹已經滿足堆的定義,算法結束。

4)否則,swap(arr[i],arr[j]),由於交換過程中破壞了原來以j為根節點的樹的堆結構,所以以j為當前調整節點轉步驟1,如果j為葉子節點則迭代結束(葉子節點本身就是堆)。

調整節點的方法接口是adjust(int[] arr,int k,int m),其中arr是待排序序列,k是待調整節點的索引值,m是堆的最大索引值。實現代碼如下: 

 1  public static void adjust(int[] arr,int k,int m){
 2         int tmp;
 3         int i=k;//要調整的節點
 4         int j=2*k+1;//調整節點的左孩子
 5         while (j<=m){//最新的調整節點的左孩子索引值不能超過堆的最大索引
 6          if(j<m&&arr[j]<arr[j+1])
 7              ++j;//得到左右孩子中的最大值節點
 8          if(arr[i]>arr[j]){
 9              break;
10          }
11          else {
12              tmp = arr[i];
13              arr[i] = arr[j];
14              arr[j] = tmp;
15              i = j;
16              j = 2 * i + 1;
17          }
18         }
19     }
View Code

 重建堆

完成了初始堆的創建之后,就可以通過不斷的重建堆進行堆排序,每次重建堆就是一趟排序,每次重建時都將堆頂節點與堆無序區的最后一個元素交換,因此每趟堆排序后堆的有序區就增加了一個元素(從數組最后與各元素開始,向前排列),下輪就使用無序區組成的堆進行重建,每次重建都只是對堆頂節點的調整,因為初始堆建成之后,其他節點都滿足堆的定義。實現代碼如下:

 1  //堆排序,m是待排序序列的大小
 2     public static void heapSort(int [] arr,int m){
 3         //創建初始堆
 4         int lastEleIndex=m-1;//無序區的最后一個元素的索引值
 5         int tmp;
 6         //從最下側的非葉子節點開始,向上創建初始堆
 7         for (int i = m/2-1; i >=0 ; i--) {
 8             adjust(arr,i,lastEleIndex);
 9         }
10         //重建堆,每趟將堆頂節點與堆無序區最后一個元素交換,然后再調整新堆頂節點。每趟完成之后完成了一個元素的排序
11         for (int i = 0; i <m ; i++) {
12             tmp=arr[0];
13             arr[0]=arr[lastEleIndex];
14             arr[lastEleIndex]=tmp;
15             adjust(arr,0,--lastEleIndex);
16         }
17         System.out.println();
18     }
View Code

堆排序特點

創建初始堆的時間復雜度是O(n),簡單的解釋是有n/2個節點需要調整,每次調整節點時只是上寫移動常數個節點,因此創建初始堆的時間復雜度是O(n)。而實際進行堆排序時,需要進行n趟,每趟進行堆重建時就是調整堆頂節點,最多移動次數不會超過書的高度O(log n),因此時間復雜度是O(n*log n)。

堆排序對數據的原始排列狀態並不敏感,所以其最壞時間復雜度、最好時間復雜度、平均時間復雜度均是O(n*log n),堆排序不是一種穩定的排序算法。

歸並排序(O(n*log n))

概述

歸並的含義就是將兩個或多個有序序列合並成一個有序序列的過程,歸並排序就是將若干有序序列逐步歸並,最終形成一個有序序列的過程。以最常見的二路歸並為例,就是將兩個有序序列歸並。歸並排序由兩個過程完成:有序表的合並和排序的遞歸實現。

 

有序表的合並

雖然說是兩個有序表的合並,不過這里並不是使用兩個數組進行合並,而是通過數組索引的形式“描述”兩個待合並的有序表,合並的方法簽名如右所示mergeArray(int arr[],int tmp,int low,int mid,int high),其中low是合並有序表t1的起始位置,mid是t1的終止位置,mid+1是t2的起始位置,high是t2的終止位置,最后tmp是存儲合並后元素的臨時數組。有序表合並完成后,將臨時數組tmp中元素復制到原數組相應位置。兩個有序數組合並,其代碼實現如下: 

 1 public static void mergeArray2(int[] arr,int tmp[],int low,int mid,int high){
 2         int i=low;
 3         int j=mid+1;
 4         int k=low;
 5         //將合並后的元素存到臨時數組中
 6         while (i<=mid&&j<=high){
 7             if(arr[i]<arr[j]){
 8                 tmp[k++]=arr[i++];
 9             }
10             else {
11                 tmp[k++]=arr[j++];
12             }
13         }
14         while (i<=mid){
15             tmp[k++]=arr[i++];
16         }
17         while (j<=high){
18             tmp[k++]=arr[j++];
19         }
20         //將臨時數組中內容賦值給原數組
21         for (int l =low ; l <=high ; l++) {
22             arr[l]=tmp[l];
23         }
24     }
View Code

 非遞歸形式

   非遞歸形式的歸並排序的實現中,關鍵是假設每個part1都有一個與之對應的part2,以part2的右邊界high為兩序列進行合並的檢測條件。以part2的左邊界mid為判斷整個待排序序列最尾部的part1是否有對應的part2的檢測條件。對於沒有對應part2的有序表不做任何處理,對應代碼如下:

 1  //二路歸並排序,非遞歸版本
 2     public static void mergeSort(int[] array,int len){
 3         int eachGroupNumbers=1;
 4         int[] temp=new int[len];
 5         int high=-1;
 6         int low;
 7         while (eachGroupNumbers<=len){
 8             low=0;
 9             high=low+2*eachGroupNumbers-1;
10             //兩兩合並數組的兩個有序序列
11             //假設每個part1都有對應的part2
12 
13             //以high作為邊界檢測條件,如果part2的右邊界high小於整個待排序序列的右邊界,則兩個有序序列進行合並。
14             for (; high<len ; high=low+2*eachGroupNumbers-1) {//以high作為邊界檢測條件
15                 mergeArray(array,low,low+eachGroupNumbers-1,high,temp);
16                 low=high+1;
17             }
18             /*
19             跳出循環,說明part2的右邊界已經超出了整個待排序序列的右邊界。
20             如果part2的左邊界mid還在整個序列的右邊界內,將兩序列進行合並,
21              */
22             if(low+eachGroupNumbers-1<len){//以mid作為邊界檢測條件
23                 mergeArray(array,low,low+eachGroupNumbers-1,len-1,temp);
24             }
25             /*
26             如果part2的左邊界也不在整個序列的右邊界范圍內,說明這個part1並沒有對應的part2,不做任何處理。
27              */
28             //本輪合並完成,繼續划分數組
29             eachGroupNumbers=eachGroupNumbers<<1;
30             //System.out.println("本輪的結果:"+Arrays.toString(array));
31             }
32         System.out.println("歸並排序后的數組為:"+Arrays.toString(array));
33     }
View Code   

遞歸形式

將待排序序列分為A和B兩部分,如果A和B都是有序的,只需要調用有序序列的合並算法mergeArray就完成了排序,可是A和B不是有序的,再分別將A和B一分為二,直至最終的序列只有一個元素,我們認為只有一個元素的序列是有序的,合並這些序列,就得到了新的有序序列,然后返回給上層調用者,上上層調用這再合並這些序列,得到更長的有序序列,這就是遞歸形式的歸並排序,示意圖如下圖所示

 

圖片來自:http://alinuxer.sinaapp.com/?p=141)。使用上述遞歸樹分析歸並排序的時間復雜度,以遞歸實現歸並排序時,是自頂向下將待排序序列一分為二,直至每個子序列元素為1。所以遞歸樹高度為log n。由於每層元素個數為n個,所以每層中,兩個有序表合並為一個新有序表時的比較次數不超過n,因此歸並排序的時間復雜度是O(n*log n),並且好像無所謂最好情況、最差情況,所有情況下時間復雜度都是O(n*log n)。

實現代碼如下: 

1  public static void mergeSort2(int[] arr,int[] tmp,int low,int high){
2         if(low<high){
3             int mid=low+(high-low)/2;
4             mergeSort2(arr,tmp,low,mid);
5             mergeSort2(arr,tmp,mid+1,high);
6             mergeArray2(arr,tmp,low,mid,high);
7         }
8     }
View Code

 歸並排序是一種穩定的排序。 

快速排序(O(n*log n))

快速排序是圖靈獎得主 C. R. A. Hoare 於 1960 年提出的一種划分交換排序。它采用了一種分治的策略,通常稱其為分治法(Divide-and-ConquerMethod)。快速排序由分區和遞歸排序兩個過程完成。

分區

分區分為三個步驟:

1.在數組中,選擇一個元素作為“基准”(pivot),一般選擇第一個元素作為基准元素。設置兩個游標i和j,初始時i指向數組首元素,j指向尾元素。
2.從數組最右側向前掃描,遇到小於基准值的元素停止掃描,將兩者交換,然后從數組左側開始掃描,遇到大於基准值的元素停止掃描,同樣將兩者交換。

3.i==j時分區完成,否則轉2。(參考)

  

分區的實現代碼,如下: 

 1 public static int partition2(int[] arr,int low,int high){
 2         int i=low;//左游標,選擇數組第一個元素作為基准值
 3         int j=high;//右游標
 4         //i==j表示分區過程的結束
 5             while (i <j) {
 6                 //從右側向左掃描,找到小於基准值的元素,使用i<j防止數組越界(原數組可能升序排列)
 7                 while (i<j&&arr[j] >=arr[i]) {
 8                     --j;
 9                 }
10                 //上述循環結束可能是因為i==j,原數組升序排列導致。如果是這樣的話,分區就可以結束了
11                if(i<j)
12                 {
13                     arr[i] ^= arr[j];
14                     arr[j] ^= arr[i];
15                     arr[i] ^= arr[j];
16                     ++i;
17                 }
18                 //從左側向右掃描,找到大於基准值arr[j]的元素,使用i<j防止數組越界(只是此時原數組可能降序排列)
19                 while (i<j&&arr[i] <= arr[j]) {
20                     i++;
21                 }
22                 //上述循環結束可能是因為i==j,原數組降序排列導致。如果是這樣的話,分區就可以結束了
23                 if(i<j)
24                 {
25                     arr[i] ^= arr[j];
26                     arr[j] ^= arr[i];
27                     arr[i] ^= arr[j];
28                     --j;
29                 }
30             }
31 
32         return i;
33     }
View Code

 遞歸形式排序

   每次分區之后,基准值所處的位置(storeIndex)就是最終排序后它的位置,並且,一次分區之后,數據集一分為二,在分別對兩側的新分區進行分區,直至最后每個子數據集中只剩下一個元素,代碼實現如下: 

1   public static void quickSort2(int [] arr,int low,int high){
2         if(low<high){
3             int mid=partition2(arr,low,high);
4             quickSort2(arr,low,mid-1);
5             quickSort2(arr,mid+1,high);
6         }
7     }
View Code

   快速排序最好情況是每次分區后,都將序列等分為兩個長度基本相等的子序列(也就是分區后基准元素都位於序列中間位置)。第一次分區后,子序列長度為n/2,第二次分區后,子序列長度為n/4,第i次分區后子序列長度為n/(2^i),直到子序列長度為1。設經過x次分區后子序列長度為1,則有n/2^x=1,則x=log n,也就是說最好情況下經過log n次分區完成排序。使用遞歸樹來理解快速排序的最好時間復雜度。遞歸樹的高度就是分區次數,由上述計算可知,遞歸樹的高度是log n。在遞歸樹的每一層總共有n個節點,並且各子序列在分區的時候關鍵字的比較次數不超過n,所以就有基本操作次數不超過n*log n。所以,快排在理想情況下的時間復雜度是O(n*log n)。

           快速排序理想情況下的遞歸樹

最壞情況

  當我們每次進行分區划分時,如果每次選擇的基准元素都是當前序列中最大或最小的記錄,這樣每次分區的時候只得到了一個新分區,另一個分區為空,並且新分區只是比分區前少一個元素,這是快速排序的最壞情況,時間復雜度上升為O(n^2),因為遞歸樹的高度為n。所以,有人提出隨機選擇基准元素,這樣在一定程度上可以避免最壞情況的發生,但是理論上最壞情況還是存在的。參考

  由於快速排序是使用遞歸實現的,所以其空間復雜度就是棧的開銷,最壞情況下的遞歸樹高度是n,此時空間復雜度是O(n),一般情況下遞歸樹的長度是log n,此時空間復雜度是O(log n)。

快速排序適用於待排序記錄個數很多且分布隨機的情況,並且快拍是目前內排序中排序算法最好的一種。

總結

各排序方法接口形式  

冒泡排序、插入排序、希爾排序、選擇排序和堆排序的排序方法接口均是sort(int[] arr,int len)的形式,其中len是序列長度。歸並排序由於需要臨時數組存放兩有序表合並的結果,排序方法接口是sort(int[] arr,int [] tmp,int low,int high),而快速排序不需要臨時數組,其排序方法接口是sort(int[] arr,int low,int high)。

各排序算法的比較 

排序算法的穩定性是指排序前后具有相同關鍵字的記錄,相對順序保持不變。形式化的定義就是,排序之前有ri=rj,ri在rj之前,而在排序之后ri仍然在rj之前,就說這種算法是穩定的。各排序算法的比較如下所示:

1)排序時,可以先嘗試一種較慢但簡單的排序,例如插入排序,如果還是慢,可以選擇希爾排序(數據量在5000以下時很有用),還是慢的話,就使用快速排序,堆排序和歸並排序在某些程度上較快速排序慢。

2)一般在不要求穩定性的場景下,使用快速排序就可以了。但是,當我們每次進行分區划分時,選擇的基准元素都是最小(排序目的是升序)/最大(排序目的是降序),這是快速排序的最壞情況,時間復雜度上升為O(n^2),這時可以使用堆排序和歸並排序。在n較大時,相對堆排序來說,歸並排序使用時間較少,但輔助空間較多(合並兩個有序序列為一個有序序列)。

3)序列基本有序且n較小時,插入排序具有很高的排序效率。因此常將它和其他的排序方法,如快速排序、歸並排序等結合在一起使用。

4)可以基於決策樹證明基於比較排序算法的時間下限為O(n*log n),也即是說比較排序算法最快就是時間復雜度為O(n*log n)。比較排序算法包括:冒泡排序、插入排序、選擇排序、希爾排序、歸並排序、快速排序。

最好情況和最壞情況

 如果是升序排列,那么排序的最好情況就是待排序序列是升序的,最壞情況待排序序列是降序的。如果目的是降序排列,情況正好相反。

參考

1)數據結構和算法C++版第二版 王紅梅

2)http://wuchong.me/blog/2014/02/09/algorithm-sort-summary/

 

 

 

 

 

 

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM