归并排序是数据结构中常用的一种排序算法,它原理上简单,易于理解。它基于分治法策略:先做划分,再做排序。归并排序的思想来源于一个朴实的想法,即,部分有序的序列要比完全无序的序列更便于排序。我们先给出算法的伪码:
dataList mergeSort(dataList& L) {
if(Length(L) <= 1) {
return L;
}
dataList L1 = L的左半部分;
dataList L2 = L的右半部分;
return merge(mergerSort(L1), mergeSort(L2));
}
算法的伪码也很容易理解,首先对于一个初始的序列,我们先将其不断二分,得到一个个单元素序列,然后依次对每个序列使用merger函数做归并,下面给出算法的过程示意:
21 25 49 25* 16 08 31 41 21 25 49 25* 16 08 31 41 21 25 49 25* 16 08 31 41
21 25 25* 49 08 16 31 41 21 25 25* 49 08 16 31 41 08 16 21 25 25* 31 41 49
从上面简单的二路归并例子我们可以看到归并排序的一般步骤,并且知道归并排序是一种稳定的排序算法。下面给出归并排序的算法。首先我们给出归并步骤的算法:
template<class T>
void merge(dataList<T>L1, dataList<T>L2, int left, int right, int mid) {
for(int k=left; k<=right; k++) {
L2[k] = L1[k];
}
int s1 = left, s2 = mid + 1, t = left;
while(s1 <= mid && s2 <=right) {
if(L2[s1]<=L2[s2]) L1[t++] = L2[s1++];
else L1[t++]=L2[s2++];
}
while(s1<=left) L1[t++] = L2[s1++];
while(s2<=right) L1[t++] = L2[s2++];
}
有了归并步骤的算法之后,我们就可以完成一个简单的二路归并算法:
template<class T>
void mergeSort(dataList<T>L, dataList<T>L2, int left, int right) {
//初始情况下,这里的L2是一个空序列
if(left > right) return;
int mid = (left + right) / 2;
mergeSort(L, L2, left, mid);
mergeSort(L, L2, mid+1, right);
merge(L, L2, left, right, mid);
}
可以看到,归并算法需要一个辅助数组,其实他就是一个典型的空间换时间的排序算法,空间消耗为,时间消耗一般为:。综合来看,其时间复杂度是。
下面介绍一种简单的,对二路归并做改进的方法:
我们看到,归并步骤时,我们需要判断指针s1和s2的位置是否超出了所在数组的限制。我们也发现,真正做归并排序的时候我并没有对数组做物理上的划分,仅仅是设置不同的位置记号来区分我们需要的子序列。其实我们可以有有一个取巧的办法,就是往L2中复制数组的时候,可以一个由left往mid复制,一个由right往mid复制。这样可以设置s1=left,s2=right,当s1==s2的时候,排序结束,具体的,我们来看相应的代码:
template<T>
void improvedmerge(dataList<T>L1, dataList<T>L2, int left, int right, int mid) {
int s1=left, s2=right, t=left, k;
for(k=left; k<=mid; k++) {
L2[k]=L1[k];
}
for(k=mid+1; k<=right; k++) {
L2[right+mid+1-k] = L1[k];
}
while(t<=right) {
if(L2[s1]<=L2[s2]) L1[t++]=L2[s1++];
else L1[t++]=L2[s2--];
}
}
同时,我们可以设置阈值,当子序列长度小于固定值K的时候,我们使用插排,这样可以提升实际运行时候的代码运行速度,但是从数量级角度而言,算法的时间复杂度是不变的。
当然了,我们也很容易想到,既然有二路归并,自然也会有三路归并、四路归并。他们计算机中的磁盘读写中经常会用到,有兴趣可以参考算法导论和操作系统。
Reference:
数据结构(用面向对象方法与C++语言描述).殷人昆.清华大学出版社
算法导论