Java面試寶典系列之基礎排序算法java
本文就是介紹一些常見的排序算法。排序是一個很是常見的應用場景,不少時候,咱們須要根據本身須要排序的數據類型,來自定義排序算法,可是,在這裏,咱們只介紹這些基礎排序算法,包括:插入排序、選擇排序、冒泡排序、快速排序(重點)、堆排序、歸併排序等等。看下圖:面試
給定數組:int data[] = {9,2,7,19,100,97,63,208,55,78}算法
1、直接插入排序(內部排序、O(n2)、穩定)shell
原理:從待排序的數中選出一個來,插入到前面的合適位置。api
public static void insertSort() {
for (int i = 1; i < unsorted.Length; i++) { if (unsorted[i - 1] > unsorted[i]) { int temp = unsorted[i]; int j = i; while (j > 0 && unsorted[j - 1] > temp) { unsorted[j] = unsorted[j - 1]; j--; } unsorted[j] = temp; } }
}
public static void insertSort() {
for (int i = 1; i < data.length; i++) {
if (data[i] >= data[i - 1]) {
continue;
}
int temp = data[i];
for (int j = i - 1; j >= 0; j--) {
if (temp < data[j]) {
data[j + 1] = data[j];
if(j==0){
data[j ] = temp;
}
} else {
data[j + 1] = temp;
break;
}
}
}
}
我簡單的講解一下過程:思路上從待排序的數據中選出一個,插入到前面合適的位置,耗時點在插入方面,合適的位置意味着咱們須要進行比較找出哪是合適的位置,舉個例子:對於9,2,7,19,100,97,63,208,55,78 這組數,第一個數9前面沒有,不作操做,當第一個數完後,剩下的數就是待排序的數,咱們將要從除去9開始的書中選出一個插入到前面合適的位置,拿到2後, 放在tmp上,進行註釋中的2處的代碼,2處的代碼就是經過循環找出這個合適的位置,發現比tmp大的數,當即將該數向後移動一位(這樣作的目的是:前面 須要空出一位來進行插入),最後經過註釋3處的代碼將數插入。數組
本排序適合:基本有序的數據數據結構
2、選擇排序(O(n2)、不穩定)
與直接插入排序正好相反,選擇排序是從待排序的數中選出最小的放在已經排好的後面,這個算法選數耗時。ide
經過循環,找出最小的數的下標,賦值於k,即k永遠保持待排序數據中最小的數的下標,最後和當前位置i互換數據便可。
函數
3、快速排序(O(nlogn)、不穩定)性能
快速排序簡稱快排,是一種比較快的排序,適合基本無序的數據,爲何這麼說呢?下面我說下快排的思路:
設置兩個指針:i和j,分別指向第一個和最後一個,i像後移動,j向前移動,選第一個數爲標準(通常這樣作,固然快排的關鍵就是這個「標準」的選取),從後面開始,找到第一個比標準小的數,互換位置,而後再從前面,找到第一個比標準大的數,互換位置,第一趟的結果就是標準左邊的都小於標準,右邊的都大於標準(但不必定有序),分紅兩撥後,繼續遞歸的使用上述方法,最終有序!代碼以下:
https://tool.lu/
class Untitled {
public static void main(String[] args) {
int data[] = { 9, 2, 7, 19, 100, 97, 63, 208, 55, 78 };
kuaisu1(data,0,data.length-1);
for (int k = 0; k < data.length; k++) {
System.out.println(data[k]);
}
}
public static void kuaisu1(int[] args,int left,int right) {
if(left<right){
int center=kuaisu2(args,left,right);
kuaisu1(args,left,center-1);
kuaisu1(args,center+1,right);
}
}
public static int kuaisu2(int[] args,int left,int right) {
int temp=args[left];
while(left<right){
while(left<right&&args[right]>=temp){
right--;
}
args[left]=args[right];
while(left<right&&args[left]<=temp){
left++;
}
args[right]=args[left];
}
args[left]=temp;
return left;
}
}
看看上面的圖,基本就明白了。
4、冒泡排序(穩定、基本有序可達O(n),最壞狀況爲O(n2))
冒泡排序是一種很簡單,不管是理解仍是時間起來都比較容易的一種排序算法,思路簡單:小的數一點一點向前起泡,最終有序。
public static void main(String[] args) {
int data[] = { 9, 2, 7, 19, 100, 97, 63, 208, 55, 78 };
for (int i = 0; i < data.length - 1; i++) {
for (int j =0; j < data.length -i-1; j++) {
if (data[j] > data[j + 1]) {
int tmp = data[j];
data[j] = data[j + 1];
data[j + 1] = tmp;
}
}
}
for (int k = 0; k < data.length; k++) {
System.out.println(data[k]);
}
}
5、堆排序
咱們這裏不詳細介紹概念,堆的話,你們只要記得堆是一個徹底二叉樹(什麼是徹底二叉樹,請不懂的讀者去查資料),堆排序分爲兩種堆,大頂堆和小頂堆,大頂堆的意思就是堆頂元素是整個堆中最大的,小頂堆的意思就是堆頂元素是整個堆中最小的,知足:任何一非葉節點的關鍵字不大於或者不小於其左右孩子節點的關鍵字。堆排序是一個相對難理解的過程,下面我會較爲清楚、詳細的講解一下堆排序。堆排序分爲三個過程:
建堆:從一個數組順序讀取元素,創建一個堆(徹底二叉樹)
初始化:將堆進行調整,使得堆頂爲最大(最大堆)或者最小(最小堆)的元素
維護:將堆頂元素出堆後,須要將堆的最後一個節點補充到堆頂,由於這樣破壞了堆的秩序,因此須要進行維護。下面咱們圖示一下:
通常狀況,建堆和初始化同步進行,
最後爲以下所示,即爲建堆、初始化成功。
咱們能夠觀察下這個最大堆,看出堆頂是整個堆中最大的元素,並且除葉子節點外每一個節點都大於其子節點。下面的過程就是當咱們輸出堆頂元素後,對堆進行維護。
過程是這樣:將堆頂元素出堆後,用最後一個元素補充堆頂元素,這樣破壞了以前的秩序,須要從新維護堆,在堆頂元素的左右節點中選出較小的和堆頂互換,而後一直遞歸下去,因此每次出一個元素,須要一次維護,堆排序適合解決topK問題,能將複雜度降到nlogK。下面是代碼:
慢慢理解一下,仍是容易明白的!
6、歸併排序
歸併排序是創建在歸併操做上的一種有效的排序算法。該算法是採用分治法(Divide and Conquer)的一個很是典型的應用。
首先考慮下如何將將二個有序數列合併。這個很是簡單,只要從比較二個數列的第一個數,誰小就先取誰,取了後就在對應數列中刪除這個數。而後再進行比較,若是有數列爲空,那直接將另外一個數列的數據依次取出便可。
能夠看出合併有序數列的效率是比較高的,能夠達到O(n)。解決了上面的合併有序數列問題,再來看歸併排序,其的基本思路就是將數組分紅二組A,B,若是這二組組內的數據都是有序的,那麼就能夠很方便的將這二組數據進行排序。如何讓這二組組內數據有序了?能夠將A,B組各自再分紅二組。依次類推,當分出來的小組只有一個數據時,能夠認爲這個小組組內已經達到了有序,而後再合併相鄰的二個小組就能夠了。這樣經過先遞歸的分解數列,再合併數列就完成了歸併排序。下面是歸併排序代碼:
7、希爾排序(不穩定、O(nlogn))
d1 = n/2,d2 = d1/2 ...
舉例一下:{9,8,7,6,5,4,3,2,1,0} 10個數,現分爲5組(9,4),(8,3),(7,2),(6,1),(5,0),而後分別對每組進行直接插入排序獲得:
(4,9),(3,8),(2,7),(1,6),(0,5),再將這5組分爲2組(4,3,2,1,0),(9,8,7,6,5)分別對這兩組進行直插排序,得:(0,1,2,3,4),(5,6,7,8,9)最終有序。
8、多路快排
JDK1.8中Arrays.sort()採用的排序算法,具備較快的時間複雜度和穩定性,基本思路爲:
1. 選取兩個中軸P1, P2。
2. 假設P1<P2,不然交換。
3. 過程當中原數組會分爲四個部分:小於中軸1,大於中軸2,介於兩個中軸之間,未排序部分(剛開始除了兩個中軸,其它元素都屬於這部分)。
4. 開始後,從未排序部分選取一個數,和兩個中軸做比較,而後放到合適的位置,一直到未排序部分無數據,結束一趟排序。
5. 遞歸地處理子數組,穩定排序,時間複雜度穩定爲O(nlogn)。
詳情能夠參見個人另外一篇博文《Java之美[從菜鳥到高手演練]之Arrays類及其方法分析》
9、其餘排序
下面的一段轉載自博友@清蒸水皮 --- 補充於2015年1月14日
==============================================
計數排序
當輸入的元素是 n 個 0 到 k 之間的整數時,它的運行時間是 O(n + k)。計數排序不是比較排序,排序的速度快於任何比較排序算法。
因爲用來計數的數組C的長度取決於待排序數組中數據的範圍(等於待排序數組的最大值與最小值的差加上1),這使得計數排序對於數據範圍很大的數組,須要大量時間和內存。例如:計數排序是用來排序0到100之間的數字的最好的算法,可是它不適合按字母順序排序人名。可是,計數排序能夠用在基數排序中的算法來排序數據範圍很大的數組。
算法的步驟以下:
貼上代碼:
桶排序
桶排序的基本思想
假設有一組長度爲N的待排關鍵字序列K[1....n]。首先將這個序列劃分紅M個的子區間(桶) 。而後基於某種映射函數 ,將待排序列的關鍵字k映射到第i個桶中(即桶數組B的下標 i) ,那麼該關鍵字k就做爲B[i]中的元素(每一個桶B[i]都是一組大小爲N/M的序列)。接着對每一個桶B[i]中的全部元素進行比較排序(可使用快排)。而後依次枚舉輸出B[0]....B[M]中的所有內容便是一個有序序列。假如待排序列K= {4九、 38 、 3五、 97 、 7六、 73 、 2七、 49 }。這些數據所有在1—100之間。所以咱們定製10個桶,而後肯定映射函數f(k)=k/10。則第一個關鍵字49將定位到第4個桶中(49/10=4)。依次將全部關鍵字所有堆入桶中,並在每一個非空的桶中進行快速排序。
桶排序代價分析
桶排序利用函數的映射關係,減小了幾乎全部的比較工做。實際上,桶排序的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),若是輸入數據很是龐大,而桶的數量也很是多,則空間代價無疑是昂貴的。此外,桶排序是穩定的。
我我的還有一個感覺:在查找算法中,基於比較的查找算法最好的時間複雜度也是O(logN)。好比折半查找、平衡二叉樹、紅黑樹等。可是Hash表卻有O(C)線性級別的查找效率(不衝突狀況下查找效率達到O(1))。你們好好體會一下:Hash表的思想和桶排序是否是有一曲同工之妙呢?
基數排序
上面的問題是多關鍵字的排序,但單關鍵字也仍然可使用這種方式。
好比字符串「abcd」 「aesc」 "dwsc" "rews"就能夠把每一個字符當作一個關鍵字。另外還有整數 42五、32一、23五、432也能夠每一個位上的數字爲一個關鍵字。
基數排序的思想就是將待排數據中的每組關鍵字依次進行桶分配。好比下面的待排序列:
27八、10九、06三、930、58九、18四、50五、26九、00八、083
咱們將每一個數值的個位,十位,百位分紅三個關鍵字: 278 -> k1(個位)=8 ,k2(十位)=7 ,k3=(百位)=2。
而後從最低位個位開始(從最次關鍵字開始),對全部數據的k1關鍵字進行桶分配(由於,每一個數字都是 0-9的,所以桶大小爲10),再依次輸出桶中的數據獲得下面的序列。
930、06三、08三、18四、50五、27八、00八、10九、58九、269
再對上面的序列接着進行鍼對k2的桶分配,輸出序列爲:
50五、00八、10九、930、06三、26九、27八、08三、18四、589
最後針對k3的桶分配,輸出序列爲:
00八、06三、08三、10九、18四、26九、27八、50五、58九、930
性能分析
很明顯,基數排序的性能比桶排序要略差。每一次關鍵字的桶分配都須要O(N)的時間複雜度,並且分配以後獲得新的關鍵字序列又須要O(N)的時間複雜度。假如待排數據能夠分爲d個關鍵字,則基數排序的時間複雜度將是O(d*2N) ,固然d要遠遠小於N,所以基本上仍是線性級別的。基數排序的空間複雜度爲O(N+M),其中M爲桶的數量。通常來講N>>M,所以額外空間須要大概N個左右。
可是,對比桶排序,基數排序每次須要的桶的數量並很少。並且基數排序幾乎不須要任何「比較」操做,而桶排序在桶相對較少的狀況下,桶內多個數據必須進行基於比較操做的排序。所以,在實際應用中,基數排序的應用範圍更加普遍。
=============================================
文章最後,給你們推薦一個數據結構學習的網站,有flash演示的:http://www.tyut.edu.cn/kecheng1/site01/index.asp
借鑑:http://blog.csdn.net/zhangerqing/article/details/8831542