vlambda博客
学习文章列表

02 算法推送--归并排序

 注:由于考虑到上两周同学们都有三门以上的期中考试,为了帮助同学们更好地复习期中考(绝对不是作者也在复习期中),本系列断更了一期。考虑到各种不确定因素,要不以后就改成不定期更新了吧(暴论x)


0.上周思考题

你在考场上绞尽脑汁地思考着这题:“我不知道最优情况下的路标设置,因此很难正面直接求出相邻路标间的最大距离……”

突然邻桌的同学啪地一声就站起来了,很快啊!然后上来就大喊一声:“思考题的答案是37!”

考场的老师大意了啊,没拦住,同学们百脸懵逼,只有你冷静下来仔细思考:“他说的答案是最优的吗?”

要验证这个答案是不是最优的,理论上我们得证明两件事:①存在一种方案能使得这个最小距离存在②不管用什么方案,这个距离都不可能更小了

事实上我们很容易解决第一个问题:如果我们只要使得每两块相邻的石头之间距离不大于37,那么很容易想到,每隔37的距离放一块石头一定是最优的……这就意味着,我能在O(n)的时间内去验证37是否可行。

“太棒了!我只要对于每一个距离都用这样的方法去检验一遍,那么最小的检验成功的距离就是最后的答案!”,你这样想。但是理性告诉你,如果你对每一个距离都去检验一遍,那么这和“考场上有一千万名同学(如果有这么多人)站起来依次报出不同的答案,然后你一个个去检验”有什么区别呢?

临近下考,你在极度愤怒的情况下突然想起了上次推送的标题——“二分!”……


 注:

①由于我知道当假定距离为x时,存在这样的方案,那么答案就一定会小于等于x,类似于上一次推送的方法,我们可以在进行O(logL)个检验后就得到答案。结合我们使用一次检验的复杂度是O(n),故我们解决这道题目的总体复杂度是O(nlogL)

②这个方法被算法竞赛选手称为“二分答案”,顾名思义就是对答案进行二分。这其实是对二分方法的一种拓展:二分不仅可以用作“序列上查找”,它还能在答案轴上二分、在时间轴上二分、一堆问题一起在答案轴上二分、一堆操作一起在时间轴上二分……

③上述的几种方法在算法竞赛中分别称为:“二分答案”、N/A、“整体二分”、“时间轴分治/CDQ分治”。感兴趣的同学可以自行搜索了解。



引入(归并排序)


02 算法推送--归并排序



     张三是T大微积分A课程的一名助教。由于工(张)作(三)量(在)巨(摸)大(鱼),他连熬了几天夜才判完全班的卷子。现在,筋疲力尽的张三终于得到了300名同学的成绩。不过还有最后一个问题:老师让他把成绩排序之后再发过去。然而张三只有一台祖传的土豆电脑,需要1秒才能完成一次普通的运算。同时,土豆电脑很不稳定,时刻需要张三在边上守着。张三只会冒泡排序算法,这意味着他要在电脑前等待几万秒。张三考虑到自己已经好几天没合眼了,就找到了你,让你提供一种高速的排序算法,在一个小时内解决问题,以防止自己在等待时猝死。由于你20分的平时分完全取决于张三的脸色,你高(被)兴(迫)地接下了这个工作……



      那么,你打算怎么解决这个问题呢?如何让排序 n 个数字所需的运算量显著地少于 n 2 次呢?你无从下手,只能去回顾这两周的程设作业,试图找到一点思路……

一、 回顾程设作业‍

02 算法推送--归并排序

    这道题目提示你,可以用 n次操作 ,把两个长度为 n/2 有序序列 合并成一个长度为 n 有序序列 。你也许会觉得这些序列已经排好序了,和张三的排序任务关系不大。
    于是你决定再看一道作业题(重点在注释部分):
02 算法推送--归并排序
这道题目告诉你,可以将一个复杂的问题 分成两半 分别求解 然后汇总成最终答案。这种思想与递归结合,可以简化很多问题。这时,关于合并有序序列的结论突然有用了起来。于是,你有了一个 大胆的 想法……

二、 设计排序算法‍


你想到了这样一种递归的排序算法——将一个序列从中间切成两半,分别排序,然后将所得序列合并得到答案,这个算法思路并不难,你高兴地写下了算法的伪代码。伪代码如下:

SORT (LEFT, RIGHT)          

//将序列从LEFT号元素到RIGHT号元素排序

{

MID=(LEFT+RIGHT)/2    //找到序列中间点

SORT (LEFT, MID) //给左半部分排序

SORT (MID+1, RIGHT)   //给右半部分排序

MERGE (LEFT, RIGHT)   //将两部分合并成答案(与作业题方法相同)

 }

那么你是怎么知道这个算法在速度上优于冒泡排序呢?你画图将无序数列   14,12,15,13,11,16   的排序过程作为例子分析。图片如下:
(图片来源:知乎@developer1024)

通过分析伪代码可以看出,运算主要发生在 合并有序序列的过程 (即 归并 )中。图中每一层递归都需要 整合总长度为6的若干个子序列 。而递归的层数为 对log 2 6上取整(即3层,想一想为什么是log的底数为2) 。利用前面关于合并有序序列的结论,你粗略估计给6个数字排序需要 6*log 2 6 次运算。
n=6 的小数据上,你的算法与需要 n 2 次运算的冒泡排序拉不开什么差距(甚至因为递归会更慢),但是对于张三面临的 n=300 量级时就有明显差别了。 n=300 时,你算出 n 2 =90000 ,而 n*log 2 n=2700 (计算中对log 2 n上取整)。在张三的土豆电脑上,前者耗时大于一天(一天为86400秒),而后者在一个小时内就可以轻松搞定。
至此,你完美地解决了张三的难题,让他成功避免了ICU一日游,在完成为祖国健康工作50年的目标上更进一步;而你自己,也稳稳地拿到了20分平时分,说不定张三还会在期末考试前,帮助你精准复习……   :D

结语:
你得到的这种高效排序算法,就是经典的 归并排序 ,是一个典型的应用 分而治之思想( Divide and Conquer 以提高效率的算法。在一般的OJ题目上,如果需要排序的数据量 n为10 4 量级 ,就得用这种高速的排序防止TLE了。

三、 思考题‍

1.  为了书写和理解方便,你在伪代码中采用的是递归写法。不过你想起来程设课上老师说过,递归函数的效率往往低于迭代(即循环)。能不能把归并排序写成迭代形式,进一步提升效率呢?
提示:写完了可以用 洛谷P1177【模板】快速排序 来测试自己的程序。

2.  洛谷P1908逆序对
提示1:题目数据量大,用scanf输入比cin快,可以避免TLE。
提示2:数据量较大,可以定义大小为500000的 全局变量数组 。在main函数中定义这么大的数组可能导致暴毙。如果使用递归法, 请不要把数组当成函数参数传来传去 ,同样会暴毙 如需使用请传递下标或指针)

3.  还有一种比归并排序 在一般情况下) 还要快一点点的排序方法——快速排序。程设课本上有快速排序算法,可以预习并比较快速排序与归并排序的异同。
作者:叶舟桐
校对&思考题解答:袁桢淏
编辑:孟子盛