七大排序演算法的個人總結(一)
氣泡排序(Bubble Sort):
很多人聽到排序第一個想到的應該就是氣泡排序了。也確實,氣泡排序的想法非常的簡單:大的東西沉底,汽泡上升。基於這種思想,我們可以獲得第一個版本的冒泡:
public static void sort1(int[] array) { for (int i = 0; i < array.length; i++) { // i為確定了幾個數 for (int j = 1; j < array.length - i; j++) { if (array[j - 1] > array[j]) { // 進行兩元素之間的位置交換 int temp = array[j - 1]; array[j - 1] = array[j]; array[j] = temp; } } } }
再想一想,其實有這樣一種情況:如果在某一個遍歷的過程中,沒有發生資料交換,那其實說明了這個陣列已經是有序的了:所以我們可以作一點小小的優化(雖然不經常有用):
// 升級版1 // 基於一個事實,如果某一次遍歷沒有發生資料交換,那麼排序已經完成 public static void sort2(int[] array) { boolean complete = false; for (int i = 0; i < array.length && !complete; i++) { complete = true; // 假設已經完成了 for (int j = 1; j < array.length - i; j++) { if (array[j - 1] > array[j]) { complete = false; int temp = array[j - 1]; array[j - 1] = array[j]; array[j] = temp; } } } }
這樣在應對某些比較特殊的情況下,會有一定的效果。
再來想想這樣一個事實:最後產生交換的位置之後的元素是有序的。想像一下,如果一個數組只是前半部分的元素是無序的,那麼我們實際上只需要遍歷到無序的位置即可,其實我們上前面的演算法中array.length – i這一步已經是做了類似的工作,因為我們知道後面已經有i個元素是有序的了。所以我們可以得到第三個版本的冒泡:
// 升級版2 // 基於這樣一個事實,如果最後的資料交換位置之後的元素是有序的 // 所以,這個也是基於版本1的再一次加強 public static void sort3(int[] array) { int flag = array.length; // 用於標識元素的最後的位置 while (flag != 0) { // 為0說明沒有發生資料的交換 int last = flag; flag = 0; for (int j = 1; j < last; j++) { if (array[j - 1] > array[j]) { flag = j; int temp = array[j - 1]; array[j - 1] = array[j]; array[j] = temp; } } } }
選擇排序(Selection Sort):
選擇排序也是比較好理解的,每次從左到右掃描一次,可以得到最大的(或最小的)元素的下標,然後我們再把它與陣列末尾(或者開頭)的元素進行交換,這樣每一次都可以找到一個最大的。實現起來也是很簡單的:
// 每次從中選出最小的元素,只進行一次交換 // 相比冒泡,大大減少了元素的交換次數 public static void sort(int[] array) { for (int i = 0; i < array.length; i++) { // 確定了多少個元素 int min = i; // 每次都預設第一個是最小的 for (int j = i + 1; j < array.length; j++) { if (array[min] > array[j]) { min = j; } } int temp = array[min]; array[min] = array[i]; array[i] = temp; } }
直接插入排序(Insertion Sort):
直接插入排序的思路有點類似於我們平時打牌時整理時的方法,比如我整理牌的方式是,右邊選擇,然後插入到左邊已經整理好的牌中。
直接插入排序也是這樣:將要排序的元素分為有序區和無序區。每次從無序區拿出一個元素,然後在有序區找到自己的位置,強勢插入。
public static void sort(int[] array) { for (int i = 1; i < array.length; i++) { // 預設第一個是有序的 int temp = array[i]; // 拿出要插入的資料 int j = i; // 尋找插入位置 while (j > 0 && temp < array[j - 1]) { array[j] = array[j - 1]; j--; } array[j] = temp; } }
對於直接插入排序來說,經常用到一個“優化”就是使用陣列的第0個元素來放置要插入的資料,這樣做有一個好處就是不用每次都去檢查j指標是否小於0。理論上可以節省點時間。
另一種優化就是可以在查詢插入位置的時候可以通過二分查詢來實現,也有一定的作用。
接下來看一下這三個演算法的PK情況,為了加強對比我們找到了Java類庫中的Arrays.sort()這個方法來參與PK,測試資料是50000個0到500000的整數。使用的是System.currentTimeMillis()這個方法來計時:
某幾次結果如下:
效能差別如此之大!顯然,這三個排序演算法都“弱暴了”。
接下來來看看今天的第一個高階一點的演算法,也就是傳說中第一批被證明是突破了N的平方執行時間的排序演算法。
希爾排序(Shell Sort):
先來看看具體的程式:
public static void sort(int[] array) { for (int step = array.length / 2; step > 0; step /= 2) { for (int i = step; i < array.length; i++) { int temp = array[i]; int j = i; while (j >= step && temp < array[j - step]) { array[j] = array[j - step]; j -= step; } array[j] = temp; } } }
這~~~看起來是如此簡單的程式碼。很難想像它有什麼牛X之處。我還記得當時這個演算法真是把我給糾結了很久,從程式碼上看,它有兩個for迴圈巢狀,裡面還有一個while迴圈。看起來時間複雜度很像是N的三次方吧。
再有,當step為1的時候,看看,是不是和直接插入排序的程式碼是一模一樣的。那這個演算法怎麼可能會快啊!
希爾排序有時被叫做縮減增量排序(diminishing increment sort),使用一個序列h1,h2,h3……這樣一個增量序列。只要h1=1時,任何增量序列都是可以的。但有些可能更好。對於希爾排序為什麼會比直接插入排序快的原因,我們可以來看一個比較極端的例子:
假如對於一個數組{8,7,6,5,4,3,2,1}以從小到大的順序來排。直接插入排序顯然是很悲劇的了。
它的每次排序結果是這樣的:
7, 8, 6, 5, 4, 3, 2, 1
6, 7, 8, 5, 4, 3, 2, 1
5, 6, 7, 8, 4, 3, 2, 1
4, 5, 6, 7, 8, 3, 2, 1
3, 4, 5, 6, 7, 8, 2, 1
2, 3, 4, 5, 6, 7, 8, 1
1, 2, 3, 4, 5, 6, 7, 8
然後我們來看看Shell排序會怎樣處理,一開始步長為4
陣列分為8, 7, 6, 5和4, 3, 2, 1
首先是7和4進行比較,交換位置。
變成了4, 7, 6, 5和8, 3, 2, 1
同理7和3,6和2,5和1也是樣的,所以當步長為4時的結果是:
4, 3, 2, 1, 8, 7, 6, 5
可以看到,大的數都在後邊了。
接下來的步長為2
這一步過程就多了很多:
一開始是4和2進行比較,交換,得到:
2, 3, 4, 1, 8, 7, 6, 5
3和1比較,交換,得到:
2, 1, 4, 3, 8, 7, 6, 5
接下來是4和8,3和7,這兩個比較沒有元素交換。接下來8和6,7和5就需要交換了。所以步長為2時的結果就是:
2, 1, 4, 3, 6, 5, 8, 7
可以明顯地感覺到,陣列變得“基本有序”了。
接下來的步長1,變成了直接插入排序。手動模擬一下就可以發現,元素的交換次數只有四次!這是相當可觀的。也由此我們可以得到一個基本的事實:對於基本有序的陣列,使用直接插入排序的效率是很高的!
那回到我們一開始的問題,希爾排序為什麼會快?
首先說明一下,我上邊的例子是極端的,不能作為正常情況來看的。但我們可以看出一點端倪:
希爾排序對元素的移動效率比直接排序要高;比如我們看第一個步長4時,直接就把4,3,2,1這四個元素的位置向前移動了4位,比起直接插入排序的一次進一步要明顯高效得多。
其次,希爾每次都將資料變得“更加有序”;這一個性質相當重要,因為它利用了上一次的排序結果,在此之上讓資料向“更加有序”更進一步。
最後,是一個觀察的事實,就是對於“基本有序”的陣列而言,直接插入排序的效率是很高的,因為只需要交換少量的元素。
好的,我們再來看看我們寫的shell排序的效率怎樣:這一次是兩個重量級的選手,所以我們把資料量提高到500000,看看shell排序和類庫中那個實現有多大的差距:
還是有差距,但比起上次那秒殺級的差距這個結果絕對可以接受了。要知道,類庫個的那個演算法可以用了“老長老長”的程式碼~~~
還有三個比較麻煩的演算法。一次是講不完的了。
先總結一下個人的一點體會:
對於排序而言,提高速度的方法明顯的有兩個,一個是減少資料的比較次數,一個是減少交換次數。
對於冒泡來說,它這兩個方法都是最差的。
而選擇排序明顯就減少了交換的次數。
而直接插入排序顯然在比較次數上要比選擇要少,因為我們是從右至左找到合適的位置就停止。
而希爾排序相對於直接插入排序在資料交換次數上,要少得多。另外就是很好的利用了“基本有序”這個性質。在比較次數上也會少很多。