目录:
0.提出
1945年由约翰·冯·诺伊曼(John von Neumann)首次提出。
1. 基本思想
归并排序是用分治思想,分治模式在每一层递归上有三个步骤:
分解(Divide):将n个元素分成个含n/2个元素的子序列。
解决(Conquer):用合并排序法对两个子序列递归的排序。
合并(Combine):合并两个已排序的子序列已得到排序结果。
2. 执行流程
1.不断地将当前序列平均分割成2个子序列
直到不能再分割(序列中只剩1个元素)
2.不断地将2个子序列合并成一个有序序列
直到最终只剩下1个有序序列
此算法浅显易懂,以一个数组为例,不断的将一个数组进行分割,直至不可分。此时再对其进行归并,排序完成。
以下列数据为例,26,5,77,1,61,11,59,15,48,19
1 把数组进行分割直至不可分。该数组有10个元素,我们不断的从中间切割。
2 再重新进行归并,如26和5进行一次比较,小的5放前面(升序)
至此,不断重复操作2,最后排序就成功了。
3. 代码如何实现?
【C++】
在实现代码之前,我们需要一个重要的问题,怎么切割成两个子序列呢。
我们应该从中间切。此外,因为是不断的从中间切割,你联想到了什么?没错,递归调用!那这个递归调用的边界退出条件是什么呢?是子序列不可分割!也就是当你切到只剩下1个元素的时候,就退出。
那现在开始动手吧,先定义一个MergeSort函数,那传的参数要有哪些呢?首先,一个数组和数组长度这是必须的。此外,因为是递归调用,所以是不是每次都要传首元素的下表和尾元素的下标呢?是的,没错,于是很轻松的写下了下面这些代码。
void MergeSort(int* array, int length) {
int* tmp = new int[length](); // 申请堆空间内存
MergeSort(array, tmp, 0, length-1);
delete[] tmp; // 销毁临时数组内存
}
void MergeSort(int* array, int* tmp, int begin, int end) {
// 左闭右闭 的设计模式
// [0,mid] [mid+1,end]
if (end - begin < 1) { // 边界条件
return;
}
// 递归分解再合并
int mid = (begin + end) >> 1; // 右移位运算符 相比 除法运算符,运行效率更高
MergeSort(array, tmp, begin, mid);
MergeSort(array, tmp, mid + 1, end);
Merge(array, tmp, begin, mid, end); // 合并,看后面
copy(array, tmp, begin, end); // 拷贝tmp数组的元素到array,拷贝范围是begin到end
}
在这里,我申请了一个临时数组来存放新排序,之后再把排好序的数组拷贝到原先的数组,再把临时数组销毁掉。此外,这里还写到左闭右闭的设计模式,大家也许会有些困惑,请看下去。
至此,分割的代码就写完了,那么如何合并呢?
/**定义三个指针
* i 指向 原数组 左半部分 初始位置
* j 指向 原数组 右半部分 初始位置
* k 指向 新数组的 初始位置
*/
void Merge(int* array, int* tmp, int begin, int mid ,int end) {
int i = begin, j = mid + 1, k = begin;
// 不断的把小的放入新数组,如若左半或者右半完成,就退出循环
while (i <= mid && j <= end) {
if (array[i] <= array[j]) { // 右半部比较大
tmp[k++] = array[i++]; // 小的放新数组,并且指针右移
}
else {
tmp[k++] = array[j++];
}
}
// 能来到这里表示此时已经有一边(左半OR右半)先挪动完成了
if (i > mid) { // 如果i>mid,表明左半部分已经搬完,现在只要搬右半的
for (int q = j; q <= end; q++) {
tmp[k++] = array[q];
}
}
else {
for (int q = i; q <= mid; q++) {
tmp[k++] = array[q];
}
}
}
没错,如上左闭右闭的设计模式,可以规范我们对边界上元素的处理逻辑,从0到mid,和mid+1到end,鲜明的区分了左右两个部分。
此外,附上另外一种归并排序的解法,这种就不用把排序好的元素放在新数组里面了,直接在原来的数组上进行排序,但是要先备份左半的元素出来。但是这次我们采用了左闭右开的设计模式。[0,mid), [mid,end)
这样子,就不用判断mid是不是要+1了。
void MergeSort(int* array, int end) {
int* list = new int[end / 2]; // 备用的数组,大小是数组长度一般
MergeSort(array, list, 0, end);
delete[] list;
}
void MergeSort(int* array, int* dest, int begin, int end) {
if (end - begin < 2) return;
// 小于2个元素 退出,这里之所以是<2而不是<1,是因为end在数组最后位元素的后一位,采取的是左闭右开的设计,即array[10],数组index从0-9,但是传了index = 10
int mid = (begin + end) >> 1; // >>1运算符表示除2
MergeSort(array, dest, begin, mid);
MergeSort(array, dest, mid, end);
Merge(array, dest, begin, mid, end);
}
void Merge(int* array, int* dest, int begin, int mid, int end) {
int li = 0, le = mid - begin; //li为左半的首位元素,le为左半的最后一个元素与左半开始之间元素的个数
int ri = mid, re = end; //ri为右半的首位元素,re为右半的最后一个元素
int ai = begin; // ai指向原数组开始位置的序号
for (int i = li; i < le; i++) {
dest[i] = array[begin + i]; // 备份左边数组
}
while (li < le) { // 如果左边的还没搬完
if (ri < re && cmp(array[ri], dest[li]) < 0) { // 左边 > 右边
array[ai++] = array[ri++]; // 右边的元素放到左边数组去
} // 能来到下边,表示两种情况
else { // 1. 左边 < 右边
// 2.右边已经搬完
array[ai++] = dest[li++]; // 把备份的左边元素放入原数组处
}
}
}
附录:
void copy(int* a, int* b, int left, int right) {
for (int i = left; i <= right; i++)
a[i] = b[i];
}