內部排序算法
先介紹幾個排序算法中的概念:
排序的穩定性是指:待排序的記錄序列中可能存在兩個或兩個以上關鍵字相等的記錄,排序後具有相等關鍵字的記錄的相對順序不變。
例如排序前的記錄序列A={2,4,1,2,8,1};則穩定排序後的序列爲B={1,1,2,2,4,8}。
原地排序是指:在排序過程中不使用額外的內存空間,而只是通過對原序列的記錄兩兩進行比較和交換來完成排序過程。
由於待排序的記錄數量不同,使得在排序過程中涉及的存儲器不同,可將排序方法分爲兩類:
內部排序:待排序記錄存放在計算機內存中進行排序的過程;
外部排序:待排序記錄的數量非常大,以致內存一次不能容納全部記錄,在排序過程中需要對外存進行訪問的排序過程。
這裏,我們主要介紹內部排序。內部排序大致可以分爲五類:
(1) 插入排序
(2) 交換排序
(3) 選擇排序
(4) 歸併排序
(5) 計數排序
按照排序過程中的工作量,可以分爲三類:
(1) 簡單排序:時間複雜度爲O(n^2)
(2) 先進排序:時間複雜度爲O(n*logn)
(3) 基數排序:時間複雜度爲O(d*n)
1、插入排序(Insertion Sort)
1.1 直接插入排序(Straight Insertion Sort)
直接插入排序是將一個記錄插入到已排好序的有序表中,從而得到一個記錄數加1的新有序表。
#include <stdlib.h>
#include <iomanip>
#include <iostream>
using namespace std;
typedef int KeyType, InfoType;
//記錄類型
struct Record{
KeyType key;
InfoType otherinfo;
};
//待排序的 序列
#define MAXSIZE 20 //序列長度
struct SqList{
Record Sq[MAXSIZE+1]; //將Sq[0]作爲哨兵
int length;
};
//隨機初始化一個序列(待排序)
void Initialize_List(SqList &L){
for(int i=1; i<=L.length; i++)
L.Sq[i].key = 1+(int)(100.0*rand()/(RAND_MAX+1.0)); //關鍵字的值在100以內
}
//打印序列
void Print_List(SqList &L){
for(int i=1; i<=L.length; i++){
cout<<L.Sq[i].key<<setw(5);
}
cout<<endl;
}
//直接插入排序
void InsertSort(SqList &L){
int i,j;
for(i=2; i<=L.length; i++){ //將L.Sq[1]看作一個已排好序的序列
if(L.Sq[i-1].key > L.Sq[i].key){
L.Sq[0] = L.Sq[i]; //保存第i個記錄
L.Sq[i] = L.Sq[i-1]; //後移第i-1個記錄
for(j=i-2; L.Sq[j].key>L.Sq[0].key; j--) //尋找插入位置j
L.Sq[j+1] = L.Sq[j]; //將比第i個記錄大的所有記錄依次往後移動一個位置
L.Sq[j+1] = L.Sq[0]; //插入新紀錄
}
}
}
int main(){
//生成一個僞隨機序列
SqList L;
L.length = MAXSIZE;
Initialize_List(L);
Print_List(L);
//排序後
InsertSort(L);
Print_List(L);
return 0;
}
結果:
1 57 20 81 59 48 36 90 83 75 18 86 72 52 31 2 10 37 15 17
1 2 10 15 17 18 20 31 36 37 48 52 57 59 72 75 81 83 86 90
代碼說明:如果待排序序列中有n個記錄,則整個排序過程執行n-1趟插入。即:先將第1個記錄看成是一個有序的子序列,然後從第2個記錄開始逐個進行插入,直至整個序列變成按關鍵字非遞減的有序序列爲止。顯然,直接插入排序的時間複雜度爲O(n^2),且插入排序是原地排序、穩定排序。
1.2 折半插入排序(Binary Insertion Sort)
直接插入排序在序列長度n很大時效率很低,在一個有序序列中插入一個新紀錄,需要將該記錄和序列中的已有紀錄順序兩兩比較,以確定新紀錄的插入位置。在尋找新紀錄的插入位置時候,我們可以採用二分法查找算法。注意:折半插入減少了關鍵字間的比較次數,但紀錄的移動次數不變,因此,折半插入的時間複雜仍爲O(n^2)。
//折半插入排序
void BinInsertSort(SqList &L){
int low, high, mid; //low,high標記插入區間
for(int i=2; i<=L.length; i++){ //將L.Sq[1]看作一個已排好序的序列,從2個記錄開始插入
low = 1;
high = i-1;
while(low <= high){
mid = (low+high)/2;
if(L.Sq[mid].key > L.Sq[i].key)
high = mid - 1;
else low = mid + 1;
}
L.Sq[0] = L.Sq[i]; //保存待插入的新紀錄到Sq[0]處
for(int k=i-1; k>=high+1; k--) //high後面的記錄後移一個位置
L.Sq[k+1] = L.Sq[k];
L.Sq[high+1] = L.Sq[0]; //插入新紀錄
}
}
1.3 希爾排序(Shell's Sort)
從對直接插入排序的分析得知,其算法時間複雜度爲O(n^2),但是,若待排記錄序列爲“正序”時,其時間複雜度可提高至O(n)。由此可設想,若待排記錄序列按關鍵字“基本有序”時,直接插入排序的效率就可大大提高。另一方面,由於直接插入排序算法簡單,則在n值很小時效率也比較高。希爾排序正是從這兩點分析出發對直接插入排序進行改進得到的一種算法,其時間複雜度爲O(n^a),其中1<a<2。並且希爾排序是非穩定排序、原地排序。
希爾排序又稱爲“縮小增量排序”(Diminishing Increment Sort),它的基本思想是:先將整個待排記錄序列分割爲若干個子序列分別進行直接插入排序,待整個序列中的記錄“基本有序”時,再對全體記錄進行一次直接插入排序。注意:子序列的構成不是簡單地“逐段分割”,而是將相隔某個“增量”的記錄組成一個子序列。例如,如果增量爲5,則待排記錄序列中的{a1, a6, a11……}、(a2, a7, a12……)都構成一個子序列,我們分別對每個增量爲5的子序列進行直接插入排序;然後縮小增量,最後分別對增量爲1的所有子序列進行直接插入排序,並完成整個排序過程。
//一趟Shell排序,前後記錄位置的增量是dk,而不是1
void ShellInsert(SqList &L, int dk){
int i,j;
for(i=dk+1; i<=L.length; i++){ //將L.Sq[i-dk]看作一個已排好序的序列
if(L.Sq[i-dk].key > L.Sq[i].key){
L.Sq[0] = L.Sq[i]; //暫存第i個記錄
for(j=i-dk; L.Sq[j].key>L.Sq[0].key && j>0; j-=dk) //尋找插入位置j
L.Sq[j+dk] = L.Sq[j]; //將比第i個記錄大的所有記錄依次往後移動一個位置
L.Sq[j+dk] = L.Sq[0]; //插入新紀錄
}
}
}
//按增量序列dlta[0..t-1]對順序表做希爾排序
void ShellSort(SqList &L, int dlta[], int t){
for(int k=0; k<t; k++)
ShellInsert(L, k);
}
2、快速排序(Quick Sort)
2.1 冒泡排序(Bubble Sort)
第 1 躺冒泡排序是從第1個記錄到第n個記錄,依次比較 第一個記錄和第二記錄(逆序則交換),第二個記錄和第三個記錄……,最後會將最大的記錄換到最後一個位置上。
第 2 躺 冒泡排序是從第1個記錄到第n-1個記錄,依次比較 第一個記錄和第二記錄(逆序則交換),第二個記錄和第三個記錄……,最後會將次最大的記錄換到倒數第二個位置上。
第 m 躺冒泡排序是依次兩兩比較從第1個記錄到第n-m+1個記錄,並將整個序列中第m大的記錄放到序列中的倒數第m個位置上去。經過n-1躺冒泡排序,就得到一個完整的有序序列。
冒泡排序的時間複雜度爲O(n^2),且也是穩定排序、原地排序。
//交換兩個記錄
void swap(Record &r1, Record &r2){
Record tmp;
tmp = r1;
r1 = r2;
r2 = tmp;
}
//冒泡排序
void BubbleSort(SqList &L){
int m, n;
bool isOK = false; //哨兵,用於提前結束冒泡排序
for(m=1; m<=L.length-1 && !isOK; m++){ //第m躺將第m大的記錄放到倒數第m個位置上
isOK = true; //假定該趟排序能夠完成
for(n=2; n<=(L.length-m+1); n++)
if(L.Sq[n].key < L.Sq[n-1].key){ //交換兩個相鄰的逆序記錄
swap(L.Sq[n-1], L.Sq[n]);
isOK = false; //如果發生了記錄交換的事件,就認爲排序未完成
}
}
}
代碼說明:如果初始序列恰好已經是非遞減序列了,則只需進行一趟排序。可見冒泡排序並不一定要走完所有n-1躺才能排好,所以上面的代碼中設置了一個哨兵,來檢測能夠提前完成的冒泡排序。
2.2 快速排序(Quick Sort)
快速排序是對冒泡排序的一種改進,它的基本思想是:通過一趟排序將待排記錄序列分割成兩個獨立的部分,其中一部分記錄的關鍵字均比另一部分記錄的關鍵字小(以支點記錄來劃分),則可分別對這兩部分記錄繼續進行排序,以達到整個序列有序。
一趟快速排序的具體做法是:附設兩個指針low和 high,設支點記錄(pivot)的關鍵字爲pivotkey,則首先從high所指位置起向前搜索找到第一個關鍵字小於pivotkey的記錄並和支點記錄互相交換,然後從low所指位置起向後搜索,找到第一個關鍵字大於pivotkey的記錄和支點記錄互相交換,重複這兩步直至low=high爲止。
//快速排序
int Partition(SqList &L, int low, int high){
KeyType pivotkey = L.Sq[low].key;
while(low < high){
while(low<high && L.Sq[high].key>=pivotkey) high--; //從high起向前搜索,找到第一個小於支點記錄的位置
swap(L.Sq[low],L.Sq[high]);
while(low<high && L.Sq[low].key<=pivotkey) low++; //從 low其向後搜索,找到第一個大於支點記錄的位置
swap(L.Sq[low],L.Sq[high]);
}
return low; //返回支點記錄所在的位置
}
//對子序列[low high]進行快速排序(遞歸實現)
void QuickSort(SqList &L, int low, int high){
KeyType pivotkey;
if(low < high){
pivotkey = Partition(L, low, high);
QuickSort(L, low, pivotkey-1);
QuickSort(L, pivotkey+1, high);
}
}
代碼說明:上面在一趟快速排序的過程中,每交換一對記錄(調用swap函數)需要進行3次賦值操作,而實際上,在排序過程中對支點記錄的賦值時多餘的,因爲只有在一趟排序結束時,即low=high的位置纔是支點記錄的最後位置。由此Partition函數可以改寫爲下面的形式,先將支點記錄暫存在L.Sq[0]的位置上,直至一趟排序結束後再將支點記錄移至正確的位置上。
int Partition_Improved(SqList &L, int low, int high){
KeyType pivotkey = L.Sq[low].key;
L.Sq[0] = L.Sq[low]; //暫存支點記錄
while(low < high){
while(low<high && L.Sq[high].key>=pivotkey) high--; //從high起向前搜索,找到第一個小於支點記錄的位置
L.Sq[low] = L.Sq[high];
while(low<high && L.Sq[low].key<=pivotkey) low++; //從 low其向後搜索,找到第一個大於支點記錄的位置
L.Sq[high] = L.Sq[low];
}
L.Sq[low] = L.Sq[0]; //還原支點記錄到位
return low; //返回支點記錄所在的位置
}
3、選擇排序(Selection Sort)
3.1 簡單選擇排序(Simple Selection Sort)
選擇排序的基本思想是:第 m 躺選擇排序是遍歷從第m個記錄到第n個記錄的子序列,並將整個序列中第m小的記錄放到序列中的第m個位置上去。經過n-1躺選擇排序,就得到一個完整的有序序列。
與冒泡排序相比,選擇排序是找到關鍵字最小的記錄並交換到相應的位置上;而冒泡排序是找到關鍵字最大的記錄並交換到相應的位置上。
與插入排序相比,選擇排序是確定一個位置的記錄;而插入排序是確定一個記錄的位置。
簡單選擇排序的時間複雜度爲O(n^2),且也是穩定排序、原地排序。
//簡單選擇排序
void SimpleSelectionSort(SqList &L){
int index;
for(int i=1; i<L.length; i++){
index = i; //index爲關鍵字最小值記錄對應的索引值
for(int j=i+1; j<L.length-1; j++)
if(L.Sq[j].key < L.Sq[index].key)
index = j;
if(i != index)
swap(L.Sq[i], L.Sq[index]);
}
}
3.2 堆排序(Heap Sort)
4、歸併排序(Merging Sort)
歸併的含義是將兩個或兩個以上的有序表組合成一個新的有序表。利用歸併的這種思想很容易實現排序:假設待排記錄序列有n個記錄,則可看成是n個有序的子序列,每個子序列的長度爲1,然後兩兩歸併,得到[n/2]個長度爲2或1的有序子序列([ ]表示向上取整);再兩兩歸併,如此重複,直到得到一個長度爲n的有序序列爲止,這種排序方法稱爲2-路歸併排序。注意:歸併排序的時間複雜度爲O(n*logn),並且它是一種穩定排序,但不是原地排序。
//將序列SR的兩個相鄰子序列SR[m,...,k]和SR[k+1,...,n]歸併爲有序的TR[m,...,n]
void Merge(Record SR[], Record TR[], int m, int k, int n){
int i, j;
//m遍歷序列SR[m,...,k],j遍歷序列SR[k+1,...,n],i遍歷序列TR
for(i=m, j=k+1; m<=k && j<=n; i++){
if(SR[m].key <= SR[j].key)
TR[i] = SR[m++];
else
TR[i] = SR[j++];
}
while(m <= k)TR[i++] = SR[m++]; //將剩餘的SR[m,...,k]複製到TR中
while(j <= n)TR[i++] = SR[j++]; //將剩餘的SR[j,...,n]複製到TR中
}
//將序列SR[s,...,t]歸併排序爲TR1[s,...,t]
void MSort(Record SR[], Record TR1[], int s, int t){
int mid;
Record TR2[MAXSIZE+1]; //輔助存儲空間
if(s == t) TR1[s] = SR[s];
else {
mid = (s+t)/2; //將序列兩等分
MSort(SR, TR2, s, mid); //遞歸地將SR[s,...,mid]歸併爲有序的TR2[s,...,mid]
MSort(SR, TR2, mid+1, t); //遞歸地將SR[mid+1,...,t]歸併爲有序的TR2[mid+1,...,t]
Merge(TR2, TR1, s, mid, t); //將TR2[s,...,mid]和TR2[mid+1,...,t]歸併到TR1[s,...,t]
}
}
//2-路歸併排序
void MergeSort(SqList &L){
MSort(L.Sq, L.Sq, 1, L.length);
}
值的注意的是:歸併排序需要進行[logn]躺([ ]表示向上取整),實現歸併排序需要和待排記錄數量相等的輔助存儲空間(非原地排序)。此外,遞歸形式的算法在形式上簡潔,但實用性很差,爲了效率通常要採用其非遞歸形式的算法。
5、基數排序(Radix Sorting)
前面討論的各類排序方法都是通過關鍵字之間的比較和移動記錄這兩種操作來實現的,基數排序是一種完全不同的排序方法。基數排序是藉助多關鍵字排序思想對單邏輯關鍵字進行排序的方法。6、總結
綜合比較本文所討論的各種內部排序算法,大致有如下結果:
(1) 上表中的簡單排序包括除希爾排序之外的所有插入排序、冒泡排序和簡單選擇排序,其中以直接插入排序爲最簡單,當序列中的記錄“基本有序”或n值較小時,它是最佳排序方法。
(2) 從平均時間性能而言,快速排序最佳,其所需時間最省,但快速排序在最壞情況下的時間性能不如堆排序和歸併排序。而後兩者相比的結果是,在n較大時,歸併排序所需時間較堆排序省,但它所需的輔助存儲量最多。
(3) 基數排序適用於n值很大而關鍵字較小的序列。若關鍵字也很大,而序列中大多數記錄的“最高位關鍵字”均不同,則亦可先按“最高位關鍵字”不同將序列分成若干“小”的子序列,而後進行直接插入排序。
(4) 從方法的穩定性來比較,所有時間複雜度爲O(n^2)的簡單排序法也是穩定的,然而,快速排序、堆排序和希爾排序等時間性能較好的排序方法都是不穩定的。
To be continued....