遞歸的邏輯(3)——遞歸與分治

  遞歸和分治天生就是一對好朋友。所謂分治,顧名思義,就是分而治之,是一種相當古老的方法。

  在遙遠的周朝,人們受生產力水平所限,無法管理龐大的土地和衆多的人民,因此採用了封邦建國的封建制度,把土地一層一層劃分下去,以達到分而治之的目的,這也許是最古老的分治法了:

分治的步驟

  正像分封土地一樣,分治法的目的就是爲了把無法解決的大問題分解成若干個能夠解決小問題。通常來說,分治法可以歸納爲三個步驟:

  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位的

  出處:http://www.cnblogs.com/bigmonkey

  本文以學習、研究和分享爲主,如需轉載,請聯繫本人,標明作者和出處,非商業用途! 

  掃描二維碼關注公衆號“我是8位的”

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章