連載|想用Python作自動化測試?遞歸函數

 遞歸函數就是函數內部調用自身,可使代碼邏輯更加易懂。可是遞歸也有坑,須要避免。
python


13.1 概念

  • 在函數內部,能夠調用其餘函數。若是一個函數在內部調用自身,這個函數就是遞歸函數。bash

  • 理論上,全部的遞歸函數均可以寫成循環的方式,但循環的邏輯不如遞歸清晰。微信

計算階乘n! = 1 x 2 x 3 x ... x n,用函數fact(n)表示:編輯器

def fact(n): if n==1: return 1 return n * fact(n - 1)

13.2 寫遞歸代碼的套路

寫遞歸代碼的關鍵就是找到如何將大問題分解爲小問題的規律,而後按照下面套路便可實現:ide

  • 第一步,寫出遞推公式函數

以計算階乘爲例,遞歸公式是:fact(n)=n!=(n−1)×⋅⋅⋅3×2×1=n×(n−1)!=n×fact(n−1)工具

  • 第二步,推敲終止條件測試

以計算階乘爲例,終止條件是n=1時,fact(1)=1。ui

13.2.1 斐波那契數列

再來看一個斐波那契數列的例子,斐波那契數列中後一個元素是前兩個相鄰元素的和。好比:spa

0,1,1,2,3,5,8,13,21,34,55,…。

那麼咱們如何獲得第n個數是多少?分兩步走:

第一步,寫出遞推公式。求第n個元素,能夠先求出n-1和n-2個元素的值,而後再將這兩個求和,因此公式是:

fibonacci(n) = fibonacci(n - 1) + fibonacci(n - 2)

第二步,推敲最終終止條件。終止條件包含三個:n=0時,f(n)=0;n=1時,f(n)=1;n=2時,f(n)=1。

if n < 1: # 遞歸終止條件 return 0if n in [1, 2]: # 遞歸終止條件 return 1

轉換成完整代碼就是:

def fibonacci(n): if n < 1: # 遞歸終止條件 return 0 if n in [1, 2]: # 遞歸終止條件 return 1  return fibonacci(n - 1) + fibonacci(n - 2) # 遞歸公式

不論是編寫遞歸仍是閱讀遞歸代碼,只要遇到遞歸,咱們就把它抽象成一個遞推公式,不用想一層層的調用關係,不要試圖用人腦搞清楚計算機每一步都是怎麼執行的。

13.2.2 n 個臺階有多少種走法

再來看看一個例子,假若有 n 個臺階,每次能夠跨 1 個臺階或者 2 個臺階,請問走這 n 個臺階有多少種走法?

咱們從第一步開始想,若是第一步跨1個臺階,問題就變成了n-1個臺架有多少種走法。若是第一步跨2個臺階,問題就變成n-2個臺階有多少種走法。咱們把n-1個臺階的走法和n-2個臺階的走法求和,就是n個臺階的走法。用公式表示就是f(n)=f(n-1)+f(n-2)。這就是遞歸公式了。

再來看看終止條件,最後1個臺階就不須要再繼續遞歸了,只有一種走法,就是f(1)=1。咱們把這個放到遞歸公式裏面看下,經過這個終止條件可否求出f(2),發現f(2)=f(1)+f(0),也就是僅知道f(1)是不能求出f(2)的,所以要麼知道f(0)的值,或者直接將f(2)做爲一個遞歸終止條件。f(0)表示0個臺階有幾種走法,f(2)表示2個臺階有幾種走法。明顯,f(2)更容易理解一些。因此定爲f(2)=2也是一個終止條件,表示最後2個臺階有兩種走法,即一次跨1個臺階和一次跨2個臺階。有了f(1)和f(2),就能求出f(3),進而求出f(n)了。

轉化成代碼便是:

def walk(n): if n == 1: # 遞歸終止條件 return 1 if n == 2: # 遞歸終止條件 return 2 return walk(n - 1) + walk(n - 2) # 遞歸公式

13.3 遞歸可解決哪類問題

  • 原始問題的解能夠分解爲幾個子問題的解

  • 原始問題和子問題,只有數據規模的不一樣,求解思路徹底同樣

  • 存在遞歸終止條件

13.4 遞歸存在的問題

  • 堆棧溢出

  • 重複計算

編寫遞歸代碼時,咱們會遇到不少問題,比較常見的一個就是堆棧溢出,而堆棧溢出會形成系統性崩潰,後果會很是嚴重。什麼是堆棧溢出呢?

函數調用會使用棧來保存臨時變量。每調用一個函數,都會將臨時變量封裝爲棧幀壓入內存棧,等函數執行完成返回時,再出棧。系統棧或者虛擬機棧空間通常都不大。若是遞歸求解的數據規模很大,調用層次很深,一直壓入棧,就會有堆棧溢出的風險。

能夠經過Pycharm工具查看調用棧的狀況。在遞歸公式那行代碼上添加斷點,不斷執行Step Over,能夠看到Frames窗口中的棧信息會不斷增長和減小,當調用一次函數會增長一幀,當調用返回後會減小一幀。最後返回第一層棧func.py。前面說的堆棧溢出的風險,體如今Frames窗口中的棧幀太多了。

那麼,如何避免出現堆棧溢出呢?

一般能夠在代碼中限制遞歸調用的最大深度的方式來解決這個問題。好比Python語言,限制了遞歸深度,當遞歸深度太高,則會拋出:RecursionError: maximum recursion depth exceeded in comparison異常,防止系統性崩潰。

咱們在代碼中也能夠本身設置遞歸的深度,好比限制n最大不能超過100,代碼以下:

def walk(n): if n == 1: return 1 if n == 2: return 2 if n > 100: raise RecursionError("recursion depth exceede 100") return walk(n - 1) + walk(n - 2)

除此以外,使用遞歸時還會出現重複計算的問題。什麼意思?拿走臺階那個例子來講明。好比計算6個臺階的走法f(6),過程以下圖:

從圖中,咱們能夠直觀地看到,想要計算 f(5),須要先計算 f(4) 和 f(3),而計算 f(4) 還須要計算 f(3),所以,f(3) 就被計算了不少次,這就是重複計算問題。

那麼怎麼解決這個問題?爲了不重複計算,咱們能夠經過字典保存已經求解過的 f(k)。當遞歸調用到 f(k) 時,先看下是否已經求解過了。若是是,則直接從字典中取值,不須要重複計算,這樣就能避免剛講的問題了。

修改下計算臺階走法的代碼,解決重複計算的問題:

data = dict() # 保存中間結果

def walk(n): if n == 1: return 1 if n == 2: return 2 if n > 100: raise RecursionError("recursion depth exceed 100") if n in data: # 若是在中間結果中,則直接返回,不用進入遞推公式再次計算 return data[n] result = walk(n - 1) + walk(n - 2) # 在遞歸公式前面增長個查找步驟 data[n] = result # 將計算結果保存在中間結果data字典中 return result

print(walk(6))


本文分享自微信公衆號 - 明說軟件測試(liuchunmingnet)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索