一、插入排序
直接插入排序(Insertion Sort)的算法描述是一種簡單直觀的排序算法。它的工作原理是通過構建有序序列,對於未排序數據,在已排序序列中從後向前掃描,找到相應位置並插入。插入排序在實現上,通常採用in-place排序(即只需用到O(1)的額外空間的排序),因而在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,爲最新元素提供插入空間。
代碼實現:
public class Inseretion_Sort { public static void main(String[] args) { //要排序的原始數組 int score[]={900,878,891,904,865,912,868,870,898,903}; //打印數組元素 System.out.print("未排序數組:"); for (int i=0;i<score.length ;i++ ){ System.out.print(score[i]+" "); } System.out.println(); //插入算法 for (int i=1;i<score.length ;i++ ){ for (int k=i;k>0 ;k-- ){ if (score[k]>score[k-1]){ int temp=score[k]; score[k]=score[k-1]; score[k-1]=temp; } } //每遍歷一次數組的結果 System.out.print("第"+i+"次插入:"); for (int j=0;j<score.length ;j++ ){ System.out.print(score[j]+" "); } System.out.println(); } //最終排序結果 System.out.print("最終排序結果:"); for (int j=0;j<score.length ;j++ ){ System.out.print(score[j]+" "); } System.out.println(); } }
希爾排序,也稱遞減增量排序算法,是插入排序的一種高速而穩定的改進版本。 它的基本思想是先取一個小於n的整數d1作爲第一個增量,把文件的全部記錄分成d1個組。所有距離爲dl的倍數的記錄放在同一個組中。先在各組內進行直接 插人排序;然後,取第二個增量d2<d1重複上述的分組和排序,直至所取的增量dt=1(dt<dt-l<…<d2< d1),即所有記錄放在同一組中進行直接插入排序爲止。該方法實質上是一種分組插入方法。
代碼實現:
public class Shell_Sort { public static void main(String[] args) { //要排序的原始數組 int score[]={900,878,891,904,865,912,868,870,898,903}; //打印數組元素 System.out.print("未排序數組:"); for (int i=0;i<score.length ;i++ ){ System.out.print(score[i]+" "); } System.out.println(); //希爾算法 for (int incre=score.length/2;incre>0 ;incre=incre/2 ) { for (int i=incre;i<score.length;i++ ) { int temp=score[i]; int k=0; for (k=i;k>=incre ;k=k-incre ) { if (temp>score[k-incre]) { score[k]=score[k-incre]; }else{ break; } } score[k]=temp; } } //最終排序結果 System.out.print("最終排序結果:"); for (int j=0;j<score.length ;j++ ){ System.out.print(score[j]+" "); } System.out.println(); } }
二、交換排序
冒泡排序(Bubble Sort)是一種簡單的排序算法。它重複地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個算法的名字由來是因爲越小的元素會經由交換慢慢“浮”到數列的頂端。
代碼實現:
public class Bubble_Sort { public static void main(String[] args) { //要排序的原始數組 int score[]={900,878,891,904,865,912,868,870,898,903}; //打印數組元素 System.out.print("未排序數組:"); for (int i=0;i<score.length ;i++ ){ System.out.print(score[i]+" "); } System.out.println(); //冒泡算法 for (int i=0;i<score.length-1 ; i++){ for (int k=0;k<score.length-i-1 ;k++ ){ if (score[k]<score[k+1]){ int temp=score[k]; score[k]=score[k+1]; score[k+1]=temp; } } //每遍歷一次數組的結果 System.out.print("第"+(i+1)+"次冒泡:"); for (int j=0;j<score.length ;j++ ){ System.out.print(score[j]+" "); } System.out.println(); } //最終排序結果 System.out.print("最終排序結果:"); for (int j=0;j<score.length ;j++ ){ System.out.print(score[j]+" "); } System.out.println(); } }
快速排序是由東尼·霍爾所發展的一種排序算法 基本思想是:通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然後再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。
代碼實現:
package com.mzsx.binarySearch; import com.google.gson.Gson; public class Quick_Sort { public static void main(String[] args) { int array[] = { 2, 44, 23, 5, 34, 13, 29, 6, 24, 26, 18, 10, 25, 12, 17 }; array = quickSort(array, 0, array.length-1); for (int i = 0; i < array.length; i++) { System.out.print(array[i] + " "); } System.out.println(); } // 快速排序算法 public static int[] quickSort(int[] arr, int lowIndex, int highIndex) { if (lowIndex < highIndex) { int start = arr[lowIndex]; // 起始數 int low = lowIndex; int high = highIndex+1; while (true) { //向右尋找大於s的數組元素的索引 while (low + 1 < arr.length && arr[++low] < start); //向左尋找小於s的數組元素的索引 while (high - 1 > -1 && arr[--high] > start); //如果i>j,則退出循環 if (low >= high) { break; } else { // 交換i和j位置的元素值 int temp = arr[low]; arr[low] = arr[high]; arr[high] = temp; } } arr[lowIndex] = arr[high]; arr[high] = start; // 對右邊進行遞歸 quickSort(arr, high + 1, highIndex); // 對左邊進行遞歸 quickSort(arr, lowIndex, high - 1); } return arr; } }
三、選擇排序
直接選擇排序(Selection sort)是一種簡單直觀的排序算法。它的工作原理如下。首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小元素,然後放到排序序列末尾(目前已被排序的序列)。以此類推,直到所有元素均排序完畢。
代碼實現:
public class Select_Sort { public static void main(String[] args) { //要排序的原始數組 int score[]={900,878,891,904,865,912,868,870,898,903}; //打印數組元素 System.out.print("未排序數組:"); for (int i=0;i<score.length ;i++ ){ System.out.print(score[i]+" "); } System.out.println(); //選擇算法 for (int i=0;i< score.length; i++){ int lowIndex=i; for (int k=i+1;k<score.length ;k++ ){ if (score[k]>score[lowIndex]){ lowIndex=k; } } int temp=score[i]; score[i]=score[lowIndex]; score[lowIndex]=temp; //每遍歷一次數組的結果 System.out.print("第"+(i+1)+"次選擇:"); for (int j=0;j<score.length ;j++ ){ System.out.print(score[j]+" "); } System.out.println(); } //最終排序結果 System.out.print("最終排序結果:"); for (int j=0;j<score.length ;j++ ){ System.out.print(score[j]+" "); } System.out.println(); } }
四、排序算法特點,算法複雜度和比較
直接插入排序
如果目標是把n個元素的序列升序排列,那麼採用直接插入排序存在最好情況和最壞情況。最好情況就是,序列已經是升序排列了,在這種情況下,需要進行的比較操作需(n-1)次即可。最壞情況就是,序列是降序排列,那麼此時需要進行的比較共有n(n-1)/2次。直接插入排序的賦值操作是比較操作的次數減去(n-1)次。平均來說直接插入排序算法複雜度爲O(n2)。因而,直接插入排序不適合對於數據量比較大的排序應用。但是,如果需要排序的數據量很小,例如,量級小於千,那麼直接插入排序還是一個不錯的選擇。 插入排序在工業級庫中也有着廣泛的應用,在STL的sort算法和stdlib的qsort算法中,都將插入排序作爲快速排序的補充,用於少量元素的排序(通常爲8個或以下)。
希爾排序
希爾排序是基於插入排序的一種算法, 在此算法基礎之上增加了一個新的特性,提高了效率。希爾排序的時間複雜度爲 O(N*(logN)2), 沒有快速排序算法快
O(N*(logN)),因此中等大小規模表現良好,對規模非常大的數據排序不是最優選擇。
但是比O(N2)複雜度的算法快得多。並且希爾排序非常容易實現,算法代碼短而簡單。
此外,希爾算法在最壞的情況下和平均情況下執行效率相差不是很多,與此同時快速排序在最壞
的情況下執行的效率會非常差。專家們提倡,幾乎任何排序工作在開始時都可以用希爾排序,若在實際使用中證明它不夠快,
再改成快速排序這樣更高級的排序算法.
希爾排序是按照不同步長對元素進行插入排序,當剛開始元素很無序的時候,步長最大,所以插入排序的元素個數很少,速度很快;當元素基本有序了,步長 很小,插入排序對於有序的序列效率很高。所以,希爾排序的時間複雜度會比o(n^2)好一些。由於多次插入排序,我們知道一次插入排序是穩定的,不會改變 相同元素的相對順序,但在不同的插入排序過程中,相同的元素可能在各自的插入排序中移動,最後其穩定性就會被打亂,所以shell排序是不穩定的
冒泡排序
時間複雜度爲O(n^2),雖然不及堆排序、快速排序的O(nlogn,底數爲2),但是有兩個優點:1.“編程複雜度”很低,很容易寫出代碼;2.具有穩定性。
其中若記錄序列的初始狀態爲"正序",則冒泡排序過程只需進行一趟排序,在排序過程中只需進行n-1次比較,且不移動記錄;反之,若記錄序列的初始狀態爲"逆序",則需進行n(n-1)/2次比較和記錄移動。因此冒泡排序總的時間複雜度爲O(n*n)。
快速排序
在最好的情況,每次我們執行一次分割,我們會把一個數列分爲兩個幾近相等的片段。這個意思就是每次遞迴調用處理一半大小的數列。因此,在到達大小爲一的數列前,我們只要作 log n 次巢狀的調用。這個意思就是調用樹的深度是O(log n)。但是在同一階層的兩個程序調用中,不會處理到原來數列的相同部份;因此,程序調用的每一階層總共全部僅需要O(n)的時間(每個調用有某些共同的額外耗費,但是因爲在每一階層僅僅只有O(n)個調用,這些被歸納在O(n)係數中)。結果是這個算法僅需使用O(n log n)時間。
另外一個方法是爲T(n)設立一個遞迴關係式,也就是需要排序大小爲n的數列所需要的時間。在最好的情況下,因爲一個單獨的快速排序調用牽涉了O(n)的工作,加上對n/2大小之數列的兩個遞迴調用,這個關係式可以是:
T(n) = O(n) + 2T(n/2)
解決這種關係式型態的標準數學歸納法技巧告訴我們T(n) = O(n log n)。
事實上,並不需要把數列如此精確地分割;即使如果每個基準值將元素分開爲 99% 在一邊和 1% 在另一邊,調用的深度仍然限制在 100log n,所以全部執行時間依然是O(n log n)。
然而,在最壞的情況是,兩子數列擁有大各爲 1 和 n-1,且調用樹(call tree)變成爲一個 n 個巢狀(nested)呼叫的線性連串(chain)。第 i 次呼叫作了O(n-i)的工作量,且遞迴關係式爲:
T(n) = O(n) + T(1) + T(n - 1) = O(n) + T(n - 1)
這與插入排序和選擇排序有相同的關係式,以及它被解爲T(n) = O(n2)。
討論平均複雜度情況下,即使如果我們無法隨機地選擇基準數值,對於它的輸入之所有可能排列,快速排序仍然只需要O(n log n)時間。因爲這個平均是簡單地將輸入之所有可能排列的時間加總起來,除以n這個因子,相當於從輸入之中選擇一個隨機的排列。當我們這樣作,基準值本質上就是隨機的,導致這個算法與亂數快速排序有一樣的執行時間。
更精確地說,對於輸入順序之所有排列情形的平均比較次數,可以藉由解出這個遞迴關係式可以精確地算出來。
在這裏,n-1 是分割所使用的比較次數。因爲基準值是相當均勻地落在排列好的數列次序之任何地方,總和就是所有可能分割的平均。
這個意思是,平均上快速排序比理想的比較次數,也就是最好情況下,只大約比較糟39%。這意味着,它比最壞情況較接近最好情況。這個快速的平均執行時間,是快速排序比其他排序算法有實際的優勢之另一個原因。
討論空間複雜度時 被快速排序所使用的空間,依照使用的版本而定。使用原地(in-place)分割的快速排序版本,在任何遞迴呼叫前,僅會使用固定的額外空間。然而,如果需要產生O(log n)巢狀遞迴呼叫,它需要在他們每一個儲存一個固定數量的資訊。因爲最好的情況最多需要O(log n)次的巢狀遞迴呼叫,所以它需要O(log n)的空間。最壞情況下需要O(n)次巢狀遞迴呼叫,因此需要O(n)的空間。
然而我們在這裏省略一些小的細節。如果我們考慮排序任意很長的數列,我們必須要記住我們的變量像是left和right,不再被認爲是佔據固定的空間;也需要O(log n)對原來一個n項的數列作索引。因爲我們在每一個堆棧框架中都有像這些的變量,實際上快速排序在最好跟平均的情況下,需要O(log2n)空間的位元數,以及最壞情況下O(n log n)的空間。然而,這並不會太可怕,因爲如果一個數列大部份都是不同的元素,那麼數列本身也會佔據O(n log n)的空間字節。
非原地版本的快速排序,在它的任何遞迴呼叫前需要使用O(n)空間。在最好的情況下,它的空間仍然限制在O(n),因爲遞迴的每一階中,使用與上一次所使用最多空間的一半,且
它的最壞情況是很恐怖的,需要
空間,遠比數列本身還多。如果這些數列元素本身自己不是固定的大小,這個問題會變得更大;舉例來說,如果數列元素的大部份都是不同的,每一個將會需要大約O(log n)爲原來儲存,導致最好情況是O(n log n)和最壞情況是O(n2 log n)的空間需求。
不同條件下,排序方法的選擇
(1)若n較小(如n≤50),可採用直接插入或直接選擇排序。
當記錄規模較小時,直接插入排序較好;否則因爲直接選擇移動的記錄數少於直接插入,應選直接選擇排序爲宜。
(2)若文件初始狀態基本有序(指正序),則應選用直接插入、冒泡或隨機的快速排序爲宜;
(3)若n較大,則應採用時間複雜度爲O(nlgn)的排序方法:快速排序、堆排序或歸併排序。
快速排序是目前基於比較的內部排序中被認爲是最好的方法,當待排序的關鍵字是隨機分佈時,快速排序的平均時間最短;
堆排序所需的輔助空間少於快速排序,並且不會出現快速排序可能出現的最壞情況。這兩種排序都是不穩定的。
若要求排序穩定,則可選用歸併排序。但本章介紹的從單個記錄起進行兩兩歸併的 排序算法並不值得提倡,通常可以將它和直接插入排序結合在一起使用。先利用直接插入排序求得較長的有序子文件,然後再兩兩歸併之。因爲直接插入排序是穩定的,所以改進後的歸併排序仍是穩定的。
(4)在基於比較的排序方法中,每次比較兩個關鍵字的大小之後,僅僅出現兩種可能的轉移,因此可以用一棵二叉樹來描述比較判定過程。
當文件的n個關鍵字隨機分佈時,任何藉助於"比較"的排序算法,至少需要O(nlgn)的時間。
箱排序和基數排序只需一步就會引起m種可能的轉移,即把一個記錄裝入m個箱子之一,因此在 一般情況下,箱排序和基數排序可能在O(n)時間內完成對n個記錄的排序。但是,箱排序和基數排序只適用於像字符串和整數這類有明顯結構特徵的關鍵字,而 當關鍵字的取值範圍屬於某個無窮集合(例如實數型關鍵字)時,無法使用箱排序和基數排序,這時只有藉助於"比較"的方法來排序。
若n很大,記錄的關鍵字位數較少且可以分解時,採用基數排序較好。雖然桶排序對關鍵字的結構無要求,但它也只有在關鍵字是隨機分佈時才能使平均時間達到線性階,否則爲平方階。同時要注意,箱、桶、基數這三種分配排序均假定了關鍵字若爲數字時,則其值均是非負的,否則將其映射到箱(桶)號時,又要增加相應的時間。
(5)有的語言(如Fortran,Cobol或Basic等)沒有提供指針及遞歸,導致實現歸併、快速(它們用遞歸實現較簡單)和基數(使用了指針)等排序算法變得複雜。此時可考慮用其它排序。
(6)本章給出的排序算法,輸人數據均是存儲在一個向量中。當記錄的規模較大時,爲避免耗費大量的時間去移動記錄,可以用鏈表作爲存儲結構。譬如插入排序、歸併排序、基數排序都易於在鏈表上實現,使之減少記錄的移動次數。但有的排序方法,如快速排序和堆排序,在鏈表上卻難於實現,在這種情況下,可以提取關鍵字建立索引表,然後對索引表進行排序。然而更爲簡單的方法是:引人一個整型向量t作爲輔助表,排序前令t[i]=i(0≤i<n),若排序算法中要求交換R[i]和R[j],則只需交換t[i]和t[j]即可;排序結束後,向量t就指示了記錄之間的順序關係:
R[t[0]].key≤R[t[1]].key≤…≤R[t[n-1]].key
若要求最終結果是:
R[0].key≤R[1].key≤…≤R[n-1].key
則可以在排序結束後,再按輔助表所規定的次序重排各記錄,完成這種重排的時間是O(n)。
五、各排序算法時間複雜度和空間複雜度
排序法 | 平均時間 | 最差情形 | 穩定度 | 額外空間 | 備註 |
冒泡 | O(n2) | O(n2) | 穩定 | O(1) | n小時較好 |
交換 | O(n2) | O(n2) | 不穩定 | O(1) | n小時較好 |
選擇 | O(n2) | O(n2) | 不穩定 | O(1) | n小時較好 |
插入 | O(n2) | O(n2) | 穩定 | O(1) | 大部分已排序時較好 |
基數 | O(logrd) | O(logrd) | 穩定 | O(n) | d是關鍵字項數(0-9), r是基數(個十百) |
Shell | O(nlogn) | O(ns) 1<s<2 | 不穩定 | O(1) | s是所選分組 |
快速 | O(nlogn) | O(n2) | 不穩定 | O(nlogn) | n大時較好 |
歸併 | O(nlogn) | O(nlogn) | 穩定 | O(1) | n大時較好 |
堆 | O(nlogn) | O(nlogn) | 不穩定 | O(1) | n大時較好 |