排序算法種類繁多。根據處理的數據規模與存儲特色,可分爲內部排序和外部排序:前者處理的數據規模不大,內存足以容納;後者處理的數據規模較大,必須將數據存放於外部存儲器中,每次排序的時候須要訪問外存。根據輸入的不一樣形式,分爲脫機算法和在線算法:前者待排序的數據是以批處理的形式給出的;而在雲計算之類的環境中,待排序的數據是實時生成的,在排序算法開始運行時,數據並未徹底就緒,而是隨着排序算法自己的進行而逐步給出的。另外,針對不一樣的體系結構,又分爲串行和並行兩大類排序算法。根據算法是否採用隨機策略,還有肯定式和隨機式之分。linux
冒泡排序是比較相鄰兩數的大小來完成排序的。這裏定義比較邊界,也就是進行大小比較的邊界。對於長度爲n的數組,第一趟的比較邊界爲[0,n-1],也就是說從a[0]開始,相鄰元素兩兩比較大小,若是知足條件就進行交換,不然繼續比較,一直到最後一個比較的元素爲a[n-1]爲止,此時第一趟排序完成。以升序排序爲例,每趟排序完成以後,比較邊界中的最大值就沉入底部,比較邊界就向前移動一個位置。因此,第二趟排序開始時,比較邊界是[0,n-2]。對於長度爲n的序列,最多須要n趟完成排序,因此冒泡排序就由兩層循環構成,最外層循環用於控制排序的趟數,最內層循環用於比較相鄰數字的大小並在本趟排序完成時更新比較邊界。web
具體代碼以下:算法
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 }
在排序後期可能數組已經有序了而算法卻還在一趟趟的比較數組元素大小,能夠引入一個標記,若是在一趟排序中,數組元素沒有發生過交換說明數組已經有序,跳出循環便可。優化後的代碼以下:shell
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 }
還能夠利用這種標記的方法還能夠檢測數組是否有序,遍歷一個數組比較其大小,對於知足要求的元素進行交換,若是不會發生交換則數組就是有序的,不然是無序的。數組
兩種方法的排序結果以下所示:數據結構
將待排序的數組劃分爲局部有序子數組subSorted和無序子數組subUnSorted,每次排序時從subUnSorted中挑出第一個元素,從後向前將其與subSorted各元素比較大小,按照大小插入合適的位置,插入完成後將此元素從subUnSorted中移除,重複這個過程直至subUnSorted中沒有元素,總之就時從後向前,一邊比較一邊移動。app
對應代碼以下: 數據結構和算法
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 }
添加一個易於理解的版本:ide
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 }
新版本: 優化
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 }
哨兵版本:
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 }
由希爾在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 }
以上代碼不夠簡潔,還能夠進一步改進。事先沒必要分組,能夠從第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 }
新版本:
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 }
希爾排序中等大小規模表現良好,對規模很是大的數據排序不是最優選擇。可是比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 }
這裏補充不使用臨時變量對兩個數值進行交換的方法。實現代碼以下:
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 }
解釋下使用異或進行交換的原理。異或位運算,當兩位相同時,結果爲1,不然爲0。使用異或進行交換的原理以下圖所示:
堆排序是基於選擇排序的改進,目的是較少比較次數。一趟選擇排序中,僅保留了最小值,而堆排序排序不只保留最小值,還把較小值保留下來,減小了比較小次數。
堆是這樣一種徹底二叉樹:根節點的值大於等於左右孩子節點的值(最大堆)或者根節點的值小於等於左右孩子節點的值(最小堆)。堆也是遞歸定義的,即堆的孩子節點自己也是堆。使用數組存儲堆。
堆具備如下性質:
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 }
完成了初始堆的建立以後,就能夠經過不斷的重建堆進行堆排序,每次重建堆就是一趟排序,每次重建時都將堆頂節點與堆無序區的最後一個元素交換,所以每趟堆排序後堆的有序區就增長了一個元素(從數組最後與各元素開始,向前排列),下輪就使用無序區組成的堆進行重建,每次重建都只是對堆頂節點的調整,由於初始堆建成以後,其餘節點都知足堆的定義。實現代碼以下:
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 }
建立初始堆的時間複雜度是O(n),簡單的解釋是有n/2個節點須要調整,每次調整節點時只是上寫移動常數個節點,所以建立初始堆的時間複雜度是O(n)。而實際進行堆排序時,須要進行n趟,每趟進行堆重建時就是調整堆頂節點,最多移動次數不會超過書的高度O(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 }
非遞歸形式的歸併排序的實現中,關鍵是假設每一個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 }
將待排序序列分爲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 }
歸併排序是一種穩定的排序。
快速排序是圖靈獎得主 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 }
每次分區以後,基準值所處的位置(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 }
快速排序最好狀況是每次分區後,都將序列等分爲兩個長度基本相等的子序列(也就是分區後基準元素都位於序列中間位置)。第一次分區後,子序列長度爲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++版第二版 王紅梅