1. 程式人生 > >Python-遞歸函數

Python-遞歸函數

引用 else 函數 urn pyc list end 次數 lose

閱讀目錄:

  1、函數執行流程 

  2、遞歸Recursion

  3、遞歸練習

內容:

1、函數執行流程 

 http://pythontutor.com/visualize.html#mode=display # 在這個網站可以輸入代碼,查看演示
或者pycharm debug查看frames
 1 def foo1(b, b1=3):
 2     print(foo1 called, b, b1)
 3 def foo2(c):
 4     foo3(c)
 5     print(foo2 called, c)
 6 def foo3(d):
 7     print(
foo3 called, d) 8 9 def main(): 10 print(main called) 11 foo1(100, 101) 12 foo2(200) 13 print(main ending) 14 15 main() 16 ------------------------------------------------- 17 main called 18 foo1 called 100 101 19 foo3 called 200 20 foo2 called 200 21 main ending

 對上面的執行流程做一個簡單的描述:

    1. 全局幀中生成foo1,foo2 ,foo3,main函數對象
    2. main函數調用
    3. main中查找內建函數print 壓棧,將常量字符串壓棧,調用函數,彈出棧頂
    4. main 中全局查找函數foo1 壓棧,將常量100 ,101 壓棧,調用函數foo1 ,創建棧幀。print函數壓棧,字符串 和 變量 b, b1 壓棧,調用函數,彈出棧頂,返回值。
    5. main中全局查找foo2 函數壓棧,將常量200,壓棧,調用foo2,創建棧幀。foo3 函數壓棧,變量c引用壓棧,調用foo3,創建棧幀,foo3 完成print 函數調用後返回,foo2 恢復調用,執行print後,返回值,main 中foo2 調用結束彈出棧頂,main 繼續執行print 函數調用,彈出棧頂,main返回。

技術分享圖片

圖1,函數讀取到內存中

技術分享圖片

圖2:執行main函數時,調用foo2(200)時候的棧幀圖

  註:

    • 函數執行 要壓棧,函數內執行函數,內層函數要落在外層函數上面。
    • 函數結束要彈出,包括函數需要的參數等。
    • 棧跟線程相關
      • 定義一個函數,兩個線程分別調用該函數,各自在各自的棧空間,每個線程互不影響。
    • 所以從這裏可以看到局部變量 local 函數調用時創建,調用結束消亡(調用時要壓棧,結束時彈出,之後局部變量引用不到了)

  註:

 1 def fn(a):
 2     print(a)
 3 
 4 g = [1]
 5 
 6 fn(g)
 7 
 8 # 彈出消亡只是 a,並不是g,如果引用類型,只是 地址,如上面
 9 # 事實上,g=【1】是放堆裏的,開辟內存空間,受到GC管理
10 # 棧只是放引用地址

  

2、遞歸Recursion

  • 函數直接或者間接調用自身就是遞歸
  • 遞歸需要有邊界條件,遞歸前進段,遞歸返回段。
  • 遞歸一定要有 邊界條件
  • 當邊界條件不滿足的時候,遞歸前進
  • 當邊界條件滿足的時候,遞歸返回。   

  2.1:斐波那契數列:1,1,2,3,5,8,13......  

技術分享圖片
 1 ---------------------斐波那契數列
 2 # no1 最初始的版本 使用for循環完成,交換
 3 def fib(n):
 4     a = 0
 5     b = 1
 6     for i in range(n):
 7         a, b = b, a + b
 8     return a
 9 print(fib(3)) # 2
10 
11 # no2.1 使用遞歸完成,但是這個版本的 遞歸效率極低,通過 ipython的jupyter 的%%timeit測試, 還不如循環。因為return的fib(n-1)+fib(n-2),d都要分別往下繼續執行,數據量大的時候,這個相當於重復了很多,前面算了的,後面fib(n-2)裏還要算一次,所以效率極低。
12 def fib(n):
13     if n < 3:
14         return 1
15     return fib(n-1) + fib(n-2)
16 
17 print(fib(3)) # 2
18 
19 # no2.2 變形版本:
20 def fib(n):
21     return 1 if n < 3 else fib(n-1) + fib(n-2)
22 
23 print(fib(3)) # 2
24 
25 # no 3 利用上次的結果的遞歸
26 # 這裏使用 缺省參數作為運算結果,而n 作為循環次數控制量
27 def fib(n, a=0, b=1):               def fib(n, a=0, b=1):                   def fib(n, a=0, b=1):
28     if n == 0:                          if n == 0:                               if n == 0:
29         return a                             return a                               return a
30     a, b = b, a + b                     a, b = b, a + b                           a, b = b, a + b
31     return fib(n-1, a, b)              return fib(n-1, a, b)                     return fib(n-1, a, b)
32 
33 print((fib(3)))
斐波那契數列遞歸以及最初版本實現

技術分享圖片

  2.2:各個解釋器對針對遞歸的保護機制:到達這個數據,就會拋異常,不管你是不是還要進行!

    CPython中:   

      import  sys
      print((sys.getrecursionlimit())) # 1000

    IPython中: 是3000

    註: 但是事實上是不夠這個數字,因為當前棧不可能只有自己的函數,可能包含其他的數據,基礎性數據等。會占一部分棧空間。

    遞歸要求:

      • 遞歸一定要有退出條件,遞歸調用一定要執行到這個退出條件。沒有退出條件的遞歸調用,就是無限調用。
      • 遞歸調用的深度不宜過深
        • Python對遞歸調用的深度做了限制,以保護解釋器
        • 超過遞歸深度限制,拋出 RecursionError:maxinum recursion depthexceeded 超出最大深度
        • sys,getrecursionlimit()

    遞歸性能:

      • for循環,遞歸兩者的性能對比,事實上,for循環的性能比遞歸性能要強,尤其數據量大的時候。
      • 循環稍微復雜一些,但是只要不是死循環,可以多次叠代直至算出結果
      • fib 函數代碼簡潔易懂,但是只有獲取到嘴歪層的函數調用,內部遞歸結果都是中結案結果,而且給定一個n都要進行2n次遞歸,深度 越深,效率月底,為了獲取斐波那契數列需要外面在套一個N次的循環,效率就更低了,但是for循環的空間復雜度比較高。
      • 遞歸還有深度極限,如果遞歸復雜,函數反復壓棧,棧內存很快就溢出了。   

    節間遞歸:

        def foo1():

          foo2()

        def foo2():

          foo1()

        foo1()

        間接遞歸,是通過別的函數調用了函數本身

        但是,如果構成了循環遞歸調用時非常危險的,但是往往這種情況在代碼復雜的情況下,還是可能發生這種調用,要用代碼的規範來避免這種 遞歸調用的發生。

    遞歸總結:

      • 遞歸是一種很自然的表達,符合邏輯思維
      • 遞歸相對運行效率低,每一次調用函數都要開辟棧幀,每次要壓棧,但是效率比較低。
      • 遞歸有深度限制,如果遞歸層次太深,函數反復壓棧,棧內存很快就溢出了。
      • 如果是有限次數的遞歸,可以使用遞歸調用,或者使用循環替代,循環代碼稍微復雜些,但是只要不是西孫煥可以多次叠代直至算出結果。
      • 絕大多數遞歸,都可以使用循環實現。
      • 線上,能不用則不用。

3、遞歸練習

https://www.cnblogs.com/JerryZao/p/9531951.html

Python-遞歸函數