一、分而治之
一种著名的递归式问题解决方法。英文全称(divide and conquer),简称D&C,它提供了解决问题的思路。
使用D&C解决问题的过程需要两个步骤:
1、找出基线条件,这种条件必须尽可能简单;
2、不断将问题分解(或者说缩小规模),直到符合基线提交。(递归条件)
举例:假如你有一块土地,它的长是1680米,宽是640米;你要将这块地均匀分成方块,且分出的方块要尽可能大。
根据D&C解决问题的两个步骤:
基线条件:最容易想到的就是一条边的长度是另一条边的整数倍;如果一个边长是25m,另一个边长是50m,那么可用的最大方块就是25m*25m。
递归条件:每次递归调用都必须缩小问题的规模,直到符合基线条件。所以按照这个思路,我们可以对这块地进行第一次的切割,640m*640*,余下640m*240m,下次再以240m为边长进行切割,依次类推,最终发现80m*80m就是我们最终要确认的方块大小。
在这里有一个很重要的概念,欧几里得算法:即适用于这小块地的最大方块,也是适用于整块地的最大方块。
小结:
1、找出简单的基线条件;
2、确定如何缩小问题的规模,使其符合基线条件;
3、编写涉及数组递归时,基线条件通常为:数组为空或只包含一个元素。
二、快速排序
快速排序是对冒泡排序的一种改进。
它的基本思路是:通过一次排序将 要排序的数组分成两个部分,其中一部分所有数据都比另一部分所有数据都要小,然后再依次对这两部分数据进行快速排序,整个排序过程可以递归进行,以达到整个数据变成有序的序列。
先来看一个示例,使用快速排序对数组L[3,2,5,9,4,0,7]进行排序。
既然快速排序应用到递归,我们可以用上节中D&C方法来解决问题。想想上节的递归解决方法:
第一步:确认基线条件
先不要受给出示例的影响,想一想,对排序算法来说,最简单的数组是什么样呢?当然就是压根就不需要排序的数组呗,也就是空数组或只包含一个元素的数组啦~
因此,第一步的基线条件为:len(L)<2,这种情况就包括了空数组和只有1个元素的数组。
第二步:不断将问题分解(缩小问题的规模),直到满足基线条件;
于是我们开始分解列表元素大于等于2种的情况,基于D&C的解题思路中不断缩小问题的规模:
所以,我们假设这个数组只有2个元素:如果第一个元素比第二个元素大,就交换他们的位置;
如果是3个元素呢,还是应用D&C的解决思路,此时我们可以任意从数组中选择一个元素作为基准值,接下来,就找出比基准值小的元素和比基准值大的元素;那么现在,我们是不是就有了一个比基准值小的子数组;基准值;一个比基准值大的子数组;然后我们再对子数组进行快速排序,你发现没有,不管何种情况,两个子数组最多只含有2个元素;2个元素的数组排序问题我们已经解决了,此处就是递归的方式,最终我们将这它们串联到一起:[比基准值小的子数组]+[基准值]+[比基准值大的子数组],最终就会形成一个有序的数组。
总结如下,这也是快速排序的核心步骤:
1、选择基准值;
2、将数组分成两个子数组:小于基准值的元素和大于基准值的元素;
3、对这两个子数组进行快速排序(也就是重复第2步)。
上述完整代码如下:
至此,你发现一个规律吗?(如果感兴趣,可以看下数学归纳法,也称归纳证明)
如果一个快速排序对包含一个元素的数组管用,对包含两个元素的数组也将管用;如果对包含两个元素的数组管用,对包含三个元素的数组也将管用;以此类推!因此,可以看出,快速排序对任何长度的数组都管用。
上述代码一种更优雅的写法:
细心的你,发现上述两种写法更深一层的意义了吗?
是的,两种写法最关键的区别就是选择的基准值不一样,所以说快速排序的性能依赖与你选择的基准值。
三、平均情况和糟糕情况
示例:一个有序序列[1,2,3,4,5,6,7,8]
假设你总将第一个元素作为基准值,这使得调用栈的高度为8,而如果你总是将数组中间的元素作基准值,调用栈的高度为4。
第一种方法为糟糕情况,此时栈长为O(n);第二种方法为最佳情况,此时栈长为O(log n);而不管如何划分数组,每次所有的时间都是O(n),即最糟情况下所用的时间为O(n)*O(n)=O(n²);而最佳情况下所用时间为O(log n)*O(n)=O(n log n),这里要说明的是,最佳情况即平均情况。