归并排序
笔者:吃汉堡吃到饱
本周学习了归并排序,在九月底初学排序的时候就早有耳闻,基本思想也曾了解过,但对于其中部分代码的具体实现,仍然感觉困难重重。
这周提前学习了二分思想和快慢指针法,为理解归并排序提供了良好的条件,于是通过这篇博客记录一下,既是为日后复习提供帮助,也作为本周学习的一次记录。
0.你为什么选择归并排序
1.速度快:归并排序的速度仅次于快速排序。时间复杂度为O(N*logN),空间复杂度为O(N)
2.稳定性强。
稳定排序概念:保证排序前两个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,如果Ai = Aj,Ai原来在位置前,排序后Ai还是要在Aj位置前。
- 稳定排序:冒泡、插入、归并
- 不稳定排序:选择、希尔、快速排序(可以做到稳定但是比较难)、堆排序
1.基本思想
在阐释之前,不妨关注一下它的名字——归并,以下是百度汉语对于归并的解释。
归并,指合并;合在一起。语出《后汉书·广陵思王荆传》:“若归并二国之众,可聚百万。”
诚然,归并排序的一大特点就在于其排序过程中可以让两个有序数组合并。不过在理解的时候,仅仅以“合并”一词概括,显然忽略了归并排序的另外一大特征:分治。
Tip:我理解的分治思想多以递归形式完成,在归并排序中,多层分治递归可以同时进行。
以我所见,不妨将归并理解为:递归+合并。
1.首先,归并排序将乱序的数组分成若干两两匹配的子序列。该过程通过递归完成,自上而下将数组分成一半,再分成一半的一半,最后分成若干个仅有单个元素的小部分。
2.而后,将小的数组之间合并。该过程通过两个步骤完成,一是双指针排序,将两个有序的数组合并,二是自下而上的迭代。(合成两个小菠萝才能合成大南瓜,合成两个大南瓜才能合成大西瓜(老梗新用))
动图源自:CSDN博主 风继续吹TT
2.紧张刺激的代码实现
TIP:代码部分,借用ACWING算法课的归并排序模板。
1.Part 1:
先取一个紧张刺激的名字,然后确定传入的参数。
看了一些大佬写的代码,发现很多代码都在函数外部算好了中值再传入,在这里,我想顺带记录一下取中值的一些心得体会,故而将计算中值的操作放在函数内部进行。
大括号内的if语句用于判断子序列是否仅仅剩下一个元素。
void guibingpaixu(int *arr,int l,int r)
{
if (l >= r) return;
}
2.Part 2:
计算中值:
int mid = l + r >> 1;
学到这里不免心生疑惑,右移操作符与除以二有何区别?(可在编译器上用负数、正数、奇数、偶数进行实验)
实验用的代码与结果不予赘述,这里仅放上结果。
- n为非负数时,>> 1和/ 2的结果是一样的
- n为负数且还是偶数时,>> 1和/ 2的结果是一样的
- n为负数且还是奇数时,>> 1和/ 2的结果是不一样的
出现这样的结果是因为截断现象,学习整数除法的时候,我们知道用奇数除以二会向下取整,但实际上这样说并不准确,对于负奇数时,右移与除号呈现出了两种不同的结果,故而可以得出结论。
右移操作符为向下取整。
而除二,则为向零取整。
3.Part 3:
递归实现
guibingpaixu(arr, l, mid);
guibingpaixu(arr, mid + 1, r);
将数组对半分为一个个子序列,各层分治递归同时运行。
划分出左右区间,左区间为[1,mid] 右区间为[mid + 1, r]
4.Part 4:
归并过程
int k = 0, i = l, j = mid + 1;
//第一部分,将两个有序数列合并
while (i <= mid && j <= r)
if (arr[i] <= arr[j])
tmp[k++] = arr[i++];
else
tmp[k++] = arr[j++];
//第二部分,将剩余元素合并
while (i <= mid)
tmp[k++] = arr[i++];
while (j <= r)
tmp[k++] = arr[j++];
1.这一步定义了两个箭头,分别指向左右两个区间的元素,依次将两个有序的数组合并。
2.但,在合并到最后时,i和j其中之一到达边界,导致两个待合并的子序列中有一个序列并未完全合并,第二部分的两个while语句可以将剩余的元素按照顺序存入tmp数组之中。
5.Part 5:
for (i = l, j = 0; i <= r; i++, j++)
arr[i] = tmp[j];
将排序好的元素存入原本数组中。
作用:保证返回到上一层递归后两个子序列中的元素是有序的。
6.Part 6:
完整代码!!!好耶!!!
void guibingpaixu(int *arr, int l, int r)
{
if (l >= r) return;
int mid = l + r >> 1;
guibingpaixu(arr, l, mid);
guibingpaixu(arr, mid + 1, r);
int k = 0, i = l, j = mid + 1;
while (i <= mid && j <= r)
if (arr[i] <= arr[j])
tmp[k++] = arr[i++];
else
tmp[k++] = arr[j++];
while (i <= mid)
tmp[k++] = arr[i++];
while (j <= r)
tmp[k++] = arr[j++];
for (i = l, j = 0; i <= r; i++, j++)
arr[i] = tmp[j];
}
3.总结
本周学习内容很杂很乱,处于艰难的摸索阶段。我在各类教学资料之间徘徊不定,浮光掠影地学了这学了那,又似乎什么也没学到。偶然因为学长分享,学习了一直望而却步的归并排序,惊喜地发现竟然把这周学到的边界取值问题、快慢指针等知识点串联在一起,有种柳暗花明之感。可能这也是学习的乐趣之一吧。
参考文献:
ACWING算法课
【精选】【数据结构】八大排序(超详解+附动图+源码)_文件记录排序-CSDN博客
【算法】排序算法之归并排序 - 知乎 (zhihu.com)