使用比較運算符""和"
",將相容的序放到輸入中,且除了賦值運算符外,這兩種運算是僅有的容許對輸入數據進行的操做,在這些條件下的排序叫作「
基於比較的排序
」。本文介紹的除了桶式排序
都是基於比較的排序。算法
最簡單的排序算法之一是插入排序。插入排序由 趟(pass)排序組成。對於
趟到
趟,插入排序保證從位置 0 到位置 P 上的元素爲已排序狀態。shell
void InsertionSort(ElementType A[], int N) { int j, P; ElementType Tmp; for (P = 1; P < N; P++) { Tmp = A[P]; for (j = P; j > 0 && A[j - 1] > Tmp; j--) A[j] = A[j - 1]; A[j] = Tmp; } } 複製代碼
運行時間上界爲
,去掉常係數項和低階項,時間複雜度爲
。編程
逆序(inversion)數組
成員存數的數組的一個逆序是指數組中具備性質但
的序偶
。例如數組
A=[34, 8, 64, 51, 32, 21]
有9個逆序,即(34, 8)
, (34, 32)
, (34, 21)
, (64, 51)
, (64, 32)
, (64, 21)
, (51, 32)
, (51, 21)
以及(32, 21)
。這正好是由插入排序執行的交換次數。狀況老是這樣,由於交換兩個不按原序排列的相鄰元素剛好消除一個逆序,而一個排過序的數組沒有逆序。markdown
假設不存在重複元素,輸入數據是前個整數的某個排列,並設全部的排列都是等可能的。有以下定理:數據結構
定理1ide
個互異數的數組的平均序數是
。oop
定理2性能
經過交換相鄰元素進行排序的任何算法平均須要時間。優化
希爾排序的名稱來源於它的發明者Donald Shell
。該算法是衝破二次時間屏障的第一批算法之一,不過,自從它最初被發現,又過了若干年後才證實了它的亞二次時間界。它經過比較相距必定間隔的元素來工做;各趟比較所用的距離隨着算法的進行而減少,直到只比較相鄰元素的最後一趟排序爲止。因爲這個緣由,希爾排序有時也叫縮小增量排序
(diminishing increment sort)。
希爾排序使用一個序列,叫作
增量序列
(increment sequence)。只要,任何增量序列都是可行的,不過有些增量序列比另一些增量序列更好。
對於排序的通常作法是,對於
中的每個位置
,把其上的元素放到
中間的正確位置上。
增量序列的一種流行(可是很差)的選擇是使用Shell
建議的序列:和
。以下爲使用這種增量序列的實現代碼實現。
void Shellsort(ElementType A[], int N) { int i, j, Increment; ElementType Tmp; for (Increment = N/2; Increment > 0; Increment /=2) for (i = Increment; i < N; i++) { Tmp = A[i]; for(j = i; j > Increment; j -= Increment) if(Tmp < A[j - Increment]) A[j] = A[j - Increment]; else break; A[j] = Tmp; } } 複製代碼
咱們知道,優先隊列(堆)能夠用於花費時間的排序。基於該想法的算法叫作
堆排序(heapsort)
並給出咱們至今所見到的最佳的大運行時間。然而,在實踐中它卻慢於使用
增量序列的希爾排序。
創建個元素的二叉堆花費
時間,而後執行
次
DeleteMin
操做。按照順序,最小的元素先離開堆。經過將這些元素記錄到第二個數組而後再將數組拷貝回來,咱們獲得個元素的排序。因爲每一個
花費時間
,所以總的運行時間是
。使用這樣的策略,在最後一次
DeleteMin
後,該數組將以遞減的順序包含這些元素。若是想要這些元素排成更典型的遞增順序,那麼咱們能夠改變序的特性使得父親的關鍵字的值大於兒子的關鍵字的值。這樣就獲得最大堆(max heap)
。
#define LeftChild(i) (2* (i) + 1) void PercDown(ElementType A[], int i, int N) { int Child; ElementType Tmp; for(Tmp = A[i]; LeftChild(i) < N; i = Child) { Child = LeftChild(i); if(Child != N-1 && A[Child + 1] > A[Child]) Child++; if(Tmp < A[Child]) A[i] = A[Child]; else break; } A[i] = Tmp; } void Heapsort(ElementType A[], int N) { int i; for(i = N/2; i >= 0; i--) /* BuildHeap */ PercDown(A, i, N); for(i = N-1; i > 0; i--) { Swap(&A[0], &A[i]); /* DeleteMax */ PercDown(A, 0, i); } } 複製代碼
能夠證實,堆排序老是使用至少次比較,並且存在輸入數據可以達到這個界。
歸併排序以最壞情形運行時間運行,而所使用的比較次數幾乎是最優的。它是遞歸算法一個很好的實例。
這個算法中基本的操做是合併兩個已排序的表。由於這兩個表是已經排序的。因此若將輸出放到第三個表中時則該算法能夠經過對輸入數據一趟排序來完成。基本的合併算法是取兩個輸入數組A
和B
,一個輸出數組C
,以及三個計數器Aptr
, Bptr
, Cptr
,它們初始置於對應數組的開始端。A[Aptr]
和B[Bptr]
中的較小者被拷貝到C
中的下一個位置,相關的計數器向前推動一步。當輸入表有一個用完的時候,則將另外一個表中剩餘部分拷貝到C
中。
合併兩個已排序的表的時間顯然是線性的,由於最多進行了次比較,其中
是元素的總數。爲了看清這一點,注意每次比較都是把一個元素加到
C
中,但最後的比較除外,它至少添加兩個元素。
/* Lpos = start of left half, Rpos = start of right half */ void Merge(ElementType A[], ElementType TmpArray[], int Lpos, int Rpos, int RightEnd) { int i, LeftEnd, NumElements, TmpPos; LeftEnd = Rpos - 1; TmpPos = Lpos; NumElements = RightEnd - Lpos + 1; /* main loop */ while(Lpos <= LefEnd && Rpos <= RightEnd) if (A[Lpos] <= A[Rpos]) TmpArray[TmpPos++] = A[Lpos++]; else TmpArray[TmpPos++] = A[Rpos++]; while(Lpos <= LeftEnd) TmpArray[TmpPos++] = A[Lpos++]; while(Rpos <= RightEnd) TmpArray[TmpPos++] = A[Rpos++]; /* copy TmpArray back */ for (i = 0; i < NumElements; i++, RightEnd--) A[RightEnd] = TmpArray[RightEnd]; } void MSort(ElementType A[], ElementType TmpArray[], int Left, int Right) { int Center; if (Left < Right) { Center = (Left + Right) / 2; MSort(A, TmpArray, Left, Center); Msort(A, TmpArray, Center + 1, Right); Merge(A, TmpArray, Left, Center + 1, Right); } } void Mergesort(ElementType A[], int N) { ElementType *TmpArray; TmpArray = malloc(N * sizeof(ElementType)); if (TmpArray != NULL) { MSort(A, TmpArray, 0, N-1); free(TmpArray); } else FatalError("No space for tmp array!!!"); } 複製代碼
正如它的名字所標示的,快速排序
是在實踐中最快的已知排序算法,它的平均運行時間是。該算法之因此特別快,主要是因爲很是精煉和高度優化的內部循環。它的最壞情形的性能爲
,但稍加努力就能夠避免這種情形。雖然多年來快速排序算法被認爲是理論上高度優化而在實踐中卻不可能正確編程的一種算法,可是現在該算法簡單易懂並且不難證實。像歸併排序同樣,快速排序也是一種分治的遞歸算法。
將數組排序的基本算法由下列簡單的四步組成:
因爲對那些等於樞紐元
的處理,第(3)步分割的描述不是惟一的,所以這就成了一個設計上的決策。一部分好的實現是將這種情形儘量有效地處理。直觀地看,咱們但願把等於樞紐元的大約一半的關鍵字分到中,而另外的一半分到
中,很像咱們但願二叉查找樹保持平衡同樣。
三數中值分割法(Median-of-Three Partitioning)。一組個數的中值是第
個最大的數。樞紐元最好的選擇是數組的
中值
。不幸的是,這很難算出,且明顯減慢快速排序的速度。這樣的中值的估計量能夠經過隨機選取三個元素並用它們的中值做爲樞紐元
獲得。事實上,隨機性並無多大的幫助,所以通常的作法是使用左端,右端和中心位置上的三個元素的中值
做爲樞紐元。例如,輸入爲 ,它的左邊元素是
,右邊元素是
,中心位置
上的元素是
。因而樞紐元則是
。
有幾種分割策略用於實踐,這裏介紹一種比較好的(但它依然很容易作錯或產生低效)。該方法:
樞紐元
而言的。樞紐元
的元素,並將樞紐元
的元素。樞紐元
與等於樞紐元的關鍵字處理:若是和
遇到等於樞紐元的關鍵字,那麼咱們就讓
和
都中止。對於這種輸入,這其實是不花費二次時間的四種可能性中唯一的一種可能。
對於很小的數組(),快速排序不如插入排序好。不只如此,由於快速排序是遞歸的,因此這樣的情形還常常發生。一般的解決方法是對於小的數組不遞歸地使用快速排序,而代之以諸如插入排序這樣的對小數組有效的排序算法。使用這樣的策略實際上能夠節省大約
(相對於自始至終使用快速排序)的運行時間。
void Quicksort(ElementType A[], int N) { Qsort(A, O, N - 1); } ElementType Median3(ElementType A[], int Left, int Right) { int Center = (Left + Right) / 2; if(A[Left] > A[Center]) Swap(&A[Left], &A[Center]); if(A[Left] > A[Right]) Swap(&A[Left], &A[Right]); if(A[Center] > A[Right]) Swap(&A[Center], &A[Right]); /* Invariant: A[Left] <= A[Center] <= A[Right] */ Swap(&A[Center], &A[Right - 1]); /* Hide pivot */ return A[Right - 1]; /* Return pivot */ } #define Cutoff (3) void Qsort(ElementType A[], int Left, int Right) { int i, j; ElementType Pivot; if (Left + Cutoff <= Right) { Pivot = Median3(A, Left, Right); i = Left; j = Right - 1; for (;;) { while(A[++i] < Pivot) {} while(A[--j] > Pivot) {} if (i < j) Swap(&A[i]), &A[j]; else break; } Swap(&A[i], &A[Right - 1]); /* Restore pivot */ Qsort(A, Left, i - 1); Qsort(A, i + 1, Right); } else /* Do an insertion sort on the subarray */ InsertionSort(A + Left, Right - Left + 1); } 複製代碼
目前爲止,關於排序的所有討論,都假設要被排序的元素是一些簡單的整數。實際應用中,經常須要某個關鍵字對大型結構進行排序。例如,咱們可能有些工資名單的記錄,每一個記錄由姓名,地址,電話號碼,諸如工資這樣的財務信息,以及稅務信息組成。咱們可能要經過一個特定的域,好比姓名,來對這些信息進行排序。對於全部算法來講,基本的操做就是交換,不過這裏交換兩個結構多是很是昂貴的操做,由於結構實際上很大。這種狀況下,實際的解法是讓輸入數組包含指向結構的指針。咱們經過比較指針指向的關鍵字,並在必要時交換指針來進行排序。這意味着,全部的數據移動基本上就像咱們對整數排序那樣進行。咱們稱之爲間接排序(indirect sorting))
;可使用這種方法處理咱們已經描述過的大部分數據結構。
雖然咱們獲得一些的排序算法,可是,尚不清楚咱們是否還能作得更好。能夠證實,任何只用到比較的算法在最壞的狀況下須要
次比較(從而
的時間),所以歸併排序和堆排序在一個常數因子範圍內是最優的。
決策樹
決策樹(decision tree)
是用於證實下界的抽象概念。它是一棵二叉樹
,每一個節點表示在元素之間一組可能的排序,它與已經進行的比較一致。比較的結果是樹的邊。經過只使用比較進行排序的每一種算法均可以用決策樹表示。固然,只有輸入數據很是少的狀況下畫決策樹纔是可行的。由排序算法所使用的比較次數
等於最深的樹葉的深度
。
雖然任何只使用比較的通常排序算法在最壞的狀況下須要運行時間,可是咱們要記住,在某些特殊狀況下以線性時間進行排序仍然是可能的。
一個簡單的例子就是桶式排序(bucket sort)
。爲使桶式排序可以正常工做,必需要知足一些額外的條件。輸入數據必須只由小於
的正整數組成。這種狀況下,排序算法就很簡單:使用一個大小爲
稱爲
Count
的數組,它被初始化爲全0
。因而,Count
有個單元(或稱桶),這些桶初始化爲空。當讀
時,
增1。在全部的輸入數據讀入後,掃描數組
Count
,打印出排序後的表。該算法用時。若是
爲
,那麼總量就是
。
儘管桶式排序看似太平凡用處不大,可是實際上卻存在許多輸入只是一些小的整數的狀況,使用像快速排序
這樣的排序方法真的是小題大做了。
目前爲止,咱們講到過的全部算法都須要將輸入數據裝入內存。而後,存在一些應用程序,它們的輸入數據量太大裝不進內存。外部排序(external sorting)
就是設計用來處理這樣很大的輸入數據的。
大部份內存排序算法都用到內存可直接尋址的事實。希爾排序
用一個時間單位比較元素和
;
堆排序
用一個時間單位比較元素和
。使用
三數中值分割法
的快速排序
以常數個時間單位比較,
和
。若是輸入數據在磁盤上,那麼全部這些操做就失去了它們的效率,由於
磁盤
上的元素只能被順序訪問。即便數據在一張磁盤上,因爲轉動磁盤和移動磁盤所需的延遲,仍然存在實際上的效率損失。
假設至少有三個磁盤驅動器進行排序工做。咱們須要兩個驅動器執行有效的排序,而第三個驅動器進行簡化的工做。若是隻有一個磁盤驅動器可用,那麼咱們則不得不說:任何算法都將須要次磁盤訪問。
基本的外部排序
算法使用歸併排序
中的Merge
子程序。假設咱們有四個磁盤,,它們是兩個輸入磁盤和兩個輸出磁盤。設數據最初在
上,並設內存能夠一次容納(和排序)
個記錄。一種天然的作法是第一步從輸入磁盤一次讀入
個記錄,在內部(內存)將這些記錄排序,而後再把這些排過序的記錄交替地寫到
或
上。咱們將把每組排過序的記錄叫作一個
順串(run)
。作完這些以後,咱們倒回全部的磁盤。假設咱們的輸入數據以下:
磁盤 | 數據 |
---|---|
![]() |
![]() |
![]() |
|
![]() |
|
![]() |
若是,那麼在順串構造之後,磁盤將包含以下所示的數據。
磁盤 | 順串 | 順串 | 順串 |
---|---|---|---|
![]() |
|||
![]() |
|||
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
如今和
都包含一組順串。咱們將每一個磁盤的第一個順串取出並將兩者合併,把結果寫到
上,該結果是一個二倍長的
順串
。而後,咱們再從每一個磁盤取出下一個順串合併,並將結果寫到上。繼續這個過程,交替使用
和
,直到
或
爲空。此時,或者
和
均爲空,或者剩下一個順串。對於後者,咱們把剩下的順串拷貝到適當的順串上。將四個磁盤倒回,並重復相同步驟,這一次用兩個
磁盤做輸入,兩個
磁盤做輸出,結果獲得一些
的順串。咱們繼續這個過程直到獲得長爲
的一個
順串
。
該算法將須要趟工做,外加一趟構造初始化的順串。例如,若咱們有
萬個記錄,每一個記錄
個字節,並有
兆字節的內存,則第一趟將創建
個順串。此時咱們再須要
趟以完成排序。上面的例子則是
趟。
第一趟:
磁盤 | 順串 | 順串 |
---|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
|
![]() |
||
![]() |
第二趟:
磁盤 | 順串 |
---|---|
![]() |
|
![]() |
|
![]() |
![]() |
![]() |
![]() |
第三趟:
磁盤 | 順串 |
---|---|
![]() |
![]() |
![]() |
|
![]() |
|
![]() |
對於最通常的內部(內存)排序
應用程序,選用的方法不是插入排序
,希爾排序
,就是快速排序
,它們的選用主要是根據輸入的大小來決定。下表顯示在一臺相對較慢的計算機上處理各類不一樣大小的文件時的運行時間(單位:秒)。
N | 插入排序![]() |
希爾排序![]() |
堆排序![]() |
快速排序![]() |
優化快速排序![]() |
---|---|---|---|---|---|
10 | 0.00044 | 0.00041 | 0.00057 | 0.00052 | 0.00046 |
100 | 0.00675 | 0.00171 | 0.00420 | 0.00284 | 0.00244 |
1000 | 0.59564 | 0.02927 | 0.05565 | 0.03153 | 0.02587 |
10000 | 58.864 | 0.42998 | 0.71650 | 0.36765 | 0.31532 |
100000 | NA | 5.7298 | 8.8591 | 4.2298 | 3.5882 |
1000000 | NA | 71.164 | 104.68 | 47.065 | 41.282 |
這裏沒有包括歸併排序
,由於它的性能對於在主存(內存)排序
不如快速排序
那麼好,並且它的編程一點也不省事。然而歸併(合併)
倒是外部排序
的中心思想。