歸並排序算法簡介
歸並排序就是利用歸並的思想實現的排序方法
假設初始序列含有n個記錄,看成是n個有序的子序列,每個子序列的長度為1,然后兩兩歸並,得到 |n/2|(|x|表示不小於x的最小整數)個長度為2或1的有序子序列;再兩兩歸並,如此重復,直至得到一個長度為n的有序序列為止,這種排序方法稱為2路歸並排序
歸並排序算法的遞歸實現
MSort()函數
//對順序表L作歸並排序(此函數的作用就是進行一次封裝,達到統一接口的效果)
void MergeSort(SqList *L){
MSort(L->r, L->r, 1, L->length);
}
//將SR[s..t]歸並排序為TR1[s..t]
void MSort(int SR[], int TR1[], int s, int t){
int m;
int TR2[MAXSIZE + 1];
//遞歸結束條件
if (s == t)
TR1[s] = SR[s];
else{
//將SR[s..t]平分為SR[s..m]和SR[m+1..t]
m = (s + t) / 2;
//遞歸將SR[s..m]歸並為有序的TR2[s..m]
MSort(SR, TR2, s, m);
//遞歸將SR[m+1..t]歸並為有序TR2[m+1..t]
MSort(SR, TR2, m + 1, t);
//將TR2[s..m]和TR2[m+1..t] 歸並到TR1[s..t]
Merge(TR2,TR1, s, m, t);
}
}
假設對數組{50,10,90,30,70,40,80,60,20}進行排序,L.length=9
則第一次調用MSort()函數的時候(還未進行遞歸),我們將原數組分成了兩個部分
-
“MSort(SR,TR2,1,5):是將數組SR中的第1~5的關鍵字歸並到有序的TR2(調用前TR2為空數組)
-
“MSort(SR,TR2,6,9):是將數組SR中的第6~9的關鍵字歸並到有序的TR2
如下圖所示:
接下來,將進行不斷的遞歸深入,直到滿足s==t的遞歸結束條件,也就是划分的每個部分只剩下一個元素
然后一層一層退出迭代,每次退出一層迭代,就會調用一次Merge()函數,將TR2數組的兩個部分歸並為一個TR1數組
整個過程如下圖所示:
在理解了整體的算法流程之后,我們再看一下Merge()函數的實現
Merge()函數
//將有序的SR[i..m]和SR[m+1..n]歸並為有序的TR[i..n]
void Merge(int SR[], int TR[], int i, int m, int n){
int j, k, l;
//將SR中記錄由小到大歸並入TR
for (j = m + 1, k = i; i <= m && j <= n; k++){
if (SR[i] < SR[j])
TR[k] = SR[i++];
else
TR[k] = SR[j++];
}
if (i <= m){ //將剩余的SR[i..m]復制到TR
for (l = 0; l <= m - i; l++)
TR[k + l]=SR[i + l];
}
if (j<=n){ //將剩余的SR[j..n]復制到TR
for (l = 0; l <= n - j; l++)
TR[k + l] = SR[j + l];
}
}
注意在該函數中:
-
i為SR數組前一部分的索引 -
j為SR數組后一部分的索引 -
k為TR數組(歸並目標數組)的索引
前面已經提到,每次退出一層遞歸,都會調用一個歸並函數Merge(),將兩個部分的數組歸並為一個數組
這里我們只解釋最后一次歸並,也就是最復雜的一次歸並
最后一次遞歸調用的Merge是將{10,30,50,70,90}與{20,40,60,80}歸並為最終有序的序列
因此數組SR為{10,30,50,70,90,20,40,60,80},i=1,m=5,n=9
如圖所示:
首先,該函數的第一個for循環負責將SR數組的前后兩個部分的元素,按照順序(逐個比較)進行歸並
例如第一次歸並的操作:
- SR[i]=SR[1]=10,SR[j]=SR[6]=20,TR[k]=TR[1]=10,並且i++
如此循環,直至i或j越界( i>m 或 j>n )
一旦這兩個部分有一個部分越界之后,說明這個部分的元素已經完成了歸並
則接下來我們需要將另一個部分的未歸並的剩余元素直接復制到TR[]中
如下圖展示了后半部分先歸並完成的情況:
最后,將歸並剩下的數組數據,移動到TR的后面:
當前k=9,i=m=5,for循環l=0,TR[k+l]=SR[i+l]=90,完成歸並排序,如下圖所示:
遞歸實現歸並排序算法總結
由於歸並排序在歸並過程中需要與原始記錄序列同樣數量的存儲空間存放歸並結果以及遞歸時深度為log2n(2為底)的棧空間
因此空間復雜度為O(n+logn)
Merge函數中if(SR[i] < SR[j])語句說明需要兩兩比較,不存在跳躍,因此歸並排序是一種穩定的排序算法
歸並排序是一種比較占用內存,但卻效率高且穩定的算法
歸並排序算法的非遞歸實現
《大話數據結構》中的實現方式:
MergeSort2()函數
//對順序表L作歸並非遞歸排序
void MergeSort2(SqList *L){
//申請額外空間
int * TR = (int *)malloc(L->length * sizeof(int));
int k = 1;
while (k < L->length){
MergePass(L->r, TR, k, L->length);
//子序列長度加倍
k = 2 * k;
MergePass(TR, L->r, k, L->length);
//子序列長度加倍
k = 2 * k;
}
}
以上是非遞歸實現的歸並排序算法的主體函數,其實現方式是使用L->r數組和TR數組互相進行兩兩歸並算法
如下圖所示,並以此類推:
每次歸並完成之后,將歸並的子序列長度x2,直至結束
其核心函數為MergePass()函數,接下來看它的實現方式
MergePass()函數
//將SR[]中相鄰長度為s的子序列兩兩歸並到TR[]
void MergePass(int SR[], int TR[], int s, int n){
int i = 1;
int j;
while (i <= n - 2 * s + 1){
//兩兩歸並
Merge(SR, TR, i, i + s - 1, i + 2 * s - 1);
i = i + 2 * s;
}
//歸並最后兩個序列
if (i < n - s + 1)
Merge(SR, TR, i, i + s - 1, n);
//若最后只剩下單個子序列
else
for (j = i; j <= n; j++)
TR[j] = SR[j];
}
首先,第一次調用MergePass(L.r,TR,k,L.length)函數的時候:
- L.r是初始無序狀態,TR為新申請的空數組,k=1,L.length=9
問:為什么while的條件是i<=n-2s+1?
答:因為下面調用Merge()函數時,將兩個部分進行歸並,而其中的后一部分的結尾下標為:i+2s-1,因此若i>n-2s+1則有i+2s-1>n,會導致數組越界
這樣第一次調用這個函數時,兩兩歸並的范圍限於1-8,9號元素會被剩下來
對於這種多余的元素,我們再判斷,i+s-1(這是用來歸並的前一部分的終點)是否小於n:
-
若
i+s-1<n,說明歸並的前一部分的終點並未到達n,則可以繼續歸並,移相得:i<n-s+1,則進入if分支,繼續調用Merge()函數進行歸並 -
若
i+s-1>=n,說明若繼續歸並,則歸並的前一部分的終點已經達到n,則無法歸並,進入else分支,可以直接將剩余部分的元素復制到TR數組
注意:Merge(int SR[], int TR[], int i, int m, int n)
-
i值指前一部分的開始下標
-
m值指前一部分的結束下標(后一部分從m+1開始)
-
n值為后一部分的結束下標
非遞歸實現歸並排序算法總結
非遞歸的迭代方法,避免了遞歸時深度為log2n(2為底)的棧空間,空間只是用到申請歸並臨時用的TR數組,空間復雜度為O(n),避免遞歸在時間性能上也有一定的提升
應該說,使用歸並排序算法時,盡量考慮使用非遞歸方法
另一種非遞歸實現方式(來自小甲魚數據結構視頻教程)
注釋十分詳細,不再講解
#include<stdio.h>
#include<stdlib.h>
#define MAXSIZE 10
void MergeSort(int k[],int n){
//next是用來標志temp數組下標的
int i,next;
//每次歸並都是對兩段數據進行對比排序
//left\right分別代表左面和右面(前面和后面)的兩段數據
//min和max分別代表各段數據的最前和最后下標
int left_min,left_max,right_min,right_max;
//申請一段內存用於存放排序的臨時變量
int *temp = (int *)malloc(n * sizeof(int));
//步長:i;從步長=1開始逐級遞增
for(i=1; i<n; i*=2){
//每次步長遞增,都從頭開始歸並處理
for(left_min=0; left_min<n-i; left_min = right_max){
//兩段數據和步長之間關系
right_min = left_max = left_min + i;
right_max = left_max + i;
//最后的下標不能超過n,否則無意義
if(right_max>n)
right_max = n;
//每次的內層循環都會將排列好的數據返回到K數組,因此next指針需每次清零
next = 0;
//兩端數據均未排完
while(left_min<left_max&&right_min<right_max){
if(k[left_min] < k[right_min])
temp[next++] = k[left_min++];
else
temp[next++] = k[right_min++];
}
//上面的歸並排序循環結束后,可能有一段數據尚未完全被排列帶temp數組中
//剩下未排列到temp中的數據一定是按照升序排列的最大的一部分數據
//此時有兩種情況:left未排列完成,right未排列完成
//若是left未排列完成(left_min<left_max),則對於這一段數據省去temp數組的中轉,直接賦值到k數組,即從right_max開始倒着賦值
//若是right未排列完成,則可以想到,那一段數據本就在應該放置的位置,則無需處理
while(left_min < left_max)
//上面分析應該從right_max開始倒着賦值,但是實際因為右邊的數據段已經全部排列
//故此時right_min=right_max
//且這里將right_min移動到需要的位置,方便下面賦值時使用
k[--right_min] = k[--left_max];
while(next>0)
//把排列好的數據段賦值給k數組
//這里可以直接用上面經過--right_min倒數過來的right_min值
//經過上面倒數的處理,right_min恰好在需要賦值和不需要賦值的數據段的分界處
k[--right_min] = temp[--next];
}
}
}
//測試
int main(){
int i,a[10] = {5,2,6,0,3,9,1,7,4,8};
MergeSort(a,10);
printf("排序后的結果是:");
for(i=0; i<10; i++)
printf("%d",a[i]);
printf("\n\n");
return 0;
}
