查找和排序算法是算法的入門知識,其經典思想能夠用於不少算法當中。由於其實現代碼較短,應用較常見。因此在面試中常常會問到排序算法及其相關的問題。但萬變不離其宗,只要熟悉了思想,靈活運用也不是難事。通常在面試中最常考的是快速排序和歸併排序,而且常常有面試官要求現場寫出這兩種排序的代碼。對這兩種排序的代碼必定要信手拈來才行。還有插入排序、冒泡排序、堆排序、基數排序、桶排序等。面試官對於這些排序可能會要求比較各自的優劣、各類算法的思想及其使用場景。還有要會分析算法的時間和空間複雜度。一般查找和排序算法的考察是面試的開始,若是這些問題回答很差,估計面試官都沒有繼續面試下去的興趣了。因此想開個好頭就要把常見的排序算法思想及其特色熟練掌握,在必要時能熟練寫出代碼。ios
接下來咱們就分析一下常見的排序算法及其使用場景。限於篇幅,某些算法的詳細演示和圖示請自行尋找其它參考資料。面試
冒泡排序是最簡單的排序之一了,其大致思想就是經過與相鄰元素的比較和交換來把小的數交換到最前面。這個過程相似於水泡向上升同樣,所以而得名。舉個例子,對5,3,8,6,4這個無序序列進行冒泡排序。首先從後向前冒泡,4和6比較,把4交換到前面,序列變成5,3,8,4,6。同理4和8交換,變成5,3,4,8,6,3和4無需交換。5和3交換,變成3,5,4,8,6,3.這樣一次冒泡就完了,把最小的數3排到最前面了。對剩下的序列依次冒泡就會獲得一個有序序列。冒泡排序的時間複雜度爲O(n^2),空間複雜度爲O(1)。算法
冒泡排序:shell
實現代碼:數組
/** *@Description:冒泡排序算法實現 */ public class BubbleSort { public static void bubbleSort(int[] arr) { if(arr == null || arr.length == 0) return ; for(int i=0; i<arr.length-1; i++) { for(int j=arr.length-1; j>i; j--) { if(arr[j] < arr[j-1]) { swap(arr, j-1, j); } } } } public static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } }
選擇排序的思想其實和冒泡排序有點相似,都是在一次排序後把最小的元素放到最前面。可是過程不一樣,冒泡排序是經過相鄰的比較和交換。而選擇排序是經過對總體的選擇。舉個例子,對5,3,8,6,4這個無序序列進行簡單選擇排序,首先要選擇5之外的最小數來和5交換,也就是選擇3和5交換,一次排序後就變成了3,5,8,6,4.對剩下的序列一次進行選擇和交換,獲得3,4,5,8,6, 以此類推,最終就會獲得一個有序序列。其實選擇排序能夠當作冒泡排序的優化,由於其目的相同,只是選擇排序只有在肯定了最小數的前提下才進行交換,大大減小了交換的次數。選擇排序的時間複雜度爲O(n^2),空間複雜度爲O(1)。函數
示意圖:性能
實現代碼:大數據
/** *@Description:簡單選擇排序算法的實現 */ public class SelectSort { public static void selectSort(int[] arr) { if(arr == null || arr.length == 0) return ; int minIndex = 0; for(int i=0; i<arr.length-1; i++) { //只須要比較n-1次 minIndex = i; for(int j=i+1; j<arr.length; j++) { //從i+1開始比較,由於minIndex默認爲i了,i就不必比了。 if(arr[j] < arr[minIndex]) { minIndex = j; } } if(minIndex != i) { //若是minIndex不爲i,說明找到了更小的值,交換之。 swap(arr, i, minIndex); } } } public static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } }
插入排序不是經過交換位置而是經過比較找到合適的位置插入元素來達到排序的目的的。相信你們都有過打撲克牌的經歷,特別是牌數較大的。在分牌時可能要整理本身的牌,牌多的時候怎麼整理呢?就是拿到一張牌,找到一個合適的位置插入。這個原理其實和插入排序是同樣的。舉個例子,對5,3,8,6,4這個無序序列進行簡單插入排序,首先假設第一個數的位置是正確的,想一下在拿到第一張牌的時候,不必整理。而後3要插到5前面,把5後移一位,變成3,5,8,6,4.想一下整理牌的時候應該也是這樣吧。而後8不用動,6插在8前面,8後移一位,4插在5前面,從5開始都向後移一位。注意在插入一個數的時候要保證這個數前面的數已經有序。簡單插入排序的時間複雜度也是O(n^2),空間複雜度爲O(1)。優化
示意圖:ui
實現代碼:
/** *@Description:簡單插入排序算法實現 */ public class InsertSort { public static void insertSort(int[] arr) { if(arr == null || arr.length == 0) return ; for(int i=1; i<arr.length; i++) { //假設第一個數位置時正確的;要日後移,必需要假設第一個。 int j = i; int target = arr[i]; //待插入的 //後移 while(j > 0 && target < arr[j-1]) { arr[j] = arr[j-1]; j --; } //插入 arr[j] = target; } } }
快速排序一聽名字就以爲很高端,在實際應用當中快速排序確實也是表現最好的排序算法。快速排序雖然高端,但其思想是來自冒泡排序,冒泡排序是經過相鄰元素的比較和交換把最小的冒泡到最頂端,而快速排序是比較和交換小數和大數,在把小數冒泡到上面的同時也把大數沉到下面。
爲何必定要j指針先動呢?首先這也不是絕對的,這取決於基準數的位置,由於在最後兩個指針相遇的時候,要交換基準數到相遇的位置。通常選取第一個數做爲基準數,那麼就是在左邊,因此最後相遇的數要和基準數交換,那麼相遇的數必定要比基準數小。因此j指針先移動才能先找到比基準數小的數。
快速排序是不穩定的,其平均時間複雜度是O(nlgn),空間複雜度是O(nlgn)。
示意圖:
實現代碼:
/** *@Description:實現快速排序算法 */ public class QuickSort { //一次劃分 public static int partition(int[] arr, int left, int right) { int pivotKey = arr[left]; int pivotPointer = left; while(left < right) { while(left < right && arr[right] >= pivotKey) right --; while(left < right && arr[left] <= pivotKey) left ++; swap(arr, left, right); //把大的交換到右邊,把小的交換到左邊。 } swap(arr, pivotPointer, left); //最後把pivot交換到中間 return left; } public static void quickSort(int[] arr, int left, int right) { if(left >= right) return ; int pivotPos = partition(arr, left, right); quickSort(arr, left, pivotPos-1); quickSort(arr, pivotPos+1, right); } public static void sort(int[] arr) { if(arr == null || arr.length == 0) return ; quickSort(arr, 0, arr.length-1); } public static void swap(int[] arr, int left, int right) { int temp = arr[left]; arr[left] = arr[right]; arr[right] = temp; } }
其實上面的代碼還能夠再優化,上面代碼中基準數已經在pivotKey中保存了,因此不須要每次交換都設置一個temp變量,在交換左右指針的時候只須要前後覆蓋就能夠了。這樣既能減小空間的使用還能下降賦值運算的次數。優化代碼以下:
/** *@Description:實現快速排序算法 */ public class QuickSort { /** * 劃分 * @param arr * @param left * @param right * @return */ public static int partition(int[] arr, int left, int right) { int pivotKey = arr[left]; while(left < right) { while(left < right && arr[right] >= pivotKey) right --; arr[left] = arr[right]; //把小的移動到左邊 while(left < right && arr[left] <= pivotKey) left ++; arr[right] = arr[left]; //把大的移動到右邊 } arr[left] = pivotKey; //最後把pivot賦值到中間 return left; } /** * 遞歸劃分子序列 * @param arr * @param left * @param right */ public static void quickSort(int[] arr, int left, int right) { if(left >= right) return ; int pivotPos = partition(arr, left, right); quickSort(arr, left, pivotPos-1); quickSort(arr, pivotPos+1, right); } public static void sort(int[] arr) { if(arr == null || arr.length == 0) return ; quickSort(arr, 0, arr.length-1); } }
總結快速排序的思想:冒泡+二分+遞歸分治,慢慢體會。。。
堆排序是藉助堆來實現的選擇排序,思想同簡單的選擇排序。
1.堆
堆其實是一棵徹底二叉樹,其任何一非葉節點知足性質:
Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]或者Key[i]>=Key[2i+1]&&key>=key[2i+2]
即任何一非葉節點的關鍵字不大於或者不小於其左右孩子節點的關鍵字。
堆分爲大頂堆和小頂堆,知足Key[i]>=Key[2i+1]&&key>=key[2i+2]稱爲大頂堆,知足 Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]稱爲小頂堆。由上述性質可知大頂堆的堆頂的關鍵字確定是全部關鍵字中最大的,小頂堆的堆頂的關鍵字是全部關鍵字中最小的。
2.堆排序的思想
利用大頂堆(小頂堆)堆頂記錄的是最大關鍵字(最小關鍵字)這一特性,使得每次從無序中選擇最大記錄(最小記錄)變得簡單。
其基本思想爲(大頂堆):
1)將初始待排序關鍵字序列(R1,R2....Rn)構建成大頂堆,此堆爲初始的無序區;
2)將堆頂元素R[1]與最後一個元素R[n]交換,此時獲得新的無序區(R1,R2,......Rn-1)和新的有序區(Rn),且知足R[1,2...n-1]<=R[n];
3)因爲交換後新的堆頂R[1]可能違反堆的性質,所以須要對當前無序區(R1,R2,......Rn-1)調整爲新堆,而後再次將R[1]與無序區最後一個元素交換,獲得新的無序區(R1,R2....Rn-2)和新的有序區(Rn-1,Rn)。不斷重複此過程直到有序區的元素個數爲n-1,則整個排序過程完成。
操做過程以下:
1)初始化堆:將R[1..n]構造爲堆;
2)將當前無序區的堆頂元素R[1]同該區間的最後一個記錄交換,而後將新的無序區調整爲新的堆。
所以對於堆排序,最重要的兩個操做就是構造初始堆和調整堆,其實構造初始堆事實上也是調整堆的過程,只不過構造初始堆是對全部的非葉節點都進行調整。
下面舉例說明:
給定一個整形數組a[]={16,7,3,20,17,8},對其進行堆排序。
首先根據該數組元素構建一個徹底二叉樹,獲得
而後須要構造初始堆,則從最後一個非葉節點開始調整,調整過程以下:
20和16交換後致使16不知足堆的性質,所以需從新調整
這樣就獲得了初始堆。
即每次調整都是從父節點、左孩子節點、右孩子節點三者中選擇最大者跟父節點進行交換(交換以後可能形成被交換的孩子節點不知足堆的性質,所以每次交換以後要從新對被交換的孩子節點進行調整)。有了初始堆以後就能夠進行排序了。
此時3位於堆頂不滿堆的性質,則需調整繼續調整
這樣整個區間便已經有序了。
從上述過程可知,堆排序其實也是一種選擇排序,是一種樹形選擇排序。只不過直接選擇排序中,爲了從R[1...n]中選擇最大記錄,需比較n-1次,而後從R[1...n-2]中選擇最大記錄需比較n-2次。事實上這n-2次比較中有不少已經在前面的n-1次比較中已經作過,而樹形選擇排序剛好利用樹形的特色保存了部分前面的比較結果,所以能夠減小比較次數。對於n個關鍵字序列,最壞狀況下每一個節點需比較log2(n)次,所以其最壞狀況下時間複雜度爲nlogn。堆排序爲不穩定排序,不適合記錄較少的排序。堆排序空間複雜度爲O(1)。
示意圖:
實現代碼:
/*堆排序(大頂堆)*/
#include <iostream>
#include<algorithm>
using namespace std;
void HeapAdjust(int *a,int i,int size) //調整堆
{
int lchild=2*i; //i的左孩子節點序號
int rchild=2*i+1; //i的右孩子節點序號
int max=i; //臨時變量
if(i<=size/2) //若是i是葉節點就不用進行調整
{
if(lchild<=size&&a[lchild]>a[max])
{
max=lchild;
}
if(rchild<=size&&a[rchild]>a[max])
{
max=rchild;
}
if(max!=i)
{
swap(a[i],a[max]);
HeapAdjust(a,max,size); //避免調整以後以max爲父節點的子樹不是堆
}
}
}
void BuildHeap(int *a,int size) //創建堆
{
int i;
for(i=size/2;i>=1;i--) //非葉節點最大序號值爲size/2
{
HeapAdjust(a,i,size);
}
}
void HeapSort(int *a,int size) //堆排序
{
int i;
BuildHeap(a,size);
for(i=size;i>=1;i--)
{
//cout<<a[1]<<" ";
swap(a[1],a[i]); //交換堆頂和最後一個元素,即每次將剩餘元素中的最大者放到最後面
//BuildHeap(a,i-1); //將餘下元素從新創建爲大頂堆
HeapAdjust(a,1,i-1); //從新調整堆頂節點成爲大頂堆
}
}
int main(int argc, char *argv[])
{
//int a[]={0,16,20,3,11,17,8};
int a[100];
int size;
while(scanf("%d",&size)==1&&size>0)
{
int i;
for(i=1;i<=size;i++)
cin>>a[i];
HeapSort(a,size);
for(i=1;i<=size;i++)
cout<<a[i]<<"";
cout<<endl;
}
return 0;
}
希爾(Shell)排序又稱爲縮小增量排序,它是直接插入排序算法的一種增強版。
該方法因DL.Shell於1959年提出而得名。
希爾排序的基本思想是:
把記錄按步長gap分組,對每組記錄採用直接插入排序方法進行排序。
隨着步長逐漸減少,所分紅的組包含的記錄愈來愈多,當步長的值減少到 1 時,整個數據合成爲一組,構成一組有序記錄,則完成排序。
咱們來經過演示圖,更深刻的理解一下這個過程。
在上面這幅圖中:
初始時,有一個大小爲 10 的無序序列。
在第一趟排序中,咱們不妨設 gap1 = N / 2 = 5,即相隔距離爲 5 的元素組成一組,能夠分爲 5 組。
接下來,按照直接插入排序的方法對每一個組進行排序。
在第二趟排序中,咱們把上次的 gap 縮小一半,即 gap2 = gap1 / 2 = 2 (取整數)。這樣每相隔距離爲 2 的元素組成一組,能夠分爲 2 組。
按照直接插入排序的方法對每一個組進行排序。
在第三趟排序中,再次把 gap 縮小一半,即gap3 = gap2 / 2 = 1。 這樣相隔距離爲 1 的元素組成一組,即只有一組。
按照直接插入排序的方法對每一個組進行排序。此時,排序已經結束。
須要注意一下的是,圖中有兩個相等數值的元素 5 和 5 。咱們能夠清楚的看到,在排序過程當中,兩個元素位置交換了。
因此,希爾排序是不穩定的算法。
希爾排序的分析是複雜的,時間複雜度是所取增量的函數,這涉及一些數學上的難題。可是在大量實驗的基礎上推出當n在某個範圍內時,時間複雜度能夠達到O(n^1.3)。空間複雜度O(1)
示意圖:
實現代碼:
/** *@Description:希爾排序算法實現 */ public class ShellSort { /** * 希爾排序的一趟插入 * @param arr 待排數組 * @param d 增量 */ public static void shellInsert(int[] arr, int d) { for(int i=d; i<arr.length; i++) { int j = i - d; int temp = arr[i]; //記錄要插入的數據 while (j>=0 && arr[j]>temp) { //從後向前,找到比其小的數的位置 arr[j+d] = arr[j]; //向後挪動 j -= d; } if (j != i - d) //存在比其小的數 arr[j+d] = temp; } } public static void shellSort(int[] arr) { if(arr == null || arr.length == 0) return ; int d = arr.length / 2; while(d >= 1) { shellInsert(arr, d); d /= 2; } } }
歸併排序是另外一種不一樣的排序方法,由於歸併排序使用了遞歸分治的思想,因此理解起來比較容易。其基本思想是,先遞歸劃分子問題,而後合併結果。把待排序列當作兩個有序的子序列,而後合併兩個子序列,接着再把子序列當作由兩個有序序列組成。。。。。倒着來看,其實就是先兩兩合併,而後四四合並。。。最終造成有序序列。時間複雜度爲O(nlogn),空間複雜度爲O(n)
示意圖:
舉個例子:
實現代碼:
/** *@Description:歸併排序算法的實現 */ public class MergeSort { public static void mergeSort(int[] arr) { mSort(arr, 0, arr.length-1); } /** * 遞歸分治 * @param arr 待排數組 * @param left 左指針 * @param right 右指針 */ public static void mSort(int[] arr, int left, int right) { if(left >= right) return ; int mid = (left + right) / 2; mSort(arr, left, mid); //遞歸排序左邊 mSort(arr, mid+1, right); //遞歸排序右邊 merge(arr, left, mid, right); //合併 } /** * 合併兩個有序數組 * @param arr 待合併數組 * @param left 左指針 * @param mid 中間指針 * @param right 右指針 */ public static void merge(int[] arr, int left, int mid, int right) { //[left, mid] [mid+1, right] int[] temp = new int[right - left + 1]; //中間數組 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++]; } for(int p=0; p<temp.length; p++) { arr[left + p] = temp[p]; } } }
若是在面試中有面試官要求你寫一個O(n)時間複雜度的排序算法,你千萬不要馬上說:這不可能!雖然前面基於比較的排序的下限是O(nlogn)。可是確實也有線性時間複雜度的排序,只不過有前提條件,就是待排序的數要知足必定的範圍的整數,並且計數排序須要比較多的輔助空間。其基本思想是,用待排序的數做爲計數數組的下標,統計每一個數字的個數。而後依次輸出便可獲得有序序列。
計數排序很是基礎,他的主要目的是對整數排序而且會比普通的排序算法性能更好。例如,輸入{1, 3, 5, 2, 1, 4}給計數排序,會輸出{1, 1, 2, 3, 4, 5}。這個算法由如下步驟組成:
例子:
輸入{3, 4, 3, 2, 1},最大是4,數組長度是5。
創建計數數組{0, 0, 0, 0}。
遍歷輸入數組:
{3, 4, 3, 2, 1} -> {0, 0, 1, 0}
{3, 4, 3, 2, 1} -> {0, 0, 1, 1}
{3, 4, 3, 2, 1} -> {0, 0, 2, 1}
{3, 4, 3, 2, 1} -> {0, 1, 2, 1}
{3, 4, 3, 2, 1} -> {1, 1, 2, 1}
計數數組如今是{1, 1, 2, 1},咱們如今把它寫回到輸入數組裏:
{0, 1, 2, 1} -> {1, 4, 3, 2, 1}
{o, o, 2, 1} -> {1, 2, 3, 2, 1}
{o, o, 1, 1} -> {1, 2, 3, 2, 1}
{o, o, o, 1} -> {1, 2, 3, 3, 1}
{o, o, o, o} -> {1, 2, 3, 3, 4}
這樣就排好序了。
實現代碼:
/** *@Description:計數排序算法實現 */ public class CountSort { public static void countSort(int[] arr) { if(arr == null || arr.length == 0) return ; int max = max(arr); int[] count = new int[max+1]; Arrays.fill(count, 0); for(int i=0; i<arr.length; i++) { count[arr[i]] ++; } int k = 0; for(int i=0; i<=max; i++) { for(int j=0; j<count[i]; j++) { arr[k++] = i; } } } public static int max(int[] arr) { int max = Integer.MIN_VALUE; for(int ele : arr) { if(ele > max) max = ele; } return max; } }
桶排序算是計數排序的一種改進和推廣,可是網上有許多資料把計數排序和桶排序混爲一談。其實桶排序要比計數排序複雜許多。
對桶排序的分析和解釋借鑑這位兄弟的文章(有改動):http://hxraid.iteye.com/blog/647759
桶排序的基本思想:
假設有一組長度爲N的待排關鍵字序列K[1....n]。首先將這個序列劃分紅M個的子區間(桶) 。而後基於某種映射函數 ,將待排序列的關鍵字k映射到第i個桶中(即桶數組B的下標 i) ,那麼該關鍵字k就做爲B[i]中的元素(每一個桶B[i]都是一組大小爲N/M的序列)。接着對每一個桶B[i]中的全部元素進行比較排序(可使用快排)。而後依次枚舉輸出B[0]….B[M]中的所有內容便是一個有序序列。bindex=f(key) 其中,bindex 爲桶數組B的下標(即第bindex個桶), k爲待排序列的關鍵字。桶排序之因此可以高效,其關鍵在於這個映射函數,它必須作到:若是關鍵字k1<k2,那麼f(k1)<=f(k2)。也就是說B(i)中的最小數據都要大於B(i-1)中最大數據。很顯然,映射函數的肯定與數據自己的特色有很大的關係。
舉個例子:
假如待排序列K= {4九、 38 、 3五、 97 、 7六、 73 、 2七、 49 }。這些數據所有在1—100之間。所以咱們定製10個桶,而後肯定映射函數f(k)=k/10。則第一個關鍵字49將定位到第4個桶中(49/10=4)。依次將全部關鍵字所有堆入桶中,並在每一個非空的桶中進行快速排序後獲得如圖所示。只要順序輸出每一個B[i]中的數據就能夠獲得有序序列了。
桶排序分析:
桶排序利用函數的映射關係,減小了幾乎全部的比較工做。實際上,桶排序的f(k)值的計算,其做用就至關於快排中劃分,希爾排序中的子序列,歸併排序中的子問題,已經把大量數據分割成了基本有序的數據塊(桶)。而後只須要對桶中的少許數據作先進的比較排序便可。
對N個關鍵字進行桶排序的時間複雜度分爲兩個部分:
(1) 循環計算每一個關鍵字的桶映射函數,這個時間複雜度是O(N)。
(2) 利用先進的比較排序算法對每一個桶內的全部數據進行排序,其時間複雜度爲 ∑ O(Ni*logNi) 。其中Ni 爲第i個桶的數據量。
很顯然,第(2)部分是桶排序性能好壞的決定因素。儘可能減小桶內數據的數量是提升效率的惟一辦法(由於基於比較排序的最好平均時間複雜度只能達到O(N*logN)了)。所以,咱們須要儘可能作到下面兩點:
(1) 映射函數f(k)可以將N個數據平均的分配到M個桶中,這樣每一個桶就有[N/M]個數據量。
(2) 儘可能的增大桶的數量。極限狀況下每一個桶只能獲得一個數據,這樣就徹底避開了桶內數據的「比較」排序操做。固然,作到這一點很不容易,數據量巨大的狀況下,f(k)函數會使得桶集合的數量巨大,空間浪費嚴重。這就是一個時間代價和空間代價的權衡問題了。
對於N個待排數據,M個桶,平均每一個桶[N/M]個數據的桶排序平均時間複雜度爲:
O(N)+O(M*(N/M)*log(N/M))=O(N+N*(logN-logM))=O(N+N*logN-N*logM)
當N=M時,即極限狀況下每一個桶只有一個數據時。桶排序的最好效率可以達到O(N)。
總結: 桶排序的平均時間複雜度爲線性的O(N+C),其中C=N*(logN-logM)。若是相對於一樣的N,桶數量M越大,其效率越高,最好的時間複雜度達到O(N)。 固然桶排序的空間複雜度 爲O(N+M),若是輸入數據很是龐大,而桶的數量也很是多,則空間代價無疑是昂貴的。此外,桶排序是穩定的。
實現代碼:
/** *@Description:桶排序算法實現 */ public class BucketSort { public static void bucketSort(int[] arr) { if(arr == null && arr.length == 0) return ; int bucketNums = 10; //這裏默認爲10,規定待排數[0,100) List<List<Integer>> buckets = new ArrayList<List<Integer>>(); //桶的索引 for(int i=0; i<10; i++) { buckets.add(new LinkedList<Integer>()); //用鏈表比較合適 } //劃分桶 for(int i=0; i<arr.length; i++) { buckets.get(f(arr[i])).add(arr[i]); } //對每一個桶進行排序 for(int i=0; i<buckets.size(); i++) { if(!buckets.get(i).isEmpty()) { Collections.sort(buckets.get(i)); //對每一個桶進行快排 } } //還原排好序的數組 int k = 0; for(List<Integer> bucket : buckets) { for(int ele : bucket) { arr[k++] = ele; } } } /** * 映射函數 * @param x * @return */ public static int f(int x) { return x / 10; } }
基數排序又是一種和前面排序方式不一樣的排序方式,基數排序不須要進行記錄關鍵字之間的比較。基數排序是一種藉助多關鍵字排序思想對單邏輯關鍵字進行排序的方法。所謂的多關鍵字排序就是有多個優先級不一樣的關鍵字。好比說成績的排序,若是兩我的總分相同,則語文高的排在前面,語文成績也相同則數學高的排在前面。。。若是對數字進行排序,那麼個位、十位、百位就是不一樣優先級的關鍵字,若是要進行升序排序,那麼個位、十位、百位優先級一次增長。基數排序是經過屢次的收分配和收集來實現的,關鍵字優先級低的先進行分配和收集。
舉個例子:
第一步
假設原來有一串數值以下所示:
73, 22, 93, 43, 55, 14, 28, 65, 39, 81
首先根據個位數的數值,在走訪數值時將它們分配至編號0到9的桶子中:
0
1 81
2 22
3 73 93 43
4 14
5 55 65
6
7
8 28
9 39
第二步
接下來將這些桶子中的數值從新串接起來,成爲如下的數列:
81, 22, 73, 93, 43, 14, 55, 65, 28, 39
接着再進行一次分配,此次是根據十位數來分配:
0
1 14
2 22 28
3 39
4 43
5 55
6 65
7 73
8 81
9 93
第三步
接下來將這些桶子中的數值從新串接起來,成爲如下的數列:
14, 22, 28, 39, 43, 55, 65, 73, 81, 93
這時候整個數列已經排序完畢;若是排序的對象有三位數以上,則持續進行以上的動做直至最高位數爲止。
時間效率:設待排序列爲n個記錄,d個關鍵碼,關鍵碼的取值範圍爲radix,則進行鏈式基數排序的時間複雜度爲O(d(n+radix)),其中,一趟分配時間複雜度爲O(n),一趟收集時間複雜度爲O(radix),共進行d趟分配和收集。 空間效率:須要2*radix個指向隊列的輔助空間,以及用於靜態鏈表的n個指針。
實現代碼:
/** *@Description:基數排序算法實現 */ public class RadixSort { public static void radixSort(int[] arr) { if(arr == null && arr.length == 0) return ; int maxBit = getMaxBit(arr); for(int i=1; i<=maxBit; i++) { List<List<Integer>> buf = distribute(arr, i); //分配 collecte(arr, buf); //收集 } } /** * 分配 * @param arr 待分配數組 * @param iBit 要分配第幾位 * @return */ public static List<List<Integer>> distribute(int[] arr, int iBit) { List<List<Integer>> buf = new ArrayList<List<Integer>>(); for(int j=0; j<10; j++) { buf.add(new LinkedList<Integer>()); } for(int i=0; i<arr.length; i++) { buf.get(getNBit(arr[i], iBit)).add(arr[i]); } return buf; } /** * 收集 * @param arr 把分配的數據收集到arr中 * @param buf */ public static void collecte(int[] arr, List<List<Integer>> buf) { int k = 0; for(List<Integer> bucket : buf) { for(int ele : bucket) { arr[k++] = ele; } } } /** * 獲取最大位數 * @param x * @return */ public static int getMaxBit(int[] arr) { int max = Integer.MIN_VALUE; for(int ele : arr) { int len = (ele+"").length(); if(len > max) max = len; } return max; } /** * 獲取x的第n位,若是沒有則爲0. * @param x * @param n * @return */ public static int getNBit(int x, int n) { String sx = x + ""; if(sx.length() < n) return 0; else return sx.charAt(sx.length()-n) - '0'; } }
在前面的介紹和分析中咱們提到了冒泡排序、選擇排序、插入排序三種簡單的排序及其變種快速排序、堆排序、希爾排序三種比較高效的排序。後面咱們又分析了基於分治遞歸思想的歸併排序還有計數排序、桶排序、基數排序三種線性排序。咱們能夠知道排序算法要麼簡單有效,要麼是利用簡單排序的特色加以改進,要麼是以空間換取時間在特定狀況下的高效排序。可是這些排序方法都不是固定不變的,須要結合具體的需求和場景來選擇甚至組合使用。才能達到高效穩定的目的。沒有最好的排序,只有最適合的排序。
下面就總結一下排序算法的各自的使用場景和適用場合。
1. 從平均時間來看,快速排序是效率最高的,但快速排序在最壞狀況下的時間性能不如堆排序和歸併排序。然後者相比較的結果是,在n較大時歸併排序使用時間較少,但使用輔助空間較多。
2. 上面說的簡單排序包括除希爾排序以外的全部冒泡排序、插入排序、簡單選擇排序。其中直接插入排序最簡單,但序列基本有序或者n較小時,直接插入排序是好的方法,所以常將它和其餘的排序方法,如快速排序、歸併排序等結合在一塊兒使用。
3. 基數排序的時間複雜度也能夠寫成O(d*n)。所以它最使用於n值很大而關鍵字較小的的序列。若關鍵字也很大,而序列中大多數記錄的最高關鍵字均不一樣,則亦可先按最高關鍵字不一樣,將序列分紅若干小的子序列,然後進行直接插入排序。
4. 從方法的穩定性來比較,基數排序是穩定的內排方法,全部時間複雜度爲O(n^2)的簡單排序也是穩定的。可是快速排序、堆排序、希爾排序等時間性能較好的排序方法都是不穩定的。穩定性須要根據具體需求選擇。
5. 上面的算法實現大多數是使用線性存儲結構,像插入排序這種算法用鏈表實現更好,省去了移動元素的時間。具體的存儲結構在具體的實現版本中也是不一樣的。
總結二:
關於時間複雜度:
(1)平方階(O(n2))排序
各種簡單排序:直接插入、直接選擇和冒泡排序;
(2)線性對數階(O(nlog2n))排序
快速排序、堆排序和歸併排序;
(3)O(n1+§))排序,§是介於0和1之間的常數。
希爾排序
(4)線性階(O(n))排序
基數排序,此外還有桶、箱排序。
關於穩定性:
穩定的排序算法:冒泡排序、插入排序、歸併排序和基數排序
不是穩定的排序算法:選擇排序、快速排序、希爾排序、堆排序。