快速排序是最常用最常考到的排序方式,但不够稳定。
今天我们来学习一种新的、稳定的排序方法:归并排序。
文章目录
1 归并排序
1、假设一个序列有n个数字:那么我们将它看成n个子序列,每个子序列有一个数字;
2、此时,我们进行两两归并,得到n/2
个长度为2的序列(也可能是(n-1)/2
个长度为2和1
个长度为1的序列);
3、继续两两归并,递归,保证每一个归并后的子序列都是有序的;
4、得到一个长度为n的有序序列,结束排序。
可见,归并排序中两个最重要的动作是:递归划分、合并排序。 下面我们需要实现这两个操作。
2 代码实现
划分过程Msort
:将数组长苏变为1,存入另一个新开辟的数组中。
合并排序Merge
:将两个有序数组(复制的数组)归并到另一个数组(最原始的数组)中。
如此动态图的排序过程,代码实现如下:
#include<iostream>
using namespace std;
int A[8] = {
6,5,3,1,8,7,2,4 };
int LENGTH = 8;
void Merge(int* data, int* copy, int start, int length, int end)//两个数组合并过程
{
int i = start + length; //原数组前一半的最后一个索引
int j = end; //原数组后一半的最后一个索引
int newIndex = end; //新数组最后一个索引
while (i >= start && j >= start + length + 1)//归并过程
{
if (copy[i] > copy[j])
{
data[newIndex] = copy[i];
newIndex--;
i--;
}
else
{
data[newIndex] = copy[j];
newIndex--;
j--;
}
}
while (i >= start)//处理没有归并进去的剩余部分(最小的一些数字)
{
data[newIndex] = copy[i];
newIndex--;
i--;
}
while (j >= start + length + 1)//处理没有归并进去的剩余部分(最小的一些数字)
{
data[newIndex] = copy[j];
newIndex--;
j--;
}
}
void Msort(int* data, int* copy, int start, int end)
{
if (start == end)
{
copy[start] = data[start];
return;
}
int length = (end - start) / 2;
Msort(copy, data, start, start + length); //将前一半数组排序
Msort(copy, data, start + length + 1, end); //将后一半数组排序
Merge(data, copy, start, length ,end); //将前后两个排序数组合并
}
void mergeSort(int* a)
{
int* data = a;
int* copy = new int[LENGTH];
for (int i = 0; i < LENGTH; i++)
{
copy[i] = data[i];
}
Msort(data, copy, 0, LENGTH - 1);
delete[] copy;//防止内存泄漏
}
int main()
{
mergeSort(A);
for (int i = 0; i < LENGTH; i++)
{
printf("%d ", A[i]);
}
system("pause");
return 0;
}
3 复杂度分析
这是个比较典型的“空间换时间”的例子。
空间复杂度:我们需要新开辟一个和原数组大小相同的数组,故空间复杂度稳定为O(n)
时间复杂度:由于归并算法是个类似“二叉树”分割的算法,故划分过程Msort
的复杂度为O(logn)
;随后的合并排序Merge
中有三个while语句,但并非嵌套,故复杂度为O(n)
。因此时间复杂度稳定为O(nlogn)
。
要特别提一下,归并排序的优势在于稳定,不同于快速排序,会有最优或最坏情况。因此在面对不同问题时要分别考虑,找到稳定和效率的平衡点,再决定采用的排序方法。
4 可应对的题:逆序数
问题:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。
解答:
#include<iostream>
using namespace std;
int A[4] = {
7,5,6,4 };
int LENGTH = 4;
int Merge(int* data, int* copy, int start, int length, int end)//两个数组合并过程
{
int i = start + length; //原数组前一半的最后一个索引
int j = end; //原数组后一半的最后一个索引
int newIndex = end; //新数组最后一个索引
int middle = 0;
while (i >= start && j >= start + length + 1)//归并过程
{
if (copy[i] > copy[j])
{
data[newIndex] = copy[i];
newIndex--;
i--;
middle += j - start - length;//增加的部分:把前面的元素移到后面所增加的逆序数
}
else
{
data[newIndex] = copy[j];
newIndex--;
j--;
}
}
while (i >= start)//处理没有归并进去的剩余部分(最小的一些数字)
{
data[newIndex] = copy[i];
newIndex--;
i--;
}
while (j >= start + length + 1)//处理没有归并进去的剩余部分(最小的一些数字)
{
data[newIndex] = copy[j];
newIndex--;
j--;
}
return middle;
}
int Msort(int* data, int* copy, int start, int end)
{
if (start == end)
{
copy[start] = data[start];
return 0;
}
int length = (end - start) / 2;
int left = Msort(copy, data, start, start + length); //将前一半数组排序
int right = Msort(copy, data, start + length + 1, end); //将后一半数组排序
int middle = Merge(data, copy, start, length ,end); //将前后两个排序数组合并
return left + right + middle;
}
void mergeSort(int* a)
{
int* data = a;
int* copy = new int[LENGTH];
for (int i = 0; i < LENGTH; i++)
{
copy[i] = data[i];
}
int count = Msort(data, copy, 0, LENGTH - 1);
delete[] copy;
printf("逆序数的数量:%d\n", count);//打印出逆序数的数量
}
int main()
{
mergeSort(A);
for (int i = 0; i < LENGTH; i++)
{
printf("%d ", A[i]);
}
system("pause");
return 0;
}
结果如下:
最后对比一下归并排序和逆序数的代码差别作为本篇博客的结语吧: