今天花了點時間把七個常見的內部排序重新復習了一遍,總結一下,也算是驗證一下自己有沒有真正理解。
冒泡排序(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排序和類庫中那個實現有多大的差距:
還是有差距,但比起上次那秒殺級的差距這個結果絕對可以接受了。要知道,類庫個的那個算法可以用了“老長老長”的代碼~~~
還有三個比較麻煩的算法。一次是講不完的了。
先總結一下個人的一點體會:
對於排序而言,提高速度的方法明顯的有兩個,一個是減少數據的比較次數,一個是減少交換次數。
對於冒泡來說,它這兩個方法都是最差的。
而選擇排序明顯就減少了交換的次數。
而直接插入排序顯然在比較次數上要比選擇要少,因為我們是從右至左找到合適的位置就停止。
而希爾排序相對於直接插入排序在數據交換次數上,要少得多。另外就是很好的利用了“基本有序”這個性質。在比較次數上也會少很多。
本身菜鳥一個,這些都是個人的總結,認識不足、甚至錯誤在所難免。希望各位指出。