本章的重点是学习一种著名的递归式问题解决方法——D&C(divide and conquer),和一个重要的D&C算法,快速排序。
D&C
D&C解决问题的过程包括两个步骤:
- 找出基线条件,这种条件必须尽可能简单
- 不断将问题分解(或者说缩小规模),直到符合基线条件
例:给定一个数组 [2, 4, 6],使用递归的方式完成求和
-
找出基线条件 当传入的数组的长度为1或者0时,计算总和将非常容易
-
缩小规模 每次抽出数组中的一个数参与计算,达到缩小规模
def sum(list):
length = len(list)
if length == 0:
return 0
elif length == 1:
return list[0]
num = list.pop(0)
return num + sum(list)
例:用递归完成二分查找
-
找出基线条件 当中间下标等于目标值的时候或者剩余数组只剩下一个的时候就是目标值
-
缩小规模 与目标值比较之后,截取符合的数组放入递归
def binary_query(list, target):
length = len(list)
if length == 1: # 基线条件 list如果只剩一个数了,一定是目标数
return list[0]
if length == 0: # 基线条件 list传的空进来,一定不存在目标数
return None
mid = (length - 1) // 2 # 计算出来中间下标
if list[mid] == target: # 基数条件 list的中间数 == target 即目标数
return list[mid]
if list[mid] > target: # 缩小规模,截取数组
_slice = slice(0, mid)
elif list[mid] < target: # 缩小规模,截取数组
_slice = slice(mid + 1, length)
return binary_query(list[_slice], target)
print(binary_query(range(0, 100), 99))
快速排序
快速排序也是一种常用算法,比选择排序快得多,快速排序也使用了D&C。
先考虑最简单的数组是什么样的呢?空数组或者是只包含一个元素的数组,我们只需要原样返回这个数组就完成了排序。
接下来我们看一个长一点的数组。对包含两个元素的数组进行排序,只需要验证第一个元素与第二个元素的大小,然后决定是否需要更换位置,我们就完成了两个元素的数组排序。
接下来考虑更长一点的数组,3个元素的数组。
这里我们需要使用D&C的思路去考虑这个问题,需要把数组分解,直到满足基线条件。
快速排序的原理简单解释就是:选择一个基准值(pivot),找出比基准值小的元素放入一个新数组,找出比基准值大的放入另外一个新数组。这被称为分区。
接下来,就是递归。重复最后一步,直到基线条件。
最后拼接数组,完成快速排序。
talk is cheap, show me the code
JavaScript实现:
function quickSort(arr) {
if(arr.length < 2) {
return arr;
} else {
const pivot = arr[0];
const pivotArr = [];
const lowArr= [];
const hightArr = [];
arr.forEach(current => {
if(current === pivot) pivotArr.push(current);
else if(current > pivot) hightArr.push(current);
else lowArr.push(current);
})
return quickSort(lowArr).concat(pivotArr).concat(quickSort(hightArr));
}
}
python实现
def quicksort(array):
if len(array) < 2:
return array
else:
pivot = array[0]
less = [i for i in array[1:] if i <= pivot]
greater = [i for i in array[1:] if i > pivot]
return quicksort(less) + [pivot] + quicksort(greater)
快速排序的运行时间平局情况是O(n * log n),糟糕情况是O(n^2)。
小结
- D&C将问题逐步分解。使用D&C处理列表时,基线条件很可能是空数组或只包含一个元 素的数组。
- 实现快速排序时,请随机地选择用作基准值的元素。快速排序的平均运行时间为O(n log n)。
- 大O表示法中的常量有时候事关重大,这就是快速排序比合并排序快的原因所在。
- 比较简单查找和二分查找时,常量几乎无关紧要,因为列表很长时,O(log n)的速度比O(n) 快得多。