排序的基本概念(轉)
所謂排序,就是要整理檔案中的記錄,使之按關鍵字遞增(或遞減)次序排列起來。其確切定義如下:
輸入:n個記錄R1,R2,…,Rn,其相應的關鍵字分別為K1,K2,…,Kn。
輸出:Ril,Ri2,…,Rin,使得Ki1≤Ki2≤…≤Kin。(或Ki1≥Ki2≥…≥Kin)。
(1)排序的分類
1.按是否涉及資料的內、外存交換分
內部排序:內部排序(簡稱內排序),是帶排序紀錄存放在計算機記憶體中,並進行的排序過程。細分又可為:插入排序、選擇排序、歸併排序和基數排序;
外部排序:指的是帶排序紀錄的數量很大,以致記憶體一次不能容納全部紀錄,在排序過程中,只有部分數被調入記憶體,並藉助記憶體調整數在外存中的存放順序排序方法。
注意:
1)內排序適用於記錄個數不很多的小檔案
2)外排序則適用於記錄個數太多,不能一次將其全部記錄放人記憶體的大檔案。
2.按策略劃分內部排序方法
可以分為五類:插入排序、選擇排序、交換排序、歸併排序和分配排序。
(2)排序演算法的基本操作
大多數排序演算法都有兩個基本的操作:
(1)比較兩個關鍵字的大小;
(2)改變指向記錄的指標或移動記錄本身。
注意:第(2)種基本操作的實現依賴於待排序記錄的儲存方式。
(3)排序演算法效能評價
1.評價排序演算法好壞的標準
評價排序演算法好壞的標準主要有兩條:
1)執行時間和所需的輔助空間
2)演算法本身的複雜程度
2.
若排序演算法所需的輔助空間並不依賴於問題的規模n,即輔助空間是O(1),則稱之為就地排序(In-PlaceSou)。非就地排序一般要求的輔助空間為O(n)。
3.排序演算法的時間開銷
大多數排序演算法的時間開銷主要是關鍵字之間的比較和記錄的移動。有的排序演算法其執行時間不僅依賴於問題的規模,還取決於輸入例項中資料的狀態。
l插入排序
一)直接插入排序
定義:直接插入排序( straight insertion sort )是一種最簡單的排序方法。它的基本操作是將一個記錄插入到一個長度為 m (假設)的有序表中,使之仍保持有序,從而得到一個新的長度為m+1
演算法思路:設有一組關鍵字{ K 1 , K 2 ,…, K n };排序開始就認為 K 1 是一個有序序列;讓 K 2 插入上述表長為 1 的有序序列,使之成為一個表長為 2 的有序序列;然後讓 K 3 插入上述表長為 2 的有序序列,使之成為一個表長為 3 的有序序列;依次類推,最後讓 K n 插入上述表長為 n-1 的有序序列,得一個表長為 n 的有序序列。
演算法時間複雜度:此演算法外迴圈 n-1 次,在一般情況下內迴圈平均比較次數的數量級為O(n) ,所以演算法總時間複雜度為O(n2) 。
直接插入排序的穩定性:直接插入排序是穩定的排序方法
具體演算法:
/* 比較資料函式模板 */
template<class Type>
typedef bool __stdcall (*PFunCustomCompare)(const Type *Data_1,const Type *Data_2);
template<class Type>
void InsertSort (Type Array[], int n, PFunCustomCompare pfCompare){
int i,j;
for (i=2 ;i<=n; i++){ //共進行n-1趟插入
Array[0] = Array[i]; // Array[0]為監視哨,也可作下邊迴圈結束標誌
j = i-1;
while (pfCompare (Array[j], Array[0]){
Array[j+1] = Array[j];
j--;
}
Array [j+1]= Array [0]; //將r[0]即原r[i]記錄內容,插到r[j]後一位置
}
} // InsertSort
或者:不需要監視哨
template<class Type>
void __stdcall InsertSort(Type Array[], int Num, PFunCusomCompare pfCompare){
for (int i=1; i<Num; ++i){
Type temp = Array[i];
int j;
for (j=i-1; j>=0 && pfCompare (t, Array[j]); --j){
Array[j+1] = Array[j];
}
Array[j+1] = temp;
}
}
【例】設有一組關鍵字序列{55,22,44,11,33},這裡 n=5,即有5個記錄。請將其按由小到大的順序排序。排序過程如圖9.1所示。
第一趟:[55]22441133
第二趟:[2255] 441133
第三趟:[224455]1133
第四趟:[11224455]33
第五趟:[1122334455]
二)折半插入排序
定義:當直接插入排序進行到某一趟時,對於 r[i].key 來講,前邊 i-1 個記錄已經按關鍵字有序。此時不用直接插入排序的方法,而改為折半查詢,找出 r[i].key 應插的位置,然後插入。這種方法就是折半插入排序( Binary insertion sort )。
具體演算法:
template<class T>
void BinarySort(T r[],int n){
int i,j,l,h,mid;
for (i=2; i<=n; i++){
r[0]=r[i];
l=1;
h=i-1; //認為在r[1]和r[i-1]之間已經有序
while (l<=h) { //對有序表進行折半查詢
mid=(l+h)/2;
if(a[0].key<a[mid].key){
h=mid-1;
}else{
l=mid+1;
}
}
//結果在1位置
for(j=i-1;j>=1;j--){
a[j+1]=a[j];
}
a[1]=a[0];
}
} // BinarySort
折半插入排序的時間複雜度:折半插入排序,關鍵字的比較次數由於採用了折半查詢而減少,數量級為O (nlog 2 n) ,但是元素移動次數仍為O (n 2 ) 。故折半插入排序時間複雜度仍為O (n 2 ) 。折半插入排序方法是穩定的。
三)2-路插入排序
四)表插入排序
五)希爾排序
定義:希爾排序( shell sort )是 D .L.希爾( D.L.Shell )提出的“縮小增量”的排序方法。它的作法不是每次一個元素挨一個元素的比較。而是初期選用大跨步(增量較大)間隔比較,使記錄跳躍式接近它的排序位置;然後增量縮小;最後增量為 1 ,這樣記錄移動次數大大減少,提高了排序效率。希爾排序對增量序列的選擇沒有嚴格規定。
演算法思路:
①.先取一個正整數 d1(d 1 <;n) ,把全部記錄分成 d1個組,所有距離為 d1的倍數的記錄看成一組,然後在各組內進行插入排序;
②.然後取 d2( d2 < d1 ) 。
③.重複上述分組和排序操作;直到取 di=1(i>=1) ,即所有記錄成為一個組為止。一般選 d1約為 n/2,d2為 d 1 /2,d3為 d 2 /2,…,d i =1 。
具體演算法:
/* 比較資料函式模板 */
template<class Type>
typedef bool __stdcall (*PFunCustomCompare)(const Type *Data_1,const Type *Data_2);
template <class Type>
void __stdcall ShellSort(Type Array[], int Num, PFunCusomCompare pfCompare){
d = Num;
do{
d = d/2;//一般增量設定為陣列元素個數,不斷除以2以取小
for (int i=d+1; i<=Num; ++i){
if (pfCompare(Array[i], Array[i-d])){
Type temp = Array[i];
for (int j=i-d; j>0 && fpCompare(temp, Array[j]); j=j-d){
Array[j-d] = Array[j];
}
Array[j+d] = temp;
}
}
}while (d>1);
}
【例】有一組關鍵字{ 76 , 81 , 60 , 22 , 98 , 33 , 12 , 79 },將其按由小到大的順序排序。這裡 n=8 ,取 d 1 =4 , d 2 =2 , d 3 =1 。排序過程如圖9.2所示。
l交換排序
交換排序主要是根據記錄的關鍵字的大小,將記錄交換來進行排序的。交換排序的特點是:將關鍵字值較大的記錄向序列的後部移動,關鍵字較小的記錄向前移動。這裡介紹兩種交換排序方法,它們是氣泡排序和快速排序。
一)氣泡排序
將被排序的記錄陣列R[1..n]垂直排列,每個記錄R[i]看作是重量為R[i].key的氣泡。根據輕氣泡不能在重氣泡之下的原則,從下往上掃描陣列R:凡掃描到違反本原則的輕氣泡,就使其向上"飄浮"。如此反覆進行,直到最後任何兩個氣泡都是輕者在上,重者在下為止。
1.演算法思路
(1)讓j取n至2,將r[j].key與r[j-1].key比較,如果r[j].key<r[j-1].key,則把記錄r[j]與r[j-1]交換位置,否則不進行交換。最後是r[2].key與r[1].key對比,關鍵字較小的記錄就換到r[1]的位置上,到此第一趟結束。最小關鍵字的記錄就象最輕的氣泡冒到頂部一樣換到了檔案的前邊。
(2)讓j取n至3,重複上述的比較對換操作,最終r[2]之中存放的是剩餘n-1個記錄(r[1]除外)中關鍵字最小的記錄。
(3) 讓j取n至i+1,經過一系列對聯對比交換之後,r[i]之中是剩餘若干記錄中關鍵字最小的記錄。
(4) 讓j取n至n-1,將r[n].key與r[n-1].key對比,把關鍵字較小的記錄交換到r[n-1]之中。
【例】設有一組關鍵字序列{ 55 , 22 , 44 , 11 , 33 },這裡 n=5 ,即有 5 個記錄。請將其按由小到大的順序排序。如圖9.3
具體演算法
template<class Type>
BubbleSort(Type Array[], int n){
int t=1,tag,j;T x;
do{
tag=0;
for(j=n;j>=i;j--)
if(r[j].key<r[j-1].key){x=r[j];r[j]=r[j-1];
r[j-1]=x;tag=1;
}
i++;
}while(tag==1&&i<i<=n);
} // BubbleSort
演算法時間複雜度:該演算法的時間複雜度為O(n2)。但是,當原始關鍵字序列已有序時,只進行一趟比較就結束,此時時間複雜度為O(n)。
二)快速排序
快速排序由霍爾 (Hoare) 提出,它是一種對氣泡排序的改正。由於其排序速度快,故稱快速排序 (quick sort) 。快速排序方法的實質是將一組關鍵字 [K 1 ,K 2 ,…,K n ] 進行分割槽交換排序。
演算法思路
①以第一個關鍵字 K 1 為控制字,將 [K 1 ,K 2 ,…,K n ] 分成兩個子區,使左區所有關鍵字小於等於 K 1 ,右區所有關鍵字大於等於 K 1 ,最後控制字居兩個子區中間的適當位置。在子區內資料尚處於無序狀態。
②將右區首、尾指標 ( 記錄的下標號 ) 儲存入棧,對左區進行與第①步相類似的處理,又得到它的左子區和右子區,控制字居中。
③重複第①、②步,直到左區處理完畢。然後退棧對一個個右子區進行相類似的處理,直到棧空。
由以上三步可以看出:快速排序演算法總框架是進行多趟的分割槽處理;而對某一特定子區,則應把它看成又是一個待排序的檔案,控制字總是取子區中第一個記錄的關鍵字。現在設計一個函式 hoare ,它僅對某一待排序檔案進行左、右子區的劃分,使控制字居中;再設計一個主體框架函式 quicksort ,在這裡多次呼叫 hoare 函式以實現對整個檔案的排序。
快速排序演算法分析
快速排序的非遞迴演算法引用了輔助棧,它的深度為 log2n 。假設每一次分割槽處理所得的兩個子區長度相近,那麼可入棧的子區長度分別為:n/21n/22,n/23 ,n/24 , … ,n/2k ,又因為 n/2k=1, 所以 k= log2n 。分母中 2 的指數恰好反映出需要入棧的子區個數,它就是 log2n ,也即棧的深度。在最壞情況下,比如原檔案關鍵字已經有序,每次分割槽處理僅能得到一個子區。可入的子區個數接近 n, 此時棧的最大深度為 n.
快速排序主體演算法時間運算量約 O(log2n) ,劃分子區函式運算量約 O(n) ,所以總的時間複雜度為 O(nlog2n) ,它顯然優於氣泡排序 O(n2). 可是演算法的優勢並不是絕對的。試分析,當原檔案關鍵字有序時,快速排序時間複雜度是 O(n2), 這種情況下快速排序不快。而這種情況的氣泡排序是 O(n), 反而很快。在原檔案記錄關