递归和分治天生就是一对好朋友。所谓分治,顾名思义,就是分而治之,是一种相当古老的方法。
在遥远的周朝,人们受生产力水平所限,无法管理庞大的土地和众多的人民,因此采用了封邦建国的封建制度,把土地一层一层划分下去,以达到分而治之的目的,这也许是最古老的分治法了:
分治的步骤
正像分封土地一样,分治法的目的就是为了把无法解决的大问题分解成若干个能够解决小问题。通常来说,分治法可以归纳为三个步骤:
1. 分解,将原问题分解成若干个与原问题结构相同但规模较小的子问题;
2. 解决,解决这些子问题。如果子问题规模足够小,直接求解,否则递归地求解每个子问题;
3. 合并,将这些子问题的解合并起来,形成原问题的解。
化质为量
分治的基本思想就是化质为量,把“质”的困难转化成“量”的复杂。其实化质为量的思想不仅仅用在分治上,来看一个很难处理的积分,正态分布进行积分:
常规的方法很难处理。现在,由于被积函数与ex相似,我们又已经知道et 的泰勒展开式,所以可以进行下面的变换:
左右两侧同时积分:
右侧的积分就是化质为量的意义所在。展开前求解函数的值很困难,展开后是幂级数,虽然有很多很多项,但是每一项都是幂函数,都很容易求解,于是,只要对展开后的函数求和,就能得到展开前的函数的值。
快速排序
分治法的典型应用当属快速排序。假设有a是一个存储了N个不同整数的乱序数组,快速排序将把数组分成两个部分,然后对每个部分进行独立排序。快速排序的关键是递归划分过程,每一次划分都会把某个元素放到位,使比它小的元素都在左边,比它大的都在右边,然后递归地对左右两侧的元素进行排序:
1 def quick_sort(a, l, r): 2 ''' 快速排序 ''' 3 if l < r: 4 m = partition(a, l, r) 5 quick_sort(a, l, m - 1) 6 quick_sort(a, m + 1, r)
如果数组中仅有一个或更少的元素,则什么都不做;否则使用partition方法来处理数组,它将把a[m]归位,将其放在l和r之间的某个位置上,然后以m为分界,递归地对m两侧的元素进行快速排序。
def partition(a, l, r): ''' 划分过程 ''' i, j, v = l, r - 1, a[r] while True: while a[i] < v: i += 1 while a[j] > v: if j == i: break j -= 1 if i >= j: break a[i], a[j] = a[j], a[i] a[i], a[r] = a[r], a[i] return i
构造一个乱序数组
1 def create(N): 2 ''' 构造一个存储N个数字的乱序数组 ''' 3 a = np.arange(N ) 4 np.random.shuffle(a) 5 return a[0:N]
在partition中,v存储了数组最右侧的元素,i和j分别是数组的左右下标,每一次循环都扫描左右下标,通过交换让i左侧的元素都比v小,j右侧的元素都比v大,直到两个下标相遇,最后把v放到第i个位置上:
找出第n大的值
仍然是乱序数组,现在问题是直接回答数组中第n大的值,一种思路是先将数组排序,然后返回a[n-1],这多少有些麻烦,能否不经过排序直接回答问题呢?当然可以,只要保证a[n-1]的左侧的元素都比它小,右侧的元素都比它大就好了,并不需要管a[n-1]的左右两侧是否是乱序。这实际上是“未完成”的快速排序:
1 def find_nth(a, n, l, r): 2 ''' 找到第n大的数''' 3 if l < r: 4 m = partition(a, l, r) 5 if m == n - 1: 6 return m 7 elif m < n - 1: 8 return find_nth(a, n, m + 1, r) 9 else: 10 return find_nth(a, n, l, m - 1)
直尺上的刻度
考虑一个在直尺上画刻度的问题,在尺子的1/2处画一个刻度,1/4处画一个稍短的刻度,1/8处画一个更短的刻度……直到要求的最小刻度为止。
这符合分治的策略,可以很容易编写出下面的代码:
1 def ruler(l, r, h): 2 ''' 3 画出刻度尺,适用于 r - l = 2^n的情况 4 :param l 左半部分的刻度值 5 :param r 右半部分的刻度值 6 :param h 刻度线的高度 7 ''' 8 m = (l + r) // 2 9 if h > 0: 10 ruler(l, m, h - 1) 11 mark(m, h) 12 ruler(m, r, h - 1)
参数中的l和r表示直尺左右端点的刻度值,h表示每一次递归所画刻度的高度,它的初始值满足2h=r-l。为了能在直尺中间画刻度,我们规定l+r总是能够被2整除。首先把直尺分成相等的两部分,在左半部分画一个稍短的刻度,然后在中间画一个长一点的标记,再在右半部分画一个稍短的刻度,如此递归下去。为一个长度为8的刻度尺标记刻度时产生的递归顺序:
可以让mark仅负责存储刻度信息,之后用paint()画出刻度尺:
1 import numpy as np 2 import matplotlib.pyplot as plt 3 import matplotlib.patches as mpathes 4 from matplotlib.lines import Line2D 5 6 def mark(m, h): 7 marks.append([(m, 0), (m, 0.2 * h)]) 8 9 def paint(): 10 fig, ax = plt.subplots() 11 # 添加没有刻度的矩形直尺 width=8, height=1 12 xy = np.array([0, 0]) 13 rect = mpathes.Rectangle(xy, 8, 1, fill=False) 14 ax.add_patch(rect) 15 16 # 标记刻度 17 ruler(0, 8, 3) 18 19 # 绘制刻度 20 for line in marks: 21 (line1_xs, line1_ys) = zip(*line) 22 ax.add_line(Line2D(line1_xs, line1_ys, linewidth=1, color='blue')) 23 24 plt.axis('equal') 25 plt.show() 26 27 if __name__ == '__main__': 28 # 刻度线 29 marks = [] 30 paint()
最终,paint()方法会画出一把漂亮的刻度尺:
作者:我是8位的