排序算法合集

1基本概念
    排序(Sorting)是計算機程序設計中的一種重要操作,其功能是對一個數據元素集合或序列重新排列成一個按數據元素某個項值有序的序列。作爲排序依據的數據項稱爲排序碼,也即數據元素的關鍵碼。爲了便於查找,通常希望計算機中的數據表是按關鍵碼有序的。如有序表的折半查找,查找效率較高。還有,二叉排序樹、B-樹和B+樹的構造過程就是一個排序過程。若關鍵碼是主關鍵碼,則對於任意待排序序列,經排序後得到的結果是唯一的;若關鍵碼是次關鍵碼,排序結果可能不唯一,這是因爲具有相同關鍵碼的數據元素,這些元素在排序結果中,它們之間的的位置關係與排序前不能保持。
    若對任意的數據元素序列,使用某個排序方法,對它按關鍵碼進行排序:若相同關鍵碼元素間的位置關係,排序前與排序後保持一致,稱此排序方法是穩定的;而不能保持一致的排序方法則稱爲不穩定的。
排序分爲兩類:內排序和外排序。
內排序:指待排序列完全存放在內存中所進行的排序過程,適合不太大的元素序列。
外排序:指排序過程中還需訪問外存儲器,足夠大的元素序列,因不能完全放入內存,只能使用外排序。
2插入排序
2.1
直接插入排序
   設有n個記錄,存放在數組r中,重新安排記錄在數組中的存放順序,使得按關鍵碼有序。即
r[1].key≤r[2].key≤……≤r[n].key
先來看看向有序表中插入一個記錄的方法:
設1<n,r[1].key≤r[2].key≤……≤r[j-1].key,將r[j]插入,重新安排存放順序,使得r[1].key≤r[2].key≤……≤r[j].key,得到新的有序表,記錄數增1。
【算法1
r[0]=r[j] //r[j]r[0]中,使r[j]爲待插入記錄空位
i=j-1
//從第i個記錄向前測試插入位置,用r[0]爲輔助單元,可免去測試i<1
r[0].key≥r[i].key,轉 //插入位置確定
r[0].key < r[i].key時,
r[i+1]=r[i]
i=i-1;轉 //調整待插入位置
r[i+1]=r[0];結束。 //存放待插入記錄
【例1】向有序表中插入一個記錄的過程如下:
r[1] r[2] r[3] r[4] r[5]
存儲單元
2 10 18 25 9
r[5]插入四個記錄的有序表中,j=5
r[0]=r[j]
i=j-1初始化,設置待插入位置
2 10 18 25 □ r[i+1]
爲待插入位置
i=4
r[0] < r[i]r[i+1]=r[i]i--調整待插入位置
2 10 18 □ 25
i=3
r[0] < r[i]r[i+1]=r[i]i--調整待插入位置
2 10 □ 18 25
i=2
r[0] < r[i]r[i+1]=r[i]i--調整待插入位置
2 □ 10 18 25
i=1
r[0] ≥r[i]r[i+1]=r[0]插入位置確定,向空位填入插入記錄
2 9 10 18 25
向有序表中插入一個記錄的過程結束
直接插入排序方法:僅有一個記錄的表總是有序的,因此,對n個記錄的表,可從第二個記錄開始直到第n個記錄,逐個向有序表中進行插入操作,從而得到n個記錄按關鍵碼有序的表。
【算法2
void InsertSort(S_TBL &p)
{ for(i=2
i<=p->lengthi++)
if(p->elem[i].key < p->elem[i-1].key) /*
小於時,需將elem[i]插入有序表*/
{ p->elem[0].key=p->elem[i].key
/*爲統一算法設置監測*/
for(j=i-1
p->elem[0].key < p->elem[j].keyj--)
p->elem[j+1].key=p->elem[j].key
/*記錄後移*/
p->elem[j+1].key=p->elem[0].key
/*插入到正確位置*/
}
}
【效率分析】
空間效率:僅用了一個輔助單元。
時間效率:向有序表中逐個插入記錄的操作,進行了n-1趟,每趟操作分爲比較關鍵碼和移動記錄,而比較的次數和移動記錄的次數取決於待排序列按關鍵碼的初始排列。
最好情況下:即待排序列已按關鍵碼有序,每趟操作只需1次比較2次移動。
總比較次數=n-1
總移動次數=2(n-1)
最壞情況下:即第j趟操作,插入記錄需要同前面的j個記錄進行j次關鍵碼比較,移動記錄的次數爲j+2次。
平均情況下:即第j趟操作,插入記錄大約同前面的j/2個記錄進行關鍵碼比較,移動記錄的次數爲j/2+2次。
由此,直接插入排序的時間複雜度爲O(n2)。是一個穩定的排序方法。
2.2折半插入排序
直接插入排序的基本操作是向有序表中插入一個記錄,插入位置的確定通過對有序表中記錄按關鍵碼逐個比較得到的。平均情況下總比較次數約爲n2/4。既然是在有序表中確定插入位置,可以不斷二分有序表來確定插入位置,即一次比較,通過待插入記錄與有序表居中的記錄按關鍵碼比較,將有序表一分爲二,下次比較在其中一個有序子表中進行,將子表又一分爲二。這樣繼續下去,直到要比較的子表中只有一個記錄時,比較一次便確定了插入位置。
二分判定有序表插入位置方法:
low=1high=j-1r[0]=r[j] // 有序表長度爲j-1,第j個記錄爲待插入記錄
//
設置有序表區間,待插入記錄送輔助單元
low>high,得到插入位置,轉
low≤highm=(low+high)/2 // 取表的中點,並將表一分爲二,確定待插入區間*/
r[0].key<r[m].keyhigh=m-1 //插入位置在低半區
否則,low=m+1 // 插入位置在高半區

high+1即爲待插入位置,從j-1high+1的記錄,逐個後移,r[high+1]=r[0];放置待插入記錄。
【算法3
void InsertSort(S_TBL *s)
{ /*
對順序表s作折半插入排序 */
for(i=2
i<=s->lengthi++)
{ s->elem[0]=s->elem[i]
/* 保存待插入元素 */
low=i
high=i-1 /* 設置初始區間 */
while(low<=high) /*
該循環語句完成確定插入位置 */
{ mid=(low+high)/2

if(s->elem[0].key>s->elem[mid].key)
low=mid+1
/* 插入位置在高半區中 */
else high=mid-1
/* 插入位置在低半區中 */
}/* while */
for(j=i-1
j>=high+1j--) /* high+1爲插入位置 */
s->elem[j+1]=s->elem[j]
/* 後移元素,留出插入空位 */
s->elem[high+1]=s->elem[0]
/* 將元素插入 */
}/* for */
}/* InsertSort */
【時間效率】
確定插入位置所進行的折半查找,關鍵碼的比較次數至多爲,次,移動記錄的次數和直接插入排序相同,故時間複雜度仍爲O(n2)。是一個穩定的排序方法。
2.3
表插入排序
直接插入排序、折半插入排序均要大量移動記錄,時間開銷大。若要不移動記錄完成排序,需要改變存儲結構,進行表插入排序。所謂表插入排序,就是通過鏈接指針,按關鍵碼的大小,實現從小到大的鏈接過程,爲此需增設一個指針項。操作方法與直接插入排序類似,所不同的是直接插入排序要移動記錄,而表插入排序是修改鏈接指針。用靜態鏈表來說明。
#define SIZE 200
typedef struct{
ElemType elem
/*元素類型*/
int next
/*指針項*/
}NodeType
/*表結點類型*/
typedef struct{
NodeType r[SIZE]
/*靜態鏈表*/
int length
/*表長度*/
}L_TBL
/*靜態鏈表類型*/
假設數據元素已存儲在鏈表中,且0號單元作爲頭結點,不移動記錄而只是改變鏈指針域,將記錄按關鍵碼建爲一個有序鏈表。首先,設置空的循環鏈表,即頭結點指針域置0,並在頭結點數據域中存放比所有記錄關鍵碼都大的整數。接下來,逐個結點向鏈表中插入即可。
【例2】表插入排序示例
MAXINT 49 38 65 97 76 13 27 49
0 - - - - - - - -
MAXINT 49 38 65 97 76 13 27 49
1 0 - - - - - - -
MAXINT 49 38 65 97 76 13 27 49
2 0 1 - - - - - -
MAXINT 49 38 65 97 76 13 27 49
2 3 1 0 - - - - -
MAXINT 49 38 65 97 76 13 27 49
2 3 1 4 0 - - - -
MAXINT 49 38 65 97 76 13 27 49
2 3 1 5 0 4 - - -
MAXINT 49 38 65 97 76 13 27 49
6 3 1 5 0 4 2 - -
MAXINT 49 38 65 97 76 13 27 49
6 3 1 5 0 4 7 2 -
MAXINT 49 38 65 97 76 13 27 49
6 8 1 5 0 4 7 2 3
1
表插入排序得到一個有序的鏈表,查找則只能進行順序查找,而不能進行隨機查找,如折半查找。爲此,還需要對記錄進行重排。
重排記錄方法:按鏈表順序掃描各結點,將第i個結點中的數據元素調整到數組的第i個分量數據域。因爲第i個結點可能是數組的第j個分量,數據元素調整僅需將兩個數組分量中數據元素交換即可,但爲了能對所有數據元素進行正常調整,指針域也需處理。
【算法3
1. j=l->r[0].next
i=1 //指向第一個記錄位置,從第一個記錄開始調整
2.
i=l->length時,調整結束;否則,
a.
i=jj=l->r[j].nexti++;轉(2) //數據元素應在這分量中,不用調整,處理下一個結點
b.
j>il->r[i].elem<-->l->r[j].elem //交換數據元素
p=l->r[j].next
// 保存下一個結點地址
l->r[j].next=l->[i].next
l->[i].next=j // 保持後續鏈表不被中斷
j=p
i++;轉(2) // 指向下一個處理的結點
c.
j<iwhile(j<i) j=l->r[j].next//j分量中原記錄已移走,沿j的指針域找尋原記錄的位置
轉到(a)
【例3】對錶插入排序結果進行重排示例
MAXINT 49 38 65 97 76 13 27 49
6 8 1 5 0 4 7 2 3
MAXINT 13 38 65 97 76 49 27 49
6 (6) 1 5 0 4 8 2 3
MAXINT 13 27 65 97 76 49 38 49
6 (6) (7) 5 0 4 8 1 3
MAXINT 13 27 38 97 76 49 65 49
6 (6) (7) (7) 0 4 8 5 3
MAXINT 13 27 38 49 76 97 65 49
6 (6) (7) (7) (6) 4 0 5 3
MAXINT 13 27 38 49 49 97 65 76
6 (6) (7) (7) (6) (8) 0 5 4
MAXINT 13 27 38 49 49 65 97 76
6 (6) (7) (7) (6) (8) (7) 0 4
MAXINT 13 27 38 49 49 65 76 97
6 (6) (7) (7) (6) (8) (7) (8) 0
2
【時效分析】
表插入排序的基本操作是將一個記錄插入到已排好序的有序鏈表中,設有序表長度爲i,則需要比較至多i+1次,修改指針兩次。因此,總比較次數與直接插入排序相同,修改指針總次數爲2n次。所以,時間複雜度仍爲O(n2)
2.4希爾排序(Shell’s Sort)
希爾排序又稱縮小增量排序,是1959年由D.L.Shell提出來的,較前述幾種插入排序方法有較大的改進。
直接插入排序算法簡單,在n值較小時,效率比較高,在n值很大時,若序列按關鍵碼基本有序,效率依然較高,其時間效率可提高到O(n)。希爾排序即是從這兩點出發,給出插入排序的改進方法。
希爾排序方法:
1.
選擇一個步長序列t1t2tk,其中ti>tjtk=1
2.
按步長序列個數k,對序列進行k趟排序;
3.
每趟排序,根據對應的步長ti,將待排序列分割成若干長度爲m的子序列,分別對各子表進行直接插入排序。僅步長因子爲1時,整個序列作爲一個表來處理,表長度即爲整個序列的長度。
【例4】待排序列爲 3980764113295078301110074186
步長因子分別取531,則排序過程如下:
p=5 39 80 76 41 13 29 50 78 30 11 100 7 41 86
└─────────┴─────────┘
└─────────┴──────────┘
└─────────┴──────────┘
└─────────┴──────────┘
└─────────┘
子序列分別爲{3929100}{80507}{767841}{413086}{1311}
第一趟排序結果:
p=3 29 7 41 30 11 39 50 76 41 13 100 80 78 86
└─────┴─────┴─────┴──────┘
└─────┴─────┴─────┴──────┘
└─────┴─────┴──────┘
子序列分別爲{2930501378}{7117610086}{41394180}
第二趟排序結果:
p=1 13 7 39 29 11 41 30 76 41 50 86 80 78 100
此時,序列基本有序,對其進行直接插入排序,得到最終結果:
7 11 13 29 30 39 41 41 50 76 78 80 86 100
3
【算法5
void ShellInsert(S_TBL &p
int dk)
{ /*
一趟增量爲dk的插入排序,dk爲步長因子*/
for(i=dk+1
i<=p->lengthi++)
if(p->elem[i].key < p->elem[i-dk].key) /*
小於時,需elem[i]將插入有序表*/
{ p->elem[0]=p->elem[i]
/*爲統一算法設置監測*/
for(j=i-dk
j>0&&p->elem[0].key < p->elem[j].keyj=j-dk)
p->elem[j+dk]=p->elem[j]
/*記錄後移*/
p->elem[j+dk]=p->elem[0]
/*插入到正確位置*/
}
}
void ShellSort(S_TBL *pint dlta[]int t)
{ /*
按增量序列dlta[01…t-1]對順序表*p作希爾排序*/
for(k=0
k<tt++)
ShellSort(p
dlta[k]) /*一趟增量爲dlta[k]的插入排序*/
}
【時效分析】
希爾排序時效分析很難,關鍵碼的比較次數與記錄移動次數依賴於步長因子序列的選取,特定情況下可以準確估算出關鍵碼的比較次數和記錄的移動次數。目前還沒有人給出選取最好的步長因子序列的方法。步長因子序列可以有各種取法,有取奇數的,也有取質數的,但需要注意:步長因子中除1外沒有公因子,且最後一個步長因子必須爲1。希爾排序方法是一個不穩定的排序方法。
3 交換排序
交換排序主要是通過兩兩比較待排記錄的關鍵碼,若發生與排序要求相逆,則交換之。
3.1
冒泡排序(Bubble Sort)
先來看看待排序列一趟冒泡的過程:設1<j≤nr[1],r[2],···,r[j]爲待排序列,
通過兩兩比較、交換,重新安排存放順序,使得r[j]是序列中關鍵碼最大的記錄。一趟冒泡方法爲:
i=1 //設置從第一個記錄開始進行兩兩比較
i≥j,一趟冒泡結束。
比較r[i].keyr[i+1].key,若r[i].key≤r[i+1].key,不交換,轉
r[i].key>r[i+1].key時, r[0]=r[i]r[i]=r[i+1]r[i+1]=r[0]
r[i]r[i+1]交換
i=i+1調整對下兩個記錄進行兩兩比較,轉
冒泡排序方法:對n個記錄的表,第一趟冒泡得到一個關鍵碼最大的記錄r[n],第二趟冒泡對n-1個記錄的表,再得到一個關鍵碼最大的記錄r[n-1],如此重複,直到n個記錄按關鍵碼有序的表。
【算法6
j=n //n記錄的表開始
j<2,排序結束
i=1 //一趟冒泡,設置從第一個記錄開始進行兩兩比較,
i≥j,一趟冒泡結束,j=j-1;冒泡表的記錄數-1,轉
比較r[i].keyr[i+1].key,若r[i].key≤r[i+1].key,不交換,轉
r[i].key>r[i+1].key時, r[i]<-->r[i+1]r[i]r[i+1]交換
i=i+1調整對下兩個記錄進行兩兩比較,轉
【效率分析】
空間效率:僅用了一個輔助單元。
時間效率:總共要進行n-1趟冒泡,對j個記錄的表進行一趟冒泡需要j-1次關鍵碼比較。
移動次數:
最好情況下:待排序列已有序,不需移動。
3.2
快速排序
快速排序是通過比較關鍵碼、交換記錄,以某個記錄爲界(該記錄稱爲支點),將待排序列分成兩部分。其中,一部分所有記錄的關鍵碼大於等於支點記錄的關鍵碼,另一部分所有記錄的關鍵碼小於支點記錄的關鍵碼。我們將待排序列按關鍵碼以支點記錄分成兩部分的過程,稱爲一次劃分。對各部分不斷劃分,直到整個序列按關鍵碼有序。
一次劃分方法:
1≤p<q≤nr[p],r[p+1],...,r[q]爲待排序列
low=phigh=q //設置兩個搜索指針,low是向後搜索指針,high是向前搜索指針
r[0]=r[low]
//取第一個記錄爲支點記錄,low位置暫設爲支點空位
low=high,支點空位確定,即爲low
r[low]=r[0]
//填入支點記錄,一次劃分結束
否則,low<high,搜索需要交換的記錄,並交換之
low<highr[high].key≥r[0].key //high所指位置向前搜索,至多到low+1位置
high=high-1
;轉 //尋找r[high].key<r[0].key
r[low]=r[high]
//找到r[high].key<r[0].key,設置high爲新支點位置,
//
小於支點記錄關鍵碼的記錄前移。
low<highr[low].key<r[0].key //low所指位置向後搜索,至多到high-1位置
low=low+1
;轉 //尋找r[low].key≥r[0].key
r[high]=r[low]
//找到r[low].key≥r[0].key,設置low爲新支點位置,
//
大於等於支點記錄關鍵碼的記錄後移。
//繼續尋找支點空位
【算法7
int Partition(S_TBL *tbl,int low,int high) /*
一趟快排序*/
{ /*
交換順序表tbl中子表tbl->[low…high]的記錄,使支點記錄到位,並反回其所在位置*/
/*
此時,在它之前()的記錄均不大()於它*/
tbl->r[0]=tbl->r[low]; /*
以子表的第一個記錄作爲支點記錄*/
pivotkey=tbl->r[low].key; /*
取支點記錄關鍵碼*/
while(low<higu) /*
從表的兩端交替地向中間掃描*/
{ while(low<high&&tbl->r[high].key>=pivotkey) high--;
tbl->r[low]=tbl->r[high]; /*
將比支點記錄小的交換到低端*/
while(low<high&&tbl-g>r[high].key<=pivotkey) low++;
tbl->r[low]=tbl->r[high]; /*
將比支點記錄大的交換到低端*/
}
tbl->r[low]=tbl->r[0]; /*
支點記錄到位*/
return low; /*
反回支點記錄所在位置*/
}
【例 5】一趟快排序過程示例
r[1] r[2] r[3] r[4] r[5] r[6] r[7] r[8] r[9] r[10]
存儲單元
49 14 38 74 96 65 8 49 55 27
記錄中關鍵碼
low=1
high=10設置兩個搜索指針, r[0]=r[low]支點記錄送輔助單元,
□ 14 38 74 96 65 8 49 55 27
↑ ↑
low high
第一次搜索交換
high向前搜索小於r[0].key的記錄,得到結果
27 14 38 74 96 65 8 49 55 □
↑ ↑
low high
low向後搜索大於r[0].key的記錄,得到結果
27 14 38 □ 96 65 8 49 55 74
↑ ↑
low high
第二次搜索交換
high向前搜索小於r[0].key的記錄,得到結果
27 14 38 8 96 65 □ 49 55 74
↑ ↑
low high
low向後搜索大於r[0].key的記錄,得到結果
27 14 38 8 □ 65 96 49 55 74
↑ ↑
low high
第三次搜索交換
high向前搜索小於r[0].key的記錄,得到結果
27 14 38 8 □ 65 96 49 55 74
↑↑
low high
low向後搜索大於r[0].key的記錄,得到結果
27 14 38 8 □ 65 96 49 55 74
↑↑
low high
low=high
,劃分結束,填入支點記錄
27 14 38 8 49 65 96 49 55 74
【算法8
void QSort(S_TBL *tbl,int low,int high) /*
遞歸形式的快排序*/
{ /*
對順序表tbl中的子序列tbl->[low…high]作快排序*/
if(low<high)
{ pivotloc=partition(tbl,low,high); /*
將表一分爲二*/
QSort(tbl,low,pivotloc-1); /*
對低子表遞歸排序*/
QSort(tbl,pivotloc+1,high); /*
對高子表遞歸排序*/
}
}

【效率分析】
空間效率:快速排序是遞歸的,每層遞歸調用時的指針和參數均要用棧來存放,遞歸調用層次數與上述二叉樹的深度一致。因而,存儲開銷在理想情況下爲O(log2n),即樹的高度;在最壞情況下,即二叉樹是一個單鏈,爲O(n)
時間效率:在n個記錄的待排序列中,一次劃分需要約n次關鍵碼比較,時效爲O(n),若設T(n)爲對n個記錄的待排序列進行快速排序所需時間。
理想情況下:每次劃分,正好將分成兩個等長的子序列,則
T(n)≤cn+2T(n/2) c是一個常數
≤cn+2(cn/2+2T(n/4))=2cn+4T(n/4)
≤2cn+4(cn/4+T(n/8))=3cn+8T(n/8)
······
≤cnlog2n+nT(1)=O(nlog2n)
最壞情況下:即每次劃分,只得到一個子序列,時效爲O(n2)
快速排序是通常被認爲在同數量級(O(nlog2n))的排序方法中平均性能最好的。但若初始序列按關鍵碼有序或基本有序時,快排序反而蛻化爲冒泡排序。爲改進之,通常以三者取中法來選取支點記錄,即將排序區間的兩個端點與中點三個記錄關鍵碼居中的調整爲支點記錄。快速排序是一個不穩定的排序方法。
4
選擇排序
選擇排序主要是每一趟從待排序列中選取一個關鍵碼最小的記錄,也即第一趟從n個記錄中選取關鍵碼最小的記錄,第二趟從剩下的n-1個記錄中選取關鍵碼最小的記錄,直到整個序列的記錄選完。這樣,由選取記錄的順序,便得到按關鍵碼有序的序列。
4.1
簡單選擇排序
操作方法:第一趟,從n個記錄中找出關鍵碼最小的記錄與第一個記錄交換;第二趟,從第二個記錄開始的n-1個記錄中再選出關鍵碼最小的記錄與第二個記錄交換;如此,第i趟,則從第i個記錄開始的n-i+1個記錄中選出關鍵碼最小的記錄與第i個記錄交換,直到整個序列按關鍵碼有序。
【算法9
void SelectSort(S_TBL *s)
{ for(i=1
i<s->lengthi++)
{ /*
length-1趟選取 */
for(j=i+1
t=ij<=s->lengthj++)
{ /*
i開始的length-n+1個記錄中選關鍵碼最小的記錄 */
if(s->elem[t].key>s->elem[j].key)
t=j
/* t中存放關鍵碼最小記錄的下標 */
}
s->elem[t]<-->s->elem[i]
/* 關鍵碼最小的記錄與第i個記錄交換 */
}
}
從程序中可看出,簡單選擇排序移動記錄的次數較少,但關鍵碼的比較次數依然是
4.2樹形選擇排序
按照錦標賽的思想進行,將n個參賽的選手看成完全二叉樹的葉結點,則該完全二叉樹有2n-22n-1個結點。首先,兩兩進行比賽(在樹中是兄弟的進行,否則輪空,直接進入下一輪),勝出的再兄弟間再兩兩進行比較,直到產生第一名;接下來,將作爲第一名的結點看成最差的,並從該結點開始,沿該結點到根路徑上,依次進行各分枝結點子女間的比較,勝出的就是第二名。因爲和他比賽的均是剛剛輸給第一名的選手。如此,繼續進行下去,直到所有選手的名次排定。
【例616個選手的比賽(n=24)
從葉結點開始的兄弟間兩兩比賽,勝者上升到父結點;勝者兄弟間再兩兩比賽,直到根結點,產生第一名91。比較次數爲 23+22+21+20=24-1=n-1
將第一名的結點置爲最差的,與其兄弟比賽,勝者上升到父結點,勝者兄弟間再比賽,直到根結點,產生第二名83。比較次數爲4,即log2n次。其後各結點的名次均是這樣產生的,所以,對於n個參賽選手來說,即對1,故時間複雜度爲O(nlog2n)。該方法佔用空間較多,除需輸出排序結果的n個單元外,尚需n-1個輔助單元。-n+1)log2n-n個記錄進行樹形選擇排序,總的關鍵碼比較次數至多爲(n
4.3 堆排序(Heap Sort)
設有n個元素的序列 k1k2kn,當且僅當滿足下述關係之一時,稱之爲堆。
若以一維數組存儲一個堆,則堆對應一棵完全二叉樹,且所有非葉結點的值均不大於(或不小於)其子女的值,根結點的值是最小(或最大)的。
設有n個元素,將其按關鍵碼排序。首先將這n個元素按關鍵碼建成堆,將堆頂元素輸出,得到n個元素中關鍵碼最小(或最大)的元素。然後,再對剩下的n-1個元素建成堆,輸出堆頂元素,得到n個元素中關鍵碼次小(或次大)的元素。如此反覆,便得到一個按關鍵碼有序的序列。稱這個過程爲堆排序。
因此,實現堆排序需解決兩個問題:
1.
如何將n個元素的序列按關鍵碼建成堆;
2.
輸出堆頂元素後,怎樣調整剩餘n-1個元素,使其按關鍵碼成爲一個新堆。
首先,討論輸出堆頂元素後,對剩餘元素重新建成堆的調整過程。
調整方法:設有m個元素的堆,輸出堆頂元素後,剩下m-1個元素。將堆底元素送入堆頂,堆被破壞,其原因僅是根結點不滿足堆的性質。將根結點與左、右子女中較小(或小大)的進行交換。若與左子女交換,則左子樹堆被破壞,且僅左子樹的根結點不滿足堆的性質;若與右子女交換,則右子樹堆被破壞,且僅右子樹的根結點不滿足堆的性質。繼續對不滿足堆性質的子樹進行上述交換操作,直到葉子結點,堆被建成。稱這個自根結點到葉子結點的調整過程爲篩選。
【例6
再討論對n個元素初始建堆的過程。
建堆方法:對初始序列建堆的過程,就是一個反覆進行篩選的過程。n個結點的完全
子樹成爲堆,之後向前依次對各結點爲根的子樹進行篩選,使之成爲堆,直到根結點。
【例7
堆排序:對n個元素的序列進行堆排序,先將其建成堆,以根結點與第n個結點交換;調整前n-1個結點成爲堆,再以根結點與第n-1個結點交換;重複上述操作,直到整個序列有序。
【算法10
void HeapAdjust(S_TBL *h
int sint m)
{/*r[s…m]
中的記錄關鍵碼除r[s]外均滿足堆的定義,本函數將對第s個結點爲根的子樹篩選,使其成爲大頂堆*/
rc=h->r[s]

for(j=2*s
j<=mj=j*2) /* 沿關鍵碼較大的子女結點向下篩選 */
{ if(j<m&&h->r[j].key<h->r[j+1].key)
j=j+1
/* 爲關鍵碼較大的元素下標*/
if(rc.key<h->r[j].key) break
/* rc應插入在位置s*/
h->r[s]=h->r[j]
s=j /* 使s結點滿足堆定義 */
}
h->r[s]=rc
/* 插入 */
}
void HeapSort(S_TBL *h)
{ for(i=h->length/2
i>0i--) /* r[1..length]建成堆 */
HeapAdjust(h
ih->length)
for(i=h->length
i>1i--)
{ h->r[1]<-->h->r[i]
/* 堆頂與堆低元素交換 */
HeapAdjust(h
1i-1) /*r[1..i-1]重新調整爲堆*/
}
}
次,交換記錄至多k次。所以,在建好堆後,排序過程中的篩選次數不超過下式:
 + … +
û2)-log2(në + û1)-log2(në( 2  )ûlog22 < nlog2n2
而建堆時的比較次數不超過4n次,因此堆排序最壞情況下,時間複雜度也爲O(nlog2n)
10.5
二路歸併排序
二路歸併排序的基本操作是將兩個有序表合併爲一個有序表。
r[u…t]由兩個有序子表r[u…v-1]r[v…t]組成,兩個子表長度分別爲v-ut-v+1。合併方法爲:
i=uj=vk=u //置兩個子表的起始下標及輔助數組的起始下標
i>v j>t,轉 //其中一個子表已合併完,比較選取結束
//選取r[i]r[j]關鍵碼較小的存入輔助數組rf
如果r[i].key<r[j].keyrf[k]=r[i] i++ k++
否則,rf[k]=r[j] j++ k++
//將尚未處理完的子表中元素存入rf
如果i<v,將r[i…v-1]存入rf[k…t] //前一子表非空
如果j<=t,將r[i…v]存入rf[k…t] //後一子表非空
合併結束。
【算法11
void Merge(ElemType *r
ElemType *rfint uint vint t)
{
for(i=u
j=vk=ui<v&&j<=tk++)
{ if(r[i].key<r[j].key)
{ rf[k]=r[i]
i++}
else
{ rf[k]=r[j]
j++}
}
if(i<v) rf[k…t]=r[i…v-1]

if(j<=t) rf[k…t]=r[j…t]

}
.兩路歸併的迭代算法
1
個元素的表總是有序的。所以對n個元素的待排序列,每個元素可看成1個有序子
表長度均爲2。再進行兩兩合併,直到生成n個元素按關鍵碼有序的表。
【算法12
void MergeSort(S_TBL *p
ElemType *rf)
{ /*
*p表歸併排序,*rf爲與*p表等長的輔助數組*/
ElemType *q1
*q2
q1=rf
q2=p->elem
for(len=1
len<p->lengthlen=2*len) /*q2歸併到q1*/
{ for(i=1
i+2*len-1<=p->lengthi=i+2*len)
Merge(q2
q1ii+leni+2*len-1) /*對等長的兩個子表合併*/
if(i+len-1<p->length)
Merge(q2
q1ii+lenp->length) /*對不等長的兩個子表合併*/
else if(i<=p->length)
while(i<=p->length) /*
若還剩下一個子表,則直接傳入*/
q1[i]=q2[i]

q1<-->q2
/*交換,以保證下一趟歸併時,仍從q2歸併到q1*/
if(q1!=p->elem) /*
若最終結果不在*p表中,則傳入之*/
for(i=1
i<=p->lengthi++)
p->elem[i]=q1[i]

}
}
.兩路歸併的遞歸算法
【算法13
void MSort(ElemType *p
ElemType *p1int sint t)
{ /*
p[s…t]歸併排序爲p1[s…t]*/
if(s==t) p1[s]=p[s]
else
{ m=(s+t)/2
/*平分*p*/
MSort(p
p2sm) /*遞歸地將p[s…m]歸併爲有序的p2[s…m]*/
MSort(p
p2m+1t) /*遞歸地將p[m+1…t]歸併爲有序的p2[m+1…t]*/
Merge(p2
p1sm+1t) /*p2[s…m]p2[m+1…t]歸併到p1[s…t]*/
}
}
void MergeSort(S_TBL *p)
{ /*
對順序表*p作歸併排序*/
MSort(p->elem
p->elem1p->length)
}

【效率分析】
需要一個與表等長的輔助元素數組空間,所以空間複雜度爲O(n)
n個元素的表,將這n個元素看作葉結點,若將兩兩歸併生成的子表看作它們的父結點,則歸併過程對應由葉向根生成一棵二叉樹的過程。所以歸併趟數約等於二叉樹的高度-1,即log2n,每趟歸併需移動記錄n次,故時間複雜度爲O(nlog2n)
6基數排序
基數排序是一種藉助於多關鍵碼排序的思想,是將單關鍵碼按基數分成多關鍵碼進行排序的方法。
6.1 多關鍵碼排序
撲克牌中52張牌,可按花色和麪值分成兩個字段,其大小關係爲:
花色:梅花 < 方塊 < 紅心 < 黑心
面值: 2 < 3 < 4 < 5 < 6 < 7 < 8 < 9 < 10 < J < Q < K < A
若對撲克牌按花色、面值進行升序排序,得到如下序列:
梅花2,3,...,A,方塊2,3,...,A,紅心2,3,...,A,黑心2,3,...,A
即兩張牌,若花色不同,不論面值怎樣,花色低的那張牌小於花色高的,只有在同花色情況下,大小關係才由面值的大小確定。這就是多關鍵碼排序。
爲得到排序結果,我們討論兩種排序方法。
方法1:先對花色排序,將其分爲4個組,即梅花組、方塊組、紅心組、黑心組。再對每個組分別按面值進行排序,最後,將4個組連接起來即可。
方法2:先按13個面值給出13個編號組(2號,3號,...A),將牌按面值依次放入對應的編號組,分成13堆。再按花色給出4個編號組(梅花、方塊、紅心、黑心),將2號組中牌取出分別放入對應花色組,再將3號組中牌取出分別放入對應花色組,……,這樣,4個花色組中均按面值有序,然後,將4個花色組依次連接起來即可。
n個元素的待排序列包含d個關鍵碼{k1k2kd},則稱序列對關鍵碼{k1k2kd}有序是指:對於序列中任兩個記錄r[i]r[j](1≤i≤j≤n)都滿足下列有序關係:
其中k1稱爲最主位關鍵碼,kd稱爲最次位關鍵碼。
多關鍵碼排序按照從最主位關鍵碼到最次位關鍵碼或從最次位到最主位關鍵碼的順序
逐次排序,分兩種方法:
最高位優先(Most Significant Digit first)法,簡稱MSD法:先按k1排序分組,同一組中記錄,關鍵碼k1相等,再對各組按k2排序分成子組,之後,對後面的關鍵碼繼續這樣的排序分組,直到按最次位關鍵碼kd對各子組排序後。再將各組連接起來,便得到一個有序序列。撲克牌按花色、面值排序中介紹的方法一即是MSD法。
最低位優先(Least Significant Digit first)法,簡稱LSD法:先從kd開始排序,再對kd-1進行排序,依次重複,直到對k1排序後便得到一個有序序列。撲克牌按花色、面值排序中介紹的方法二即是LSD法。
6.2鏈式基數排序
將關鍵碼拆分爲若干項,每項作爲一個關鍵碼,則對單關鍵碼的排序可按多關鍵碼排序方法進行。比如,關鍵碼爲4位的整數,可以每位對應一項,拆分成4項;又如,關鍵碼由5個字符組成的字符串,可以每個字符作爲一個關鍵碼。由於這樣拆分後,每個關鍵碼都在相同的範圍內(對數字是09,字符是'a''z'),稱這樣的關鍵碼可能出現的符號個數爲,記作RADIX。上述取數字爲關鍵碼的10;取字符爲關鍵碼的26。基於這一特性,用LSD法排序較爲方便。
基數排序:從最低位關鍵碼起,按關鍵碼的不同值將序列中的記錄分配RADIX個隊列中,然後再收集之。如此重複d次即可。鏈式基數排序是用RADIX個鏈隊列作爲分配隊列,關鍵碼相同的記錄存入同一個鏈隊列中,收集則是將各鏈隊列按關鍵碼大小順序鏈接起來。
【例8】以靜態鏈表存儲待排記錄,頭結點指向第一個記錄。鏈式基數排序過程如下圖。
(a):初始記錄的靜態鏈表。
(b):第一趟按個位數分配,修改結點指針域,將鏈表中的記錄分配到相應鏈隊列中。
(c):第一趟收集:將各隊列鏈接起來,形成單鏈表。

(d):第二趟按十位數分配,修改結點指針域,將鏈表中的記錄分配到相應鏈隊列中。
(e):第二趟收集:將各隊列鏈接起來,形成單鏈表。
(f):第三趟按百位數分配,修改結點指針域,將鏈表中的記錄分配到相應鏈隊列中。
(g):第三趟收集:將各隊列鏈接起來,形成單鏈表。此時,序列已有序。
10
【算法14
#define MAX_KEY_NUM 8 /*
關鍵碼項數最大值*/
#define RADIX 10 /*
關鍵碼基數,此時爲十進制整數的基數*/
#define MAX_SPACE 1000 /*
分配的最大可利用存儲空間*/
typedef struct{
KeyType keys[MAX_KEY_NUM]
/*關鍵碼字段*/
InfoType otheritems
/*其它字段*/
int next
/*指針字段*/
}NodeType
/*表結點類型*/
typedef struct{
NodeType r[MAX_SPACE]
/*靜態鏈表,r[0]爲頭結點*/
int keynum
/*關鍵碼個數*/
int length
/*當前表中記錄數*/
}L_TBL
/*鏈表類型*/
typedef int ArrayPtr[radix]
/*數組指針,分別指向各隊列*/
void Distribute(NodeType *sint iArrayPtr *fArrayPtr *e)
{ /*
靜態鏈表ltblr域中記錄已按(kye[0]keys[1]keys[i-1])有序)*/
/*
本算法按第i個關鍵碼keys[i]建立RADIX個子表,使同一子表中的記錄的keys[i]相同*/
/*f[0…RADIX-1]
e[0…RADIX-1]分別指向各子表的第一個和最後一個記錄*/
for(j=0
j<RADIXj++) f[j]=0 /* 各子表初始化爲空表*/
for(p=r[0].next
pp=r[p].next)
{ j=ord(r[p].keys[i])
/*ord將記錄中第i個關鍵碼映射到[0…RADIX-1]*/
if(!f[j]) f[j]=p

else r[e[j]].next=p

e[j]=p
/* p所指的結點插入到第j個子表中*/
}
}
void Collect(NodeType *rint iArrayPtr fArrayPtr e)
{/*
本算法按keys[i]自小到大地將f[0…RADIX-1]所指各子表依次鏈接成一個鏈表*e[0…RADIX-1]爲各子表的尾指針*/
for(j=0
!f[j]j=succ(j)) /*找第一個非空子表,succ爲求後繼函數*/
r[0].next=f[j]
t=e[j] /*r[0].next指向第一個非空子表中第一個結點*/
while(j<RADIX)
{ for(j=succ(j)
j<RADIX-1&&!f[j]j=succ(j)) /*找下一個非空子表*/
if(f[j]) {r[t].next=f[j]
t=e[j]} /*鏈接兩個非空子表*/
}
r[t].next=0
/*t指向最後一個非空子表中的最後一個結點*/
}
void RadixSort(L_TBL *ltbl)
{ /*
ltbl作基數排序,使其成爲按關鍵碼升序的靜態鏈表,ltbl->r[0]爲頭結點*/
for(i=0
i<ltbl->lengthi++) ltbl->r[i].next=i+1
ltbl->r[ltbl->length].next=0
/*ltbl改爲靜態鏈表*/
for(i=0
i<ltbl->keynumi++) /*按最低位優先依次對各關鍵碼進行分配和收集*/
{ Distribute(ltbl->r
ife) /*i趟分配*/
Collect(ltbl->r
ife) /*i趟收集*/
}
}
【效率分析】
時間效率:設待排序列爲n個記錄,d個關鍵碼,關鍵碼的取值範圍爲radix,則進行鏈式基數排序的時間複雜度爲O(d(n+radix)),其中,一趟分配時間複雜度爲O(n),一趟收集時間複雜度爲O(radix),共進行d趟分配和收集。
空間效率:需要2*radix個指向隊列的輔助空間,以及用於靜態鏈表的n個指針。
7外排序
7.1
外部排序的方法
   外部排序基本上由兩個相互獨立的階段組成。首先,按可用內存大小,將外存上含n個記錄的文件分成若干長度爲k的子文件或段(segment),依次讀入內存並利用有效的內部排序方法對它們進行排序,並將排序後得到的有序子文件重新寫入外存。通常稱這些有序子文件爲歸併段或順串;然後,對這些歸併段進行逐趟歸併,使歸併段(有序子文件)逐漸由小到大,直至得到整個有序文件爲止。
顯然,第一階段的工作已經討論過。以下主要討論第二階段即歸併的過程。先從一個例子來看外排序中的歸併是如何進行的?
假設有一個含 10000 個記錄的
文件,首先通過10次內部排序得到
10
個初始歸併段 R1R10 ,其中每
一段都含1000個記錄。然後對它們
作如圖11所示的兩兩歸併,直至
得到一個有序文件爲止。

從圖11可見,由10個初始歸併段到一個有序文件,共進行了四趟歸併,每一趟
將兩個有序段歸併成一個有序段的過程,若在內存中進行,則很簡單,前面討論的2-路歸併排序中的Merge函數便可實現此歸併。但是,在外部排序中實現兩兩歸併時,不僅要調用Merge函數,而且要進行外存的讀/寫,這是由於我們不可能將兩個有序段及歸併結果同時放在內存中的緣故。對外存上信息的讀/寫是以物理塊爲單位。假設在上例中每個物理塊可以容納200個記錄,則每一趟歸併需進行5050,四趟歸併加上內部排序時所需進行的讀/寫,使得在外排序中總共需進行500次的讀/寫。
一般情況下,外部排序所需總時間=
內部排序(產生初始歸併段)所需時間 m*tis
+
外存信息讀寫的時間 d*tio
+
內部歸併排序所需時間 s*utmg
其中:tis是爲得到一個初始歸併段進行的內部排序所需時間的均值;tio是進行一次外存讀/寫時間的均值;utmg是對u個記錄進行內部歸併所需時間;m爲經過內部排序之後得到的初始歸併段的個數;s爲歸併的趟數;d爲總的讀/寫次數。由此,上例10000個記錄利用2-路歸併進行排序所需總的時間爲:
10*tis+500*tio+4*10000tmg
其中tio取決於所用的外存設備,顯然,tiotmg要大的多。因此,提高排序效率應主要着眼於減少外存信息讀寫的次數d
下面來分析d歸併過程的關係。若對上例中所得的10個初始歸併段進行5-平衡歸併(即每一趟將5個或5個以下的有序子文件歸併成一個有序子文件),則從下圖可見,僅需進行二趟歸併,外部排序時總的讀/寫次數便減少至2×100+100=300,比2-路歸併減少了200次的讀/寫。
R1 R2 R3 R4 R5 R6 R7 R8 R9 R10
└─┴─┼─┴─┘ └─┴─┼─┴─┘
R1' R2'
└────┬────┘
有序文件
12
可見,對同一文件而言,進行外部排序時所需讀/寫外存的次數和歸併的趟數s成正比。而在一般情況下,對m個初始歸併段進行k-路平衡歸併時,歸併的趟數
可見,若增加k或減少m便能減少s。下面分別就這兩個方面討論之。
7.2
多路平衡歸併的實現
從上式可見,增加k可以減少s,從而減少外存讀/寫的次數。但是,從下面的討論中又可發現,單純增加k將導致增加內部歸併的時間utmg。那末,如何解決這個矛盾呢?
先看2-路歸併。令u個記錄分佈在兩個歸併段上,按Merge函數進行歸併。每得到歸併後的含u個記錄的歸併段需進行u-1次比較。
再看k-路歸併。令u個記錄分佈在k個歸併段上,顯然,歸併後的第一個記錄應是k個歸併段中關鍵碼最小的記錄,即應從每個歸併段的第一個記錄的相互比較中選出最小者,這需要進行k-1次比較。同理,每得到歸併後的有序段中的一個記錄,都要進行k-1次比較。顯然,爲得到含u個記錄的歸併段需進行(u-1)(k-1)次比較。由此,對n個記錄的文件進行外部排序時,在內部歸併過程中進行的總的比較次數爲s(k-1)(n-1)。假設所得初始歸併段爲m個,則可得內部歸併過程中進行比較的總的次數爲

k
而減少外存信息讀寫時間所得效益,這是我們所不希望的。然而,若在進行k-路歸併時利用敗者樹”(Tree of Loser),則可使在k個記錄中選出關鍵碼最小的記錄時僅需進
它不再隨k的增長而增長。
何謂敗者樹?它是樹形選擇排序的一種變型。相對地,我們可稱圖10.5和圖10.6中二叉樹爲勝者樹,因爲每個非終端結點均表示其左、右子女結點中勝者。反之,若在雙親結點中記下剛進行完的這場比賽中的敗者,而讓勝者去參加更高一層的比賽,便可得到一棵敗者樹
【例9
(a) (b)
13 實現5-路歸併的敗者樹
13(a)即爲一棵實現5-路歸併的敗者樹ls[0…4],圖中方形結點表示葉子結點(也可看成是外結點),分別爲5個歸併段中當前參加歸併的待選擇記錄的關鍵碼;敗者樹中根結點ls[1]的雙親結點ls[0]冠軍,在此指示各歸併段中的最小關鍵碼記錄爲第三段中的記錄;結點ls[3]指示b1b2兩個葉子結點中的敗者即是b2,而勝者b1b3(b3是葉子結點b3b4b0經過兩場比賽後選出的獲勝者)進行比較,結點ls[1]則指示它們中的敗者爲b1。在選得最小關鍵碼的記錄之後,只要修改葉子結點b3中的值,使其爲同一歸併段中的下一個記錄的關鍵碼,然後從該結點向上和雙親結點所指的關鍵碼進行比較,敗者留在該雙親,勝者繼續向上直至樹根的雙親。如圖10.13(b)所示。當第3個歸併段中第2個記錄參加歸併時,選得最小關鍵碼記錄爲第一個歸併段中的記錄。爲了防止在歸併過程中某個歸併段變爲空,可以在每個歸併段中附加一個關鍵碼爲最大的記錄。當選出的冠軍記錄的關鍵碼爲最大值時,表明此次歸併已完成。由於實現k-路歸併的敗者樹
的初始化也容易實現,只要先令所有的非終端結點指向一個含最小關鍵碼的葉子結點,然後從各葉子結點出發調整非終端結點爲新的敗者即可。
下面程序中簡單描述了利用敗者樹進行k-路歸併的過程,爲了突出如何利用敗者樹進行歸併,避開了外存信息存取的細節,可以認爲歸併段已存在。
【算法15
typedef int LoserTree[k]; /*
敗者樹是完全二叉樹且不含葉子,可採用順序存儲結構*/
typedef struct{
KeyType key;
}ExNode,External[k]; /*
外結點,只存放待歸併記錄的關鍵碼*/
void K_Merge(LoserTree *ls,External *b) /*k-路歸併處理程序*/
{ /*
利用敗者樹ls將編號從0k-1k個輸入歸併段中的記錄歸併到輸出歸併段*/
/*b[0]
b[k-1]爲敗者樹上的k個葉子結點,分別存放k個輸入歸併段中當前記錄的關鍵碼*/
for(i=0;i<k;i++) input(b[i].key); /*
分別從k個輸入歸併段讀入該段當前第一個記錄的*/
/*
關鍵碼到外結點*/
CreateLoserTree(ls); /*
建敗者樹ls,選得最小關鍵碼爲b[0].key*/
while(b[ls[0]].key!=MAXKEY)
{ q=ls[0]; /*q
指示當前最小關鍵碼所在歸併段*/
output(q); /*
將編號爲q的歸併段中當前(關鍵碼爲b[q].key的記錄寫至輸出歸併段)*/
input(b[q].key); /*
從編號爲q的輸入歸併段中讀入下一個記錄的關鍵碼*/
Adjust(ls,q); /*
調整敗者樹,選擇新的最小關鍵碼*/
}
output(ls[0]); /*
將含最大關鍵碼MAXKEY的記錄寫至輸出歸併段*/
}

void Adjust(LoserTree *ls,int s) /*
選得最小關鍵碼記錄後,從葉到根調整敗者樹,選下一個最小關鍵碼*/
{ /*
沿從葉子結點b[s]到根結點ls[0]的路徑調整敗者樹*/
t=(s+k)/2; /*ls[t]
b[s]的雙親結點*/
while(t>0)
{ if(b[s].key>b[ls[t]].key) s<-->ls[t]; /*s
指示新的勝者*/
t=t/2;
}
ls[0]=s;
}
void CreateLoserTree(LoserTree *ls) /*建立敗者樹*/
{ /*
已知b[0]b[k-1]爲完全二叉樹ls的葉子結點存有k個關鍵碼,沿從葉子到根的k條路徑*/
/*
ls調整爲敗者樹*/
b[k].key=MINKEY; /*
MINKEY爲關鍵碼可能的最小值*/
for(i=0;i<k;i++) ls[i]=k; /*
設置ls敗者的初值*/
for(i=k-1;k>0;i--) Adjust(ls,i); /*
依次從b[k-1],b[k-2],…,b[0]出發調整敗者*/
}
最後要提及一點,k值的選擇並非越大越好,如何選擇合適的k是一個需要綜合考慮的問題。
 
 
發佈了13 篇原創文章 · 獲贊 2 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章