从之前的学习可以看到,对大型vectory要求的排序,选择排序算法显然不符合要求,因为运行时间与输入问题规模大小的平方成比例增加,对于以线性顺序处理向量的元素的大多数排序算法也是如此。 所以要采用不同的方法来开发更好的排序算法。我们可以试着反过来思考。
强大的分治法(divide-and-conquer)
分治法的具体详见 C++抽象编程——递归简介(1)——递归范式 我们先来看看排序算法的性能为什么在问题规模增大后变得如此糟糕?我们之前分析过二次复杂度(即O(N^2)类)的基本特征是,随着问题的大小增加,运行时间增加了问题规模的两倍(比如问题规模增加2倍,那么运行时间要增加4倍)。 然而,反过来我们可以这样想。 如果将二次问题的大小除以2,则可以将运行时间减少相同的四倍。 也就是说我们可以将vector除以一半,然后应用递归的方法继续将问题的规模拆分,就可以成倍的减少所需的排序时间。
举个例子,假设你有一个很大的vector需要排序。如果将vector分成两半,然后使用选择排序算法对这些片段进行排序,会发生什么? 因为选择排序是的复杂度是二次的,每个较小的vector需要原始时间的四分之一(问题规模减少了2倍,时间就提高4倍)。 当然,你需要对这两半分别进行排序,但是排序两个较小vector所需的总时间仍然是排序原始vector所需的时间的一半。如果分开一个vector的两半可以简化整个vector排序的问题,我们将能够大大减少排序需要的总时间。更重要的是,一旦发现如何在一个地方提高性能,就可以使用相同的算法递归地对每一个进行排序。 为了确定分治法是否适用于这个排序问题,我们需要确定一个问题,即将vector分为两个较小的vector,然后对每个vector进行排序是否有助于解决一般问题(也就是拿一个实例来分析一下)。假设你从一个包含以下八个元素的vector开始排序:
如果将8个元素的vector划分为长度为4的两个vector,然后对每个较小的vector进行排序,就会得到下图:
现在我们需要从这些较小的vector中取出值,并将它们以正确的顺序放回到原始vector中。
合并两个vector
从较小的排序vector重组成完整的vector比排序本身要简单得多。这个过程我们称为合并(merging)。即完整排序中的第一个元素必须是v1中的第一个元素或v2中的第一个元素,以较小者为准。回到这个例子当中, 1. 我们新组成的的vector中的第一个元素是第二个vector(v2)中的第一个元素。然后将该元素添加到空的向量vec,此时我们把v2的19叉掉,表示已经取出,我们下图的结果
2. 再来一次,下一个元素只能是两个较小向量之一中的第一个未取出的元素。比较v1中的25与v2中的30,并选择前者:
3. 重复此过程,从v1或v2中选择较小的值,直到重构整个vector
合并排序算法
合并操作与递归分解相结合,产生了一种称为合并排序的新的排序算法,可以直接实现。 算法的基本思想可以概括如下:
1. 检查vector是否为空或只有一个元素。如果是这样,它肯定已经被排序。此条件用于定义递归的simple case。
2. 将vector分成两个较小的vector,每个vector的大小是前者的一半(意味着,不是值分成两个vector,而是每个分开的vector还可以继续分,重复这个过程)
3. 递归地对每个较小的vector进行排序。
4. 清除原始的vector,使其再次为空。(用来储存新的排序好的数字)
5. 将两个排序好的vector合并回原来的vector。
合并排序的C++代码
合并排序思路简单,但是实现起来并不那么容易,下面是本人写的C++代码,在VS2015中编译通过:
/*
运行效果如图:
合并排序算法的代码可以整齐地分为两个函数:排序和合并。 排序代码直接来自算法的步骤。在检查特殊情况后,算法将原始vector分为两个较小的v1和v2。一旦sort代码将所有元素复制到v1或v2中,v1,V2就已经被创建,其余的函数会递归地排序这些vector,最后清除原始vector,然后调用merge来重新组合vector,从而实现合并排序。 实际上大部分的工作是通过合并函数完成的,该函数采用目标vec,以及较小的向量v1和v2。指标p1和p2标记跟踪每一个vector的下标。 在循环的每个循环中,该函数从v1或v2选择一个元素取较小者,并将该值添加到vec的末尾。一旦两个较小的vector中的任何一个的元素被取尽,该函数可以简单地从另一个vector中直接复制元素而再比较它们。实际上,因为这些向vector中的其中一个已经在第一个while循环退出时已经耗尽,所以该函数可以将vector的其余部分复制到vec。 其中一个vector为空,相应的while循环将完全不执行。
这里说一下,v1[p1++],其实我们都知道 i++返回的 i的值是自增1的。但是这个运算符返回的是自增前的值。也就是说比如 i = 2,执行
i++;
之后,就是 i = 3,但是 (i++)这个整体的值就还是 2(可以写个程序试试)。 所以说
v1[p1++]
这句代码等价于:
v1[p1]; p1 ++;
下一篇的文章我们就去分析一下这个算法的复杂度