經過前面的知識,咱們已經知道,有序的數據在查找時有極大的性能提高。不少查找都基於有序數據,但並非全部的結構都能像二叉排序樹同樣,在插入數據時就已經排好序,不少時候每每是無序的數據,須要咱們使用時再進行排序,這就意味着咱們須要尋找高效率的排序算法。接下來,咱們對當下使用較爲廣泛的幾個算法逐一進行分析。這裏推薦一個能夠查看算法運行動態過程的網站,加深對算法原理的理解。git
基礎知識 排序定義 假設含有n個記錄的序列爲{r1. r2, ..., rn},其相應的關鍵字分別爲{k1, k2, ..., kn},需肯定1, 2, ..., n的一種排列p1, p2, ..., pn,使其相應的關鍵字知足kp1≤kp2≤...≤kpn(非遞減或非遞增) 關係,即便得序列成爲一個按關鍵字有序的序列{rp1, rp2, ..., rpn} , 這樣的操做就稱爲排序。github
穩定性 假設ki=kj( 1≤i≤n, 1≤j≤ n, i≠j ) ,且在排序前的序列中 ri 領先於 rj (即i<j) 。若是排序後 ri 仍領先於 rj,則稱所用的排序方法是穩定的;反之,若可能使得排序後的序列中 rj 領先 ri,則稱所用的排序方法是不穩定的。算法
簡單來講,就是對於原數據中相等的數據,排序先後若是相對位置沒有改變,就是穩定的。shell
內排序與外排序 內排序是在排序整個過程當中,待排序的全部記錄所有被放置在內存中。外排序是因爲排序的記錄個數太多,不能同時放置在內存,整個排序過程須要在內外存之間屢次交換數據才能進行。本文先介紹內排序算法,外排序之後再來分析。數組
冒泡排序 冒泡排序(Bubble Sort)是一種交換排序,它的基本思想是:兩兩比較相鄰記錄的關鍵字,若是反序則交換,直到沒有反序的記錄爲止。性能
冒泡排序多是咱們最熟悉的排序算法了,它的核心在於兩兩交換,代碼代碼:學習
private void bubbleSort(int[] arr){ int len = arr.length;大數據
for (int i = 0; i < len-1; i++) { for (int j=0; j < len-1-i; j++) { if(arr[j]>arr[j+1]){ swap(arr, j, j+1); } } }
} 它的最壞時間複雜度是1+2+...+(n-1) = n(n-1)/2,也就是O(n2),這個複雜度相對仍是比較高的,因此只適合小量數據排序。由於冒泡排序每次遍歷後,最後的數據必定是有序的,因此當初始數據部分有序時,還能夠對它進行優化。好比數組爲{1,0,5,6,7,8,9,10},當第一次遍歷後,數組就是有序的,這時後續的循環遍歷都是沒有用的,優化後的算法以下:優化
private void bubbleSort1(int[] arr){ int len = arr.length; boolean flag = false; for (int i = 0; i < len-1; i++) { flag = false; for (int j=0; j < len-1-i; j++) { if(arr[j]>arr[j+1]){ swap(arr, j, j+1); flag = true; } } } } 使用一個flag標記是否有數據交換,冒泡排序若是沒有數據交換,則意味着後邊的數據必定是有序的,這樣一來能夠有效地提升冒泡排序的性能。但整體而言,冒泡排序仍是不適合大數據量、數據比較亂的狀況。網站
簡單選擇排序 選擇排序的思想是每一趟從待排序的記錄中選出最小的元素,順序放在已排好序的序列最後,直到所有記錄排序完畢。簡單選擇排序就基於此思想,除此以外還有樹型選擇排序和堆排序也是基於此思想。
簡單選擇排序法就是經過n-i次關鍵字間的比較,從n-i+1個記錄中選出關鍵字最小的記錄,並和第 i (0≤i≤n)個記錄交換。
它的實現以下:
private void selectSort(int[] arr){ int len = arr.length; int min; for (int i = 0; i < len; i++) { min = i; for (int j = i+1; j < len; j++) { if(arr[min]>arr[j]){ min = j; } } if(i!=min){ swap(arr,i,min); } } } 整體來看,簡單排序算法的最壞時間複雜度也是O(n2),可是它交換數據的次數明顯比冒泡排序要少不少,因此性能也比冒泡排序略優。
直接插入排序 直接插入排序和咱們排序撲克牌的道理一致,就是把新的一張牌插入到已經排好序的牌中,它的基本操做是將一個記錄插入到已經排好序的有序表中,從而獲得一個新的、記錄數增1的有序表。它的代碼實現以下:
private void insertSort(int[] arr){ int len = arr.length; int temp; for (int i = 0; i < len; i++) { for (int j = 0; j < i; j++) { if(arr[i]<arr[j]){ temp = arr[i]; // j及之後的記錄向後移動一位,而後把當前值存放在j的位置 System.arraycopy(arr,j,arr,j+1,i-j); arr[j] = temp; } } } } 它的最壞時間複雜度依然是O(n2)。
咱們介紹了三種最簡單,最容易理解的算法,可是它們的效率也確實較低。在數據量小的時候影響不大,然而現實是咱們更多地要對大量數據進行排序。接下來介紹的幾種算法屬於改進算法,它們的效率都較爲高一些。
希爾排序 簡單的排序算法,都須要在數據量少,或者部分有序的狀況下,才能發揮較好的性能。可是再大規模的數據均可以拆分紅多個小規模的數據,希爾排序的思想就是把大規模的數據拆分紅多個小規模數據,而後每部分分別進行直接插入排序,最後再對所有數據進行總體排序。如何拆分就是希爾排序的重點,好比數據是{0, 9, 2, 4, 6, 1},要將它拆成兩部分,若是按照先後拆分,那麼進行直接插入排序後結果是{0, 2, 9, 1, 4, 6},這樣排序後對後續總體排序沒有幫助。那麼希爾排序是如何作的呢?咱們先經過一個簡單的數組來演示希爾排序的過程,首先有數組以下: 希爾排序 假設第一次取數據的一半做爲間隔值,以後每次減半,咱們把這個值記爲inc,那麼第一次inc=5,咱們在對應位置前加一條紅色虛線表示,以下所示:
接下來咱們就要進行直接插入排序了,前面說過希爾排序不是按照先後區分,而是按照間隔區分的,因此,在進行完這一輪的排序後,咱們要保證如下數據是有序的,以下圖所示,不一樣顏色表示不一樣的子數組,只要保證每一個子數組有序便可:
能夠看到,每一個子數組的元素下標間隔都是inc,這就是inc值的意義。根據這一原則,咱們就能夠進行直接插入排序了,只須要依次將每一個子數組排好序便可。首先比較0和5位置的值,發現已經有序,無需交換,以下:
而後比較位置1和6,發現數值順序錯誤,對它進行交換,以下所示:
接下來再依次比較2和7,3和8,4和9的值,將其排序,最終結果以下所示:
如今,拆分的子數組都已是有序了。接下來,咱們須要合併,咱們把inc的值折半,再進行上述操做,那麼inc的位置和拆分的子數組以下所示:
能夠看到,每一個子數組的元素下標間隔都和inc值同樣,這時子數組只有兩個了。咱們來對這兩個子數據依次進行直接插入排序,首先對包含位置0的數組進行排序,直接插入排序就是把當前值插入到已有的有序子數組中,因此0位置依然是0,而後把位置2的元素插入,由於1>0,因此它的位置不變,以下所示:
位置4和6的元素又是最大值,因此也不須要交換,結果以下所示:
接下來位置8,插入後須要和6交換,以下所示:
這樣,第一個數組就調整完畢了,接下來調整第二個數組,位置1和3須要交換,以下所示:
接下來調整位置5,由於值3是最小值,應該放在位置1,因此須要把1和3位置的值向後移動,而後再插入,結果以下:
最後位置7和9也按照一樣的方式進行,最終結果以下:
如今,咱們再進行合併時就是一個完整的數組了,能夠看到,這個數組已是基本有序的了,較小的值基本位於左側,較大的值基本位於右側,這比直接進行直接插入排序要好的多。希爾排序的代碼以下:
p
rivate void shellSort(int[] arr){ int len = arr.length; int inc = len; // 設置間隔值 for (inc=len/2; inc>0; inc/=2) { // i 從inc走到len,j正好能夠把全部子數組遍歷一次 // j會先比較每一個子數組的第一個值,再第二個值,這樣橫向進行遍歷 for (int i = inc; i < len; i++) { for (int j = i; j>=inc && arr[j]<arr[j-inc]; j-=inc) { swap(arr,j,j-inc); } } } }
希爾排序整體而言效率比直接插入排序要好,由於它最開始當inc值較大時,數據的移動距離很長,而當inc值小時,由於數據已經大體有序,可使直接插入排序更有效率,這兩點是希爾排序高效必備的條件。inc值的選取對希爾排序十分關鍵,像以上這種折半方式,在某些狀況下仍是較慢,可是咱們沒有辦法找到完美的計算方案使希爾排序最高效,以inc=inc*3+1構建的間隔也是經常使用的一種,示例代碼以下:
private void shellSort1(int[] arr) { //首先根據數組的長度肯定增量的最大值 int inc=1; // inc * 3 + 1獲得增量序列的最大值 while(inc <= arr.length / 3) inc = inc * 3 + 1; //進行增量查找和排序 while(inc>=1){ for(int i=inc;i<arr.length;i++){ for(int j=i;j >= inc && arr[j] < arr[j-inc];j -= inc){ swap(arr,j,j-inc); } } inc = inc/3; } }
目前,最高效的希爾排序的時間複雜度能夠達到O(n3/2),相關知識你們能夠查閱書籍瞭解,這裏咱們就再也不追究了。
堆排序 堆排序是對簡單選擇排序的優化,在簡單選擇排序中,每排序一個數據,都要在剩餘所有數據中尋找最小值,可是在這個尋找的過程當中,沒有對剩餘的數據記錄,因此以後的尋找會進行屢次重複操做。堆排序則是會把這些數據記錄在堆中,以後的尋找只須要在堆中進行。
堆是具備下列性質的徹底二叉樹:每一個結點的值都大於或等於其左右孩子結點的值,稱爲大頂堆;或者每一個結點的值都小於或等於其左右孩子結點的值,稱爲小頂堆。
根據以上定義,能夠肯定,根結點必定是最大(或最小)值。大頂堆和小頂堆示意以下:
若是按照層序遍歷的順序給堆的每一個結點編號:0, 1, ..., (n-1),那麼它必定符合如下條件:
a[i]≤a[2i+1] 且 a[i]≤a[2i+2],其中0 ≤ i ≤ (n-1)/2,或 a[i]≥a[2i+1] 且 a[i]≥a[2i+2],其中0 ≤ i ≤ (n-1)/2。
掌握了堆的概念以後,就能夠進行堆排序了,以從小到大排序爲例,它的過程是先將待排序的數組構建成一個大頂堆,此時,根結點就是最大值,將它放置在數組的結尾,而後將剩餘數據從新構建成一個堆,如此循環進行,直到所有有序。
那,咱們如何構建一個大頂堆,又如何進行調整呢?接下來,咱們用一個數組示例,來演示堆排序的過程,假如數組以下:{50, 20, 90, 30, 80, 40, 70, 60, 10},咱們第一步要作的就是把它看作是一個徹底二叉樹層序遍歷的結果集,因此它對應的徹底二叉樹以下:
咱們要作的,就是把這棵徹底二叉樹調整爲一個大頂堆結構,按照樹的通常處理思路,咱們只須要把每一個子樹都調整爲大頂堆,就能夠把整棵樹調整爲大頂堆,因此,咱們只須要自下而上,依次把分別以三、二、一、0爲根結點的子樹調整爲大頂堆便可,結點3就是最後一個子樹,以後的結點都是葉子結點。
下面先看調整的代碼,以下所示:
/** * 堆的調整 * root:子樹的根結點位置 * len:當前排序數組的長度 */ private void heapAdjust(int[] arr, int root, int len){ if(len<=0)return; int temp; // 根結點的值先保存 temp = arr[root]; // i是這個結點的左孩子,或者是它孩子的左孩子 for (int i=2*root+1; i<len; i=2*i+1) { if(i<len-1 && arr[i]<arr[i+1]){ // 尋找到兩個孩子的較大者 i++; } // 根結點的值比兩個孩子都大,就不須要再調整了 if(temp>=arr[i]){ break; } // 把根結點的值記爲這個較大的孩子的值 arr[root] = arr[i]; // 再向下一級子樹遍歷 root=i; } // 最後把temp的值存放在空置的位置 arr[root] = temp; }
按照以上思路,這段代碼看起來就比較簡單了,那就是尋找到這棵樹的最大值,而且每次都選擇它的兩個孩子中較大的那個進行交換,最終最大值處於根結點。有了調整的代碼,咱們就能夠把原數組構建成一個大頂堆了,只須要對結點三、二、一、0依次調用調整方法便可。以下所示:
for (int i = (len-2)/2; i>=0; i--) { heapAdjust(arr,i,len); }
這裏要說明一下 i 的起點的設置,按照咱們的定義,一個長度爲 n 的數組,其下標範圍是 0 到(n-1),若是 n 是奇數,那麼最後一個有孩子的結點必定有兩個孩子,如上面這棵樹的結點3就有兩個孩子,若是 n 是偶數,那麼最後一個有孩子的結點只有一個左孩子。對於有兩個孩子的,咱們用n-1-1,就獲得了它左孩子的下標,對於只有一個孩子的,由於 n 是偶數,因此n-1是奇數,n-1-1仍是偶數,能夠知道(n-1)/2和(n-2)/2是相等的。綜上所述,咱們使用(n-2)/2,就能夠獲得最後一個有孩子結點的下標。
如今,就能夠實現完整的堆排序算法了,只須要每次都把最大值移動到數組最後,而後剩餘部分再進行一次調整便可,代碼以下所示:
private static void heapSort(int[] arr){ int len = arr.length; // 從最後一個有孩子的結點開始,逐一進行堆的調整 for (int i = (len-2)/2; i>=0; i--) { heapAdjust(arr,i,len); } // 對於一個堆,最大值必定在根結點,也就是在數組位置0,把它換到數組最後,而後對剩餘的數據再進行一次堆的調整 for (int i = len-1; i>0; i--) { // 把最大值放在數組的最後 swap(arr,0,i); // 剩餘的值進行堆的調整 heapAdjust(arr,0,i); }
} 堆排序的最壞時間複雜度爲O(nlogn),其中 n 是外層循環,logn是調整內部的for循環,這個for循環和遞歸相似。由於它對原始數據並不敏感,因此最好、平均和最壞時間複雜度都是O(nlogn),和O(n2)相比效率高了不少。堆排序由於操做是在原地進行,因此空間複雜度爲O(1)。
歸併排序 歸併排序也利用了徹底二叉樹,從而把時間複雜度下降到O(nlogn),它的思想是一種分而治之的思想,咱們這裏以2路歸併排序爲例,來講明它的核心原理。
假設初始序列含有 n 個記錄,則能夠當作是 n 個有序的子序列,每一個子序列的長度爲1,而後兩兩歸併,獲得n/2個長度爲 2 或 1 的有序子序列;再兩兩歸併,...,如此重複,直至獲得一個長度爲 n 的有序序列爲止,這種排序方法稱爲2路歸併排序。
歸併排序的原理並不複雜,經過一張圖就能夠徹底理解它的意圖,以下所示,它的過程就是先分後治的分而治之思想的體現:
分,就是把數組拆分紅一條一條數據,2路歸併就是採用二分法,直到每部分只含一條數據爲止。治,就是把數據排序後再合併,從而使得每部分有序,再合併,直到所有有序爲止。分的過程可使用遞歸,這很好實現,代碼以下所示:
private void mergeSort(int[] arr, int left, int right){ if(left<right){ int mid = (left+right)/2; mergeSort(arr,left,mid); mergeSort(arr,mid+1,right); // 歸併操做 ... } }
接下來就是治的過程,這個過程就是把兩個有序數組合併成一個有序數組,以把{2, 8}和{3, 7}合併成{2, 3, 7, 8}爲例,首先比較2和3,選擇2,以下所示:
接下來應該比較3和8,選擇3,以下所示:
接下來比較7和8,選擇7以後,只剩下8了,能夠確定8及以後(若是有)的全部數據都是比較大且有序的,無需再次比較。根據這個思路,參考代碼以下所示:
private void merge(int[] arr, int[] temp, int left, int mid, int right){ int i = left; int j = mid+1; int k = 0; while(i<=mid && j<=right){ if(arr[i]<arr[j]){ temp[k++] = arr[i++]; }else{ temp[k++] = arr[j++]; } } while(i<=mid){ temp[k++] = arr[i++]; } while(j<=right){ temp[k++] = arr[j++]; } k=0; while (left<=right) { arr[left++] = temp[k++]; }
} 其中temp是事先建立好的數組,由於數組的特殊性,比較操做沒法在原數組進行,因此須要在temp數組進行比較後,再將有序結果複製到原數組。最終,歸併排序代碼以下:
private void mergeSort(int[] arr){ int[] temp = new int[arr.length]; mergeSort(arr,temp,0,arr.length-1); } private void mergeSort(int[] arr, int[] temp, int left, int right){ if(left<right){ int mid = (left+right)/2; mergeSort(arr,temp,left,mid); mergeSort(arr,temp,mid+1,right); merge(arr,temp,left,mid,right); } }
歸併排序的時間複雜度是O(nlogn),而以上使用遞歸的作法,它的空間複雜度是O(n+logn),其中 n 是temp數組,logn是遞歸佔用的棧空間。能夠看到,遞歸佔用了不菲的空間,那麼咱們能不能用非遞歸的方式實現歸併排序呢?答案是確定的,許多遞歸均可以轉爲線性操做。歸併排序是從單個數據開始的,而數組自己就能夠看作是一個一個數據,非遞歸實現的思路以下:
其中不一樣顏色表明不一樣的子數組,第一次從原數組進行一次歸併後,temp數組中存放的其實就是第二次歸併的原始數據,這時只要再從temp數組歸併到原數組,就獲得了第三次歸併的原始數據,重複下去,直到歸併完畢。能夠看到,只須要一個數組的空間就能夠完成所有過程,因此空間複雜度下降到了O(n)。由於篇幅的緣由,代碼在文末github連接中,你們能夠參考。
快速排序 快速排序:經過一趟排序將待排記錄分割成獨立的兩部分,其中一部分記錄的關鍵字均比另外一部分記錄的關鍵字小,則可分別對這兩部分記錄繼續進行排序,以達到整個序列有序的目的。
從這段定義能夠發現,這又是遞歸能夠發揮能力的算法,快速排序的關鍵在於用來分割的關鍵字的選擇。咱們先從選擇每一個子數組最左側數據爲例來實現快速排序,代碼以下:
private void quickSort(int[] arr){ qSort(arr,0,arr.length-1); } private void qSort(int[] arr, int low, int high) { int pivot; if(low<high){ pivot = partition(arr,low,high); qSort(arr,low,pivot-1); qSort(arr,pivot+1,high); } } private int partition(int[] arr, int low, int high) { int pivotKey = arr[low]; while (low<high) { while (low<high&&arr[high]>=pivotKey) { high--; } swap(arr,low,high); while (low<high&&arr[low]<=pivotKey) { low++; } swap(arr,low,high); } return low; }
關鍵的代碼就在partition這個方法中,先選擇一個關鍵字,而後用它左右兩側數據與之對比並調整位置,最後返回這個關鍵字的地址,再以此分爲左右兩部分重複此操做。下面,咱們用一個簡單的數組來模擬以上操做,以下所示,紅色標註的數據就是選擇的關鍵字:
先比較high的值與關鍵字,若是不須要調整,就向前移動,以下所示:
接下來5和6都比關鍵字大,直到high的值爲1時,交換low與high的值,注意咱們的關鍵字仍是2,以下所示:
接下來比較low的值與關鍵字,1比2小,因此low指針後移,以下所示:
接下來8比2大,因此交換low和high的值,以下所示:
交換low和high 接下來直到high指向 7 都再也不進行交換,第一輪排序就結束了,能夠看到,low的值依然是以前的關鍵字。這也是爲什麼先比較high指針再比較low指針的緣由,也是爲什麼最終返回low的緣由。接下來只要按照這個規則,就能夠把數組排序好。
快速排序最好的時間複雜度爲O(nlogn),也就是每次關鍵字取值都能剛好把數組平分兩部分時的狀況,最壞時間複雜度是O(n2),也就是十分不幸地,每次拆分都分紅了一邊空一邊是剩餘所有的兩部分。而空間複雜度也跟隨着變化,從O(logn)到O(n)。
能夠看到,快速排序嚴重受關鍵字選擇的影響,像以上示例關鍵字2僅把數組分紅了一邊長度爲一、一邊長度爲6的兩部分,顯然不夠高效。因而就有了三數取中法,作法是取三個關鍵字先進行排序,而後用中間的值做爲選擇的關鍵字,這樣的好處是這個關鍵字至少不是最大值或最小值,並且頗有可能取到比較接近中間的值,這在大多數狀況下都能提升必定的效率。三數取中法只須要在partition中增長如下代碼便可:
private static int partition(int[] arr, int low, int high) { // 三數取中法,把中間值存放在low中 int mid = low + (high-low)/2; if (arr[low]>arr[high]) { swap(arr, low, high); } if (arr[mid]>arr[high]) { swap(arr,mid,high); } if (arr[low]>arr[mid]) { swap(arr,low,mid); } int pivotKey = arr[low]; ... }
固然,三數取中法並不完美,它有可能很高效也可能很低效,這點就須要根據實際狀況來合理選擇了,甚至有人提出採用九數取中法來進一步提升效率,感興趣的話能夠查閱相關資料進一步研究。接下來咱們對快速排序的其餘部分進行優化,在排序過程當中,選取的關鍵字從最初到最終的位置通過了屢次移動,這是沒有必要的,可讓它直接到達終點,修改代碼以下所示:
private int partition(int[] arr, int low, int high) { int pivotKey = arr[low]; // 暫存關鍵字 int temp = pivotKey; while (low<high) { while (low<high&&arr[high]>=pivotKey) { high--; } arr[low] = arr[high]; //swap(arr,low,high); while (low<high&&arr[low]<=pivotKey) { low++; } arr[high] = arr[low]; // swap(arr,low,high); } // 恢復關鍵字 arr[low] = temp; return low;
} 以上優化用複製數據代替了交換數據,從而使性能有必定的提高,能夠這樣作的緣由是由於每次進行交換的值都包含關鍵字。除此以外,它的遞歸部分也能夠進行優化,優化後的代碼以下所示:
private void qSort(int[] arr, int low, int high) { int pivot; // 遞歸 // if(low<high){ // pivot = partition(arr,low,high); // qSort(arr,low,pivot-1); // qSort(arr,pivot+1,high); // } // 迭代代替遞歸 while(low<high){ pivot = partition(arr,low,high); qSort(arr,low,pivot-1); low = pivot+1; } }
這個優化就是用循環代替了遞歸,只是寫法上有些不一樣,是否真的有優化效果還有待考證。關於遞歸和循環,也不必定是全部遞歸都應該使用循環代替,這裏有一篇文章我以爲分析的不錯,你們能夠參考一下,連接以下:快速排序的優化和關於遞歸的問題,說說個人想法。
分配排序 最後,咱們還要講一個應用場景較少的排序算法,它的時間複雜度能夠達到線性階,也就是O(n)。根據不一樣的分配方式,又主要有計數排序、桶排序和基數排序三個算法。
計數排序 計數排序的原理很簡單,顧名思義就是對每一個數據計數,而後分配到下標爲0-max的數組中,而後對計數進行排列便可。以下所示,桶中存儲的是每一個數據出現的次數: 計數 有了計數,就能夠獲得排好序的數組了,0有0個,1有1個,因此第一個有序值是1,2有一個,因此第二個值是2,依次類推,最後有序數組爲{1, 2, 3, 3, 5, 7, 7, 8}。實現代碼以下:
private void countingSort(int[] arr){ int len = arr.length; // 獲取最大值 int max = arr[0]; for (int i = 1; i < len; i++) { if(max<arr[i]){ max = arr[i]; } } // 建立max+1個桶,從0-max int[] bucket = new int[max+1]; for (int i = 0; i < len; i++) { // 每獲取一個數,就把它放在編號和其一致的桶中 bucket[arr[i]]++; } int j = 0; for (int i = 0, bLen = bucket.length; i < bLen; i++) { // 遍歷桶,按順序恢復每條數據 for (int k = bucket[i]; k > 0; k--) { arr[j++] = i; } } }
由於通常重複數據比較少,因此每一個桶內的值不會很大,它的最好時間複雜度是O(n)。可是它有很嚴格的使用條件,那就是值是離散的,有窮的,並且數據要緊密,好比有數組{0, 2, 5, ..., 10000},其中10000與其餘數據差距很大,那麼就會形成嚴重的空間浪費,也給遍歷增長了難度。可是若是數據能知足這些要求,它的排序速度很是快。
桶排序 桶排序和計數排序相似,只是再也不精確地一個下標對應一個數組,而是取多個區間,好比[0, 10), [10, 20), ...,而後每一個部分再使用如直接插入排序等方法進行排序。這一點和哈希表相似,須要數組和鏈表結合使用,以下所示: 桶排序 數組的每一位存儲的都是鏈表,對這個鏈表進行排序比對所有數據排序要好的多,這裏就再也不給出代碼實現了。
基數排序 基數排序,就是從每一個數的低位開始排序,先排序個位數,再排序十位數、百位數,直至整個數組有序。它的原理以下所示,首先按照個位排序:
根據個位排序的結果,再進行十位數排序,以下所示:
最後再按照百位數排序,以下所示:
4. 總結 分配排序針對整數這種結構,在數據較爲均勻,緊密性較好的前提下進行了優化,可使得排序時間複雜度接近O(n)。不過由於它的使用場景較少,且佔用空間比較多,所以不常被使用。
總結 除了分配排序這種十分苛刻的排序算法,其餘排序的時間複雜度都在O(nlogn)到O(n2)之間。快速排序是當前使用最多的一種排序算法,可是咱們也不能盲目的選擇它,而是要針對實際狀況選擇不一樣的算法。一般,當數據量十分小(通常是7-10個)時,會使用直接插入排序來代替其它排序,由於當數據不多時,算法的時間複雜度並不能做爲評判算法效率的惟一標準,時間複雜度自己比較粗略,在 n 很小時有可能O(n2)比O(n)還要快,好比n=5,O(n2)算法實際運行次數是n2=25次,而O(n)算法實際運行次數是10n=50次,這時候常數項也會對算法有所影響。
最後,咱們對多種排序的綜合性能進行對比,以下表所示:
最後,再對這裏的穩定性簡單說明一下,對於兩兩比較的算法必定是穩定的,而存在跳躍比較的算法則是不穩定的,由於兩兩比較的是相鄰值,那麼相等的數據不會發生交換,而跳躍比較就沒法保證了,因此若是對穩定性要求很高,可能歸併排序就是最好的選擇。
以上就是常見排序算法的所有解析了,經歷了這麼多年,還誕生了更多更有趣的排序算法,之後有機會再來一睹爲快吧。
QQ討論羣組:984370849 706564342 歡迎加入討論
想要深刻學習的同窗們能夠加入QQ羣討論,有全套資源分享,經驗探討,沒錯,咱們等着你,分享互相的故事!