您的位置:首页 > 其它

Merge Sort(归并排序)

2017-10-04 00:35 183 查看
归并排序是建立在归并操作(将若干(通常为两个)有序的序列合成为一个更大的有序序列)上的一种排序算法,是分治法的典型应用。

算法思想

用分治法的思想,将已有序的子序列合并,得到完全有序的序列,即先使每个子序列有序,再使子序列段间有序。常见的归并排序将两个有序的子序列合成一个更大的序列,称为二路归并排序。



实现步骤

以自顶向下的二路归并排序为例(从小到大)

1.申请辅助空间,存放子序列合并后的序列

2.设置两个指针,分别指向两个有序的子序列的起始位置

3.比较两个指针指向的元素值大小,将较小的元素放入辅助空间,并移动指针到下一个位置

4.重复步骤3,直到其中一个子序列的所有元素都访问完

5.将另一个元素未被访问完的子序列中的剩余所有未访问元素直接复制到辅助空间队列尾

源码

自顶向下的归并排序(递归实现)

void Merge (int *a, int *tmp, int first, int mid, int last)
{
int i = first, j = mid+1, k = first;
while (i != mid+1 && j != last+1)
{
if (a[i] < a[j])
tmp[k++] = a[i++];
else
tmp[k++] = a[j++];
}
while (i != mid+1)
tmp[k++] = a[i++];
while (j != last+1)
tmp[k++] = a[j++];
for (i = first; i <= last; i++)
a[i] = tmp[i];
}
void MergeSort (int *a, int *tmp, int first, int last)
{
int mid;
if (first < last)
{
mid = (first+last)/2;
MergeSort(a, tmp, first, mid);
MergeSort(a, tmp, mid+1, last);
Merge(a, tmp, first, mid, last);
}
}

递归实现的归并排序是分治法的典型应用,分治法将一个大问题分割成小问题来分别解决,然后用所有小问题的答案来解决整个大问题。

自底向上的归并排序(非递归实现)

实现归并的另一种方法是先归并小序列,然后再成对归并得到的子序列,如此这般,直到将整个序列归并到一起(听起来像是自顶向下归并的逆过程)。自底向上的归并首先进行的是两两归并(把每个元素都看做是一个大小为1的子序列),然后将归并后得到的大小为2的子序列再两两归并,直到归并完整个序列。最后一次归并的第二个子序列可能会比第一个小,否则所有归并过程中两个子序列大小都会是一样的。
void Merge (int *a, int *tmp, int first, int mid, int last)
{
int i = first, j = mid+1, k = first;
while (i != mid+1 && j != last+1)
{
if (a[i] < a[j])
tmp[k++] = a[i++];
else
tmp[k++] = a[j++];
}
while (i != mid+1)
tmp[k++] = a[i++];
while (j != last+1)
tmp[k++] = a[j++];
for (i = first; i <= last; i++)
a[i] = tmp[i];
}
void MergeSort (int *a, int *tmp, int n)
{
for (int gap = 1; gap < n; gap *= 2)
for (int i = 0; i < n-gap; i += gap*2)
Merge(a, tmp, i, i+gap-1, min(i+gap*2-1, n-1));
}


自底向上的归并排序比较适合用链表组织的数据。想象一下先按大小为1的子链表进行排序,然后是大小为2的子链表,然后是大小为4的子链表,依此类推。这种方法只需要重新组织链表链接就能将链表原地排序(不需要创建任何新的链表结点)。

改进

1.对小规模子序列使用插入排序
用不同的排序算法配合处理小规模问题能改进大多数递归算法的性能,因为递归会使小规模问题中方法的调用过于频繁,所以改进对它们的处理方法就能改进整个算法。插入排序非常简单,相较而言插入排序的原地、迭代实现的特性使得它在小规模数据上更有优势,因此很可能在小序列上比归并排序更快(这种速度从算法的时间复杂度上不能体现出来)。因此可以使用插入排序或选择排序处理小规模的子序列,一般可以将归并排序的运行时间缩短10%~15%。通过对插入排序和归并排序两者运行时间的分析可以得出数据规模的阙值在10~20之间。
这种改进只针对规模较大的数据。
void InsertionSort (int *a, int first, int last)
{
int i, j, temp;
for (i = first+1; i < last-first; i++)
{
if (a[i] < a[i-1])
{
temp = a[i];
for (j = i-1; j >= first && a[j] > temp; j--)
a[j+1] = a[j];
a[j+1] = temp;
}
}
}
void Merge (int *a, int *tmp, int first, int mid, int last)
{
int i = first, j = mid+1, k = first;
while (i != mid+1 && j != last+1)
{
if (a[i] < a[j])
tmp[k++] = a[i++];
else
tmp[k++] = a[j++];
}
while (i != mid+1)
tmp[k++] = a[i++];
while (j != last+1)
tmp[k++] = a[j++];
for (i = first; i <= last; i++)
a[i] = tmp[i];
}
void MergeSort (int *a, int *tmp, int first, int last)
{
int mid;
if (first < last)
{
mid = (first+last)/2;
if ((last-first)/2 < 15)
{
InsertionSort(a, first, mid);
InsertionSort(a, mid+1, last);
}
else
{
MergeSort(a, tmp, first, mid);
MergeSort(a, tmp, mid+1, last);
}
Merge(a, tmp, first, mid, last);
}
}


2.测试序列是否已经有序
添加一个判断条件,当子序列已经有序的时候就可以跳过Merge函数。这个改动不影响排序的递归调用,但任意有序的子序列算法的运行时间就变为线性的了。这样优化会使归并排序失去稳定性。
void Merge (int *a, int *tmp, int first, int mid, int last)
{
int i = first, j = mid+1, k = first;
while (i != mid+1 && j != last+1)
{
if (a[i] < a[j])
tmp[k++] = a[i++];
else
tmp[k++] = a[j++];
}
while (i != mid+1)
tmp[k++] = a[i++];
while (j != last+1)
tmp[k++] = a[j++];
for (i = first; i <= last; i++)
a[i] = tmp[i];
}
void MergeSort (int *a, int *tmp, int first, int last)
{
int mid;
if (first < last)
{
mid = (first+last)/2;
MergeSort(a, tmp, first, mid);
MergeSort(a, tmp, mid+1, last);
if (a[mid] > a[mid+1])
Merge(a, tmp, first, mid, last);
}
}


3.不将元素复制到辅助空间
传统归并排序需要将原序列中的元素反复与辅助空间交换,我们可以考虑节省将元素复制到辅助空间所用的时间(但空间不行)。要做到这一点需要调用两种排序方法,一种将数据从输入序列排序到辅助空间,一种将数据从辅助空间排序到输入序列。简单来说,就是让辅助空间也能用来排序,这样从理论上就减少了一半的数据交换次数,更充分利用了辅助空间。这种方法需要在递归调用的每个层次交换输入序列和辅助序列的角色。
void Merge (int *a, int *tmp, int first, int mid, int last)
{
int i = first, j = mid+1, k = first;
while (i != mid+1 && j != last+1)
{
if (a[i] < a[j])
tmp[k++] = a[i++];
else
tmp[k++] = a[j++];
}
while (i != mid+1)
tmp[k++] = a[i++];
while (j != last+1)
tmp[k++] = a[j++];
for (i = first; i <= last; i++)
a[i] = tmp[i];
}
void MergeSort1 (int *a, int *tmp, int first, int last)
{
int mid;
if (first < last)
{
mid = (first+last)/2;
MergeSort2(tmp, a, first, mid);
MergeSort2(tmp, a, mid+1, last);
Merge(tmp, a, first, mid, last);
}
}
void MergeSort2 (int *tmp, int *a, int first, int last)
{
int mid;
if (first < last)
{
mid = (first+last)/2;
MergeSort1(a, tmp, first, mid);
MergeSort1(a, tmp, mid+1, last);
Merge(a, tmp, first, mid, last);
}
}


时间复杂度

以自顶向下的归并排序为例,通过前面的示例图可以看到,一个拥有N个元素的序列需要划分log2N次(层),最下面的第k层有2k个子序列,每个子序列的长度为2n-k,归并最多需要2n-k次比较。因此每层的比较次数为2k×2n-k=2n,n层总共有n2n=Nlog2N。故归并排序所需的时间和Nlog2N成正比,也即归并排序的时间复杂度为O(nlog2n)。
当序列长度为2的幂时,自顶向下和自底向上的归并排序所用的比较次数和数组访问次数正好相同,只是顺序不同。其他时候,两种方法的比较和数组访问的次序会有所不同。

此外,没有任何基于比较的排序算法能够保证使用少于log2(N!)~Nlog2N次比较将长度为N的序列排序。因此,归并排序已经是基于比较的排序算法中时间复杂度最优的排序算法了。

稳定性

对于相等元素,归并排序在分解和归并过程中并不会改变相等元素的相对顺序,因此归并排序是稳定的排序算法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息