排序算法性能比較

所謂排序,即將原來無序的一個序列從新排列成有序的序列。css

排序方法中涉及到穩定性,所謂穩定性,是指待排序的序列中有兩個或兩個以上相同的項,在排序前和排序後看這些相同項的相對位置有沒有發生變化,若是沒有發生變化,即該排序方法是穩定的,若是發生變化,則說明該排序方法是不穩定的。html

若是記錄中關鍵字不能重複,則排序結果是惟一的,那麼選擇的排序方法穩定與否就可有可無了;若是關鍵字能夠重複,則在選擇排序方法時,就要根據具體的需求來考慮選擇穩定仍是不穩定的排序方法。那麼,哪些排序算法是不穩定的呢?算法

「快些選堆」:其中「快」指快速排序,「些」指希爾排序,「選」指選擇排序,「堆」指堆排序,即這四種排序方法是不穩定的,其餘天然都是穩定的。shell

排序算法分類

一、插入類排序數據結構

即在一個已經有序的序列中,插入一個新的記錄,就比如軍訓排隊,已經排好一個縱隊,這時來了個新傢伙,因而新來的「插入」這個隊伍中的合適位置。這類排序有:直接插入排序、折半插入排序、希爾排序。函數

二、交換類排序性能

該類方法的核心是「交換」,即每趟排序,都是經過一系列的「交換」動做完成的,如軍訓排隊時,教官說:你比旁邊的高,你倆交換下,還比下一個高就繼續交換。這類排序有:冒泡排序、快速排序。ui

三、選擇類排序this

該方法的核心是「選擇」,即每趟排序都選出一個最小(或最大)的記錄,把它和序列中的第一個(或最後一個)記錄交換,這樣最小(或最大)的記錄到位。如軍訓排隊時,教官選出個子最小的同窗,讓他和第一個位置的同窗交換,剩下的繼續選擇。這類排序有:選擇排序、堆排序。spa

四、歸併類排序

所謂歸併,就是將兩個或兩個以上的有序序列合併成一個新的有序序列。如軍訓排隊時,教官說:每一個人先和旁邊的人組成二人組,組內排好隊,二人組和旁邊的二人組組成四人組,內部再排好隊,以此類推,直到最後所有同窗都歸併到一個組中並排好序。這類排序有:(二路)歸併排序。

五、基數類排序

此類方法較爲特別,是基於多關鍵字排序的思想,把一個邏輯關鍵字拆分紅多個關鍵字,如一副撲克牌,按照基數排序思想能夠先按花色排序,則分紅4堆,每堆再按A-K的順序排序,使得整副撲克牌最終有序。

排序算法分析

本文主要分析的排序算法有:冒泡排序、選擇排序、插入排序、希爾排序、快速排序、歸併排序、堆排序。

交換算法

因爲大部分排序算法中使用到兩個記錄相互交換的動做,所以將交換動做單獨封裝出來,便於各排序算法使用。

1 //交換函數
2 Array.prototype.swap = function(i, j) {  
3     var temp = this[i];  
4     this[i] = this[j];  
5     this[j] = temp;  
6 }  

插入排序

算法思想:每趟將一個待排序的關鍵字,按照其關鍵字值的大小插入到已經排好的部分序列的適當位置上,直到插入完成。

 1 //插入排序
 2 Array.prototype.insertionSort = function() {  
 3     for (var i = 1; i < this.length; ++i)  
 4     {  
 5         var j = i, 
 6             value = this[i];  
 7         while (j > 0 && this[j - 1] > value)  
 8         {  
 9             this[j] = this[j - 1];  
10             --j;  
11         }  
12         this[j] = value;  
13     }  
14 }  

算法性能:在內層循環中this[j]=this[j-1],這句是做爲基本操做。考慮最壞狀況,即整個序列是逆序的,則其基本操做總的執行次數爲n*(n-1)/2,其時間複雜度爲O(n*n)。考慮最好狀況,即整個序列已經有序,則循環內的操做均爲常量級,其時間複雜度爲O(n)。所以本算法平均時間複雜度爲O(n*n)。算法所需的額外空間只有一個value,所以空間複雜度爲O(1)。

希爾排序

算法思想:希爾排序又叫作縮小增量排序,是將待排序的序列按某種規則分紅幾個子序列,分別對這幾個子序列進行插入排序,其中這一規則就是增量。如可使用增量五、三、1來分格序列,且每一趟希爾排序的增量都是逐漸縮小的,希爾排序的每趟排序都會使得整個序列變得更加有序,等整個序列基本有序了,再使用一趟插入排序,這樣會更有效率,這就是希爾排序的思想。

 1 //希爾排序
 2 Array.prototype.shellSort = function() {  
 3     for (var step = this.length >> 1; step > 0; step >>= 1)  
 4     {  
 5         for (var i = 0; i < step; ++i)  
 6         {  
 7             for (var j = i + step; j < this.length; j += step)  
 8             {  
 9                 var k = j, value = this[j];  
10                 while (k >= step && this[k - step] > value)  
11                 {  
12                     this[k] = this[k - step];  
13                     k -= step;  
14                 }  
15                 this[k] = value;  
16             }  
17         }  
18     }  
19 }  

算法性能:希爾排序的時間複雜度平均狀況爲O(nlogn),空間複雜度爲O(1)。希爾排序的增量取法要注意,首先增量序列的最後一個值必定是1,其次增量序列中的值沒有除1以外的公因子,如8,4,2,1這樣的序列就不要取(有公因子2)。

冒泡排序

算法思想:經過一系列的「交換」動做完成的,首先第一個記錄與第二個記錄比較,若是第一個大,則兩者交換,不然不交換;而後第二個記錄和第三個記錄比較,若是第二個大,則兩者交換,不然不交換,以此類推,最終最大的那個記錄被交換到了最後,一趟冒泡排序完成。在這個過程當中,大的記錄就像一塊石頭同樣沉底,小的記錄逐漸向上浮動。冒泡排序算法結束的條件是一趟排序沒有發生元素交換。

1 //冒泡排序
2 Array.prototype.bubbleSort = function() {  
3     for (var i = this.length - 1; i > 0; --i)  
4     {  
5         for (var j = 0; j < i; ++j)  
6             if (this[j] > this[j + 1]) 
7                 this.swap(j, j + 1);  
8     }  
9 }  

算法性能:最內層循環的元素交換操做是算法的基本操做。最壞狀況,待排序列逆序,則基本操做的總執行次數爲(n-1+1)*(n-1)/2=n(n-1)/2,其時間複雜度爲O(n*n);最好狀況,待排序列有序,則時間複雜度爲O(n),所以平均狀況下的時間複雜度爲O(n*n)。算法的額外輔助空間只有一個用於交換的temp,因此空間複雜度爲O(1)。

快速排序

算法思想:以軍訓排隊爲例,教官說以第一個同窗爲中心,比他矮的站他左邊,比他高的站他右邊,這就是一趟快速排序。所以,一趟快速排序是以一個樞軸,將序列分紅兩部分,樞軸的一邊比它小(或小於等於),另外一邊比它大(或大於等於)。

 1 //遞歸快速排序
 2 Array.prototype.quickSort = function(s, e) {  
 3     if (s == null) 
 4         s = 0;  
 5     if (e == null) 
 6         e = this.length - 1;  
 7     if (s >= e) 
 8         return;  
 9     this.swap((s + e) >> 1, e);  
10     var index = s - 1;  
11     for (var i = s; i <= e; ++i)   
12         if (this[i] <= this[e]) this.swap(i, ++index);  
13     this.quickSort(s, index - 1);  
14     this.quickSort(index + 1, e);  
15 }  

算法性能:快速排序最好狀況下時間複雜度爲O(nlogn),待排序列越接近無序,則該算法效率越高,在最壞狀況下時間複雜度爲O(n*n),待排序列越接近有序,則該算法效率越低,算法的平均時間複雜度爲O(nlogn)。就平均時間而言,快速排序是全部排序算法中最好的。該算法的空間複雜度爲O(logn),快速排序是遞歸進行的,須要棧的輔助,所以須要的輔助空間比前面幾類排序方法要多。

快速排序的效率和選取的「樞軸」有關,選取的樞軸越接近中間值,算法效率就越高,所以爲了提升算法效率,能夠在第一次選取「樞軸」時作文章,如在數據堆中隨機選取3個值,取3個值的平均值做爲「樞軸」,就如抽樣通常。關於具體如何提升快速排序算法的效率,在本文不作詳細介紹了,點到爲止。(感興趣的讀者能夠自行去研究)

選擇排序

算法思想:該算法的主要動做就是「選擇」,採用簡單的選擇方式,從頭到尾順序掃描序列,找出最小的一個記錄,和第一個記錄交換,接着從剩下的記錄中繼續這種選擇和交換,最終使序列有序。

 1 //選擇排序
 2 Array.prototype.selectionSort = function() {  
 3     for (var i = 0; i < this.length; ++i)  
 4     {  
 5         var index = i;  
 6         for (var j = i + 1; j < this.length; ++j)  
 7         {  
 8             if (this[j] < this[index]) 
 9                 index = j;  
10         }  
11         this.swap(i, index);  
12     }  
13 }  

算法性能:將最內層循環中的比較視爲基本操做,其執行次數爲(n-1+1)*(n-1)/2=n(n-1)/2,其時間複雜度爲O(n*n),本算法的額外空間只有一個temp,所以空間複雜度爲O(1)。

堆排序

算法思想:堆是一種數據結構,最好的理解堆的方式就是把堆當作一棵徹底二叉樹,這個徹底二叉樹知足任何一個非葉節點的值,都不大於(或不小於)其左右孩子節點的值。若父親大孩子小,則這樣的堆叫作大頂堆;若父親小孩子大,這樣的堆叫作小頂堆。根據堆的定義,其根節點的值是最大(或最小),所以將一個無序序列調整爲一個堆,就能夠找出這個序列的最大(或最小)值,而後將找出的這個值交換到序列的最後(或最前),這樣有序序列元素增長1個,無序序列中元素減小1個,對新的無序序列重複這樣的操做,就實現了序列排序。堆排序中最關鍵的操做是將序列調整爲堆,整個排序的過程就是經過不斷調整使得不符合堆定義的徹底二叉樹變爲符合堆定義的徹底二叉樹的過程。

堆排序執行過程(大頂堆):

(1)從無序序列所肯定的徹底二叉樹的第一個非葉子節點開始,從右至左,從下至上,對每一個節點進行調整,最終將獲得一個大頂堆。將當前節點(a)的值與其孩子節點進行比較,若是存在大於a值的孩子節點,則從中選出最大的一個與a交換。當a來到下一層的時候重複上述過程,直到a的孩子節點值都小於a的值爲止。

(2)將當前無序序列中第一個元素,在樹中是根節點(a)與無序序列中最後一個元素(b)交換。a進入有序序列,到達最終位置,無序序列中元素減小1個,有序序列中元素增長1個,此時只有節點b可能不知足堆的定義,對其進行調整。

(3)重複過程2,直到無序序列中的元素剩下1個時排序結束。

 1 //堆排序
 2 Array.prototype.heapSort = function() {  
 3     for (var i = 1; i < this.length; ++i)  
 4         {  
 5         for (var j = i, k = (j - 1) >> 1; k >= 0; j = k, k = (k - 1) >> 1)  
 6         {  
 7             if (this[k] >= this[j]) 
 8                 break;  
 9             this.swap(j, k);  
10         }  
11     }  
12     for (var i = this.length - 1; i > 0; --i)  
13     {  
14         this.swap(0, i);  
15         for (var j = 0, k = (j + 1) << 1; k <= i; j = k, k = (k + 1) << 1)  
16         {  
17             if (k == i || this[k] < this[k - 1]) 
18                 --k;  
19             if (this[k] <= this[j]) 
20                 break;  
21             this.swap(j, k);  
22         }  
23     }  
24 }  

算法性能:徹底二叉樹的高度爲[log(n+1)],即對每一個節點調整的時間複雜度爲O(logn),基本操做總次數是兩個並列循環中基本操做次數相加,則整個算法時間複雜度爲O(logn)*n/2+O(logn)*(n-1),即O(nlogn)。額外空間只有一個temp,所以空間複雜度爲O(1)。

堆排序的優勢是適合記錄數不少的場景,如從1000000個記錄中選出前10個最小的,這種狀況用堆排序最好,若是記錄數較少,則不提倡使用堆排序。另外,Hash表+堆排序是處理海量數據的絕佳組合,關於海量數據處理會在以後的博文中介紹到。

歸併排序

算法思想:其核心就是「兩兩歸併」,首先將原始序列當作每一個只含有單獨1個元素的子序列,兩兩歸併,造成若干有序二元組,則第一趟歸併排序結束,再將這個序列當作若干個二元組子序列,繼續兩兩歸併,造成若干有序四元組,則第二趟歸併排序結束,以此類推,最後只有兩個子序列,再進行一次歸併,即完成整個歸併排序。

 1 //歸併排序
 2 Array.prototype.mergeSort = function(s, e, b) {  
 3     if (s == null) 
 4         s = 0;  
 5     if (e == null) 
 6         e = this.length - 1;  
 7     if (b == null) 
 8         b = new Array(this.length);  
 9     if (s >= e) 
10         return;  
11     var m = (s + e) >> 1;  
12     this.mergeSort(s, m, b);  
13     this.mergeSort(m + 1, e, b);  
14     for (var i = s, j = s, k = m + 1; i <= e; ++i)   
15         b[i] = this[(k > e || j <= m && this[j] < this[k]) ? j++ : k++];  
16     for (var i = s; i <= e; ++i) 
17         this[i] = b[i];  
18 }  

算法性能:能夠選取「歸併操做」做爲基本操做,「歸併操做」即爲將待歸併表中元素複製到一個存儲歸併結果的表中的過程,其次數爲要歸併的兩個子序列中元素個數之和。算法總共須要進行logn趟排序,每趟排序執行n次基本操做,所以整個歸併排序中總的基本操做執行次數爲nlogn,即時間複雜度爲O(nlogn),說明歸併排序時間複雜度和初始序列無關。因爲歸併排序須要轉存整個待排序列,所以空間複雜度爲O(n)。

一些結論

(1)快速排序、希爾排序、歸併排序、堆排序的平均時間爲O(nlogn),其餘的爲O(n*n)。

(2)快速排序、希爾排序、選擇排序、堆排序不穩定,其餘的穩定。

(3)通過一趟排序可以保證一個元素到達最終位置的是冒泡排序、快速排序、選擇排序、堆排序。

(4)元素比較次數和原始序列無關的是選擇排序、折半插入排序。

(5)排序趟數和原始序列有關的是交換類排序。

(6)直接插入排序和折半插入排序的區別是尋找插入位置的方式不一樣,一個是按順序查找方式,另外一個是按折半查找方式。

效果展現

相關文章
相關標籤/搜索