python算法遞歸於尾遞歸!

2020-12-11 小熊百知

遞歸概念

遞歸:程序調用自身的編程技巧稱為遞歸( recursion)。用一種通俗的話來說就是自己調用自己,它通常把一個大型複雜的問題層層轉化為一個與原問題相似的、但是規模較小的問題來求解,當問題小到一定規模的時候,需要一個遞歸出口返回。遞歸策略只需少量的程序就可描述出解題過程所需要的多次重複計算,大大地減少了程序的代碼量。遞歸的能力在於用有限的語句來定義對象的無限集合。

遞歸函數:在程式語言中,函數直接或間接調用函數本身,則該函數稱為遞歸函數;在數學上的定義如下:對於某一函數 f(x)

f(x),其定義域是集合 A,那麼若對於 A 集合中的某一個值 X0

X

,其函數值 f(x0)

f(x

) 由 f(f(x0))

f(f(x

)) 決定,那麼就稱 f(x)

f(x) 為遞歸函數。

遞歸要素

遞歸必須包含一個基本的出口(結束條件),否則就會無限遞歸,最終導致棧溢出;

遞歸必須包含一個可以分解的問題,例如要想求得 fact(n)

fact(n),就需要用 nfact(n1)

nfact(n1);

遞歸必須必須要向著遞歸出口靠近,例如每次遞歸調用都會 n1

n1,向著遞歸出口 n==0

n==0 靠近。

遞歸與迭代的區別

遞歸(recursion):遞歸則是一步一步往前遞推,直到遞歸基礎,尋找一條路徑, 然後再由前向後計算。(A調用A)

迭代(iteration):迭代是重複反饋過程的活動,其目的通常是為了逼近所需目標或結果。每一次對過程的重複稱為一次「迭代」,而每一次迭代得到的結果會作為下一次迭代的初始值,因此迭代是從前往後計算的。(A重複調用B)

示例一:階乘

一個正整數的階乘(factorial)是所有小於及等於該數的正整數的積,並且 0 的階乘為 1。即 n!=1×2×3×...×(n1)×n

n!=1×2×3×...×(n1)×n,以遞歸方式定義:n!=(n1)!×n

n!=(n1)!×n

def factorial(n):

if n == 0:

return 1

else:

return n * factorial(n-1)

示例二:斐波那契數列

斐波那契數列(Fibonacci sequence),又稱黃金分割數列、因數學家萊昂納多·斐波那契以兔子繁殖為例子而引入,故又稱為「兔子數列」。

有一個數列:0、1、1、2、3、5、8、13、21、34、55、89…,這個數列從第3項開始,每一項都等於前兩項之和。以遞推的方法定義:F(n)=F(n1)+F(n2)(n≥3,n∈N)

F(n)=F(n1)+F(n2)(n≥3,n∈N

def fibonacc(n):

if n == 1 or n == 2:

return 1

else:

return fibonacc(n-1) + fibonacc(n-2)

以上方法的時間複雜度為O(2n)

O(2

n

),稍微大一點的數都會算很久,有一個簡單的解決方案,使用 lru_cache 緩存裝飾器,緩存一些中間結果:

from functools import lru_cache

# 緩存斐波那契函數已經計算出的結果,最多佔用1024位元組內存

@lru_cache(maxsize=1024)

def fibonacc(n):

if n == 1 or n == 2:

return 1

else:

return fibonacc(n-1) + fibonacc(n-2)

另外還有更加節省時間和空間的方法:

def fibonacc(n, current=0, next=1):

if n == 0:

return current

else:

return fibonacc(n-1, next, current+next)

示例三:漢諾塔問題

漢諾塔(又稱河內塔)問題是源於印度一個古老傳說的益智玩具。大梵天創造世界的時候做了三根金剛石柱子,在一根柱子上從下往上按照大小順序摞著64片黃金圓盤。大梵天命令婆羅門把圓盤從下面開始按大小順序重新擺放在另一根柱子上。並且規定,在小圓盤上不能放大圓盤,在三根柱子之間一次只能移動一個圓盤。64片黃金圓盤移動完畢之日,就是世界毀滅之時。

01漢諾塔

對於 n 個盤子,移動步驟如下:

把 n-1 個盤子由 A 經過 C 移動到 B

把最後一個盤子移動到 C

把 n-1 個盤子由 B 經過 A 移動到 C

02漢諾塔

遞歸代碼實現:

def hanoi(n, a, b, c): # n 個盤子,a,b,c三個柱子

if n > 0:

hanoi(n-1, a, c, b) # 把 n-1 個盤子由 a 經過 c 移動到 b

print('moving from {0} to {1}'.format(a, c)) # 把最後一個盤子移動到 c

hanoi(n-1, b, a, c) # 把 n-1 個盤子由 b 經過 a 移動到 c

示例:

def hanoi(n, a, b, c):

if n > 0:

hanoi(n-1, a, c, b)

print('moving from {0} to {1}'.format(a, c))

hanoi(n-1, b, a, c)

hanoi(3, 'A', 'B', 'C')

moving from A to C

moving from A to B

moving from C to B

moving from A to C

moving from B to A

moving from B to C

moving from A to C

尾遞歸

如果一個函數中所有遞歸形式的調用都出現在函數的末尾,我們稱這個遞歸函數是尾遞歸的。通俗來講就是遞歸調用放在了函數的最後。

# 一般遞歸

def func(n):

if n > 0:

func(n-1)

print(n)

# 一般遞歸

def func(n):

if n > 0:

return func(n-1) + n

# 尾遞歸

def func(n):

a = n

if n > 0:

a += 1

print(a, n)

return func(n-1)

對於普通的遞歸,每一級遞歸都產生了新的局部變量,必須創建新的調用棧,隨著遞歸深度的增加,創建的棧越來越多,容易造成爆棧。

def normal_recursion(n):

if n == 1:

return 1

else:

return n + normal_recursion(n-1)

normal_recursion(5) 執行:

normal_recursion(5)

5 + normal_recursion(4)

5 + 4 + normal_recursion(3)

5 + 4 + 3 + normal_recursion(2)

5 + 4 + 3 + 2 + normal_recursion(1)

5 + 4 + 3 + 3

5 + 4 + 6

5 + 10

15

尾遞歸基於函數的尾調用,每一級調用直接返回遞歸函數更新調用棧,沒有新局部變量的產生,類似迭代的實現。

def tail_recursion(n, total=0):

if n == 0:

return total

else:

return tail_recursion(n-1, total+n)

normal_recursion(5) 執行:

tail_recursion(5, 0)

tail_recursion(4, 5)

tail_recursion(3, 9)

tail_recursion(2, 12)

tail_recursion(1, 14)

tail_recursion(0, 15)

15

在 Python,Java,Pascal 等語言中是無法實現尾遞歸優化的,所以採用了 for,while,goto 等特殊結構以迭代的方式來代替尾遞歸。

Python 中尾遞歸的解決方案

使用普通的遞歸來實現斐波那契數列的計算,代碼段如下:

def fibonacc(n, current=0, next=1):

if n == 0:

return current

else:

return fibonacc(n-1, next, current+next)

a = fibonacc(1000)

print(a)

此時會報錯,因為超過了最大遞歸深度(默認深度900-1000左右):

Traceback (most recent call last):

File "F:/PycharmProjects/algorithm/fibonacc_test.py", line 57, in

a = fibonacc(1000)

File "F:/PycharmProjects/algorithm/fibonacc_test.py", line 47, in fibonacc

return fibonacc(n-1, next, current+next)

File "F:/PycharmProjects/algorithm/fibonacc_test.py", line 47, in fibonacc

return fibonacc(n-1, next, current+next)

File "F:/PycharmProjects/algorithm/fibonacc_test.py", line 47, in fibonacc

return fibonacc(n-1, next, current+next)

[Previous line repeated 995 more times]

File "F:/PycharmProjects/algorithm/fibonacc_test.py", line 44, in fibonacc

if n == 0:

RecursionError: maximum recursion depth exceeded in comparison

如果是遞歸深度不是很大的情況,可以手動重設遞歸深度來解決:

import sys

sys.setrecursionlimit(10000) # 遞歸深度設置為 10000

如果遞歸深度非常大,那麼就可以採用尾遞歸優化,但是 Python 官方是並不支持尾遞歸的(不知道為啥),然而這難不到廣大的程式設計師們,早在 2006 年 Crutcher Dunnavant 就想出了一個解決辦法,實現一個 tail_call_optimized 裝飾器,

相關焦點

  • python遞歸算法(上)
    什麼是遞歸在函數內部,是可以調用其他函數的。如果一個函數在內部調用自身,就稱這個函數就是遞歸函數。舉個例子:實現一個可以自定義重複列印你好的函數。原理很好理解,就是不斷的調用自身,如果前面不加上if條件判斷,理論上是會陷入死循環的,但是實際上遞歸到一定次數(最大遞歸次數)就會報錯停止。遞歸有什麼用知道遞歸是怎麼回事,那麼遞歸有什麼實際用處嘛,或者說有什麼獨特之處。
  • 詳細解讀Python 遞歸函數!
    /sum.py", line 18, in sum_recu if n > 0:RecursionError: maximum recursion depth exceeded in comparison***解決遞歸調用棧溢出的方法是通過尾遞歸優化,事實上尾遞歸和循環的效果是一樣的,所以,把循環看成是一種特殊的尾遞歸函數也是可以的。
  • 算法圖解 | 遞歸算法和棧的應用
    遞歸算法:什麼是遞歸呢?我們用算法來解決這個問題,為了找到這個鑰匙,你將使用什麼算法?特點:遞歸只是讓解決方案更清晰,並沒有性能上的優勢。基線條件和遞歸條件:對於循環,我們都知道有一個循環條件,一旦不滿足這個條件,算法會停止循環跳出。
  • JAVA開發之——遞歸算法
    一、遞歸的定義和優缺點遞歸算法的定義:遞歸算法是一種直接或者間接地調用自身算法的過程。遞歸算法的優缺點: (1) 在計算機編寫程序中, 遞歸算法對解決一大類問題是十分有效的,它往往使算法的描述簡潔而且易於理解。 (2) 遞歸算法解題通常顯得很簡潔,但運行效率較低。所以一般不提倡用遞 歸算法設計程序。 (3) 在遞歸調用的過程當中系統為每一層的返回點、局部量等開闢了棧來存 儲。遞歸次數過多容易造成棧溢出等。
  • 「通俗易懂的文字」+「經典的案例」希望能讓你順利入門「遞歸算法」
    遞歸是非常常見的一種算法,非常經典,可以解決非常多的問題。但我估計雖然大部分人知道遞歸,也能看得懂遞歸,但在實際運用中,很容易被遞歸給搞暈(數據,變量,函數等來回的出棧入棧)。今天寫篇文章分享下,或許,能夠給你帶來一些幫助。
  • java 數據結構與算法 之遞歸算法
    我們也只有一點一點的積累,趁現在有時間,今天討論一下java 的數據結構與算法:遞歸算法,希望能達到溫故而知新的效果。一。定義:遞歸(recursion):是指定義自身的同時又出現了對自身的引用。遞歸算法:同理一個算法直接或間接調用自己就叫遞歸算法。一個有意義的遞歸算法總是包含兩部分:遞歸的調用與遞歸的終止條件。二。
  • memoization:讓遞歸算法更高效
    21CTO導讀:本文為學習如何使用稱為memoization的技術通過動態編程使遞歸算法變得高效。
  • 程式設計師必會算法系列之——「遞歸算法」
    講遞歸算法時會涉及到「棧」數據結構的一些知識,如果有不太了解的可以看這裡遞歸的缺點使用遞歸算法時每次方法的調用都需要在棧中開闢出一個空間保存相關數據,頻繁的壓棧、彈棧會導致效率變低。遞歸使用注意事項1、使用遞歸算法解決問題必須要有出口,不然就形成死循環了。
  • Python遞歸函數、閉包和裝飾器
    遞歸一定要控制遞歸的層數,當符合某一條件時要終止遞歸,幾乎所有的遞歸都能用while循環來代替。缺點:遞歸因系統環境影響大,當遞歸過深,可能會得到不可預知的結果。如果一個函數在內部調用自己本身,這個函數就是遞歸函數,遞歸會形成一個深度循環!2、 遞歸函數的使用舉個例子:我們來計算階乘 n!
  • 藍橋杯簡單java遞歸算法
    只能粗糙的寫成這樣了~)想哭~~~~~~~~~~~~~~~~對不起你們~~~~~ 下次一定打一個字存一次草稿~(寫了兩個小時直接沒了~) 作者:浪潮之巔的小蘿蔔頭(純手打,求支持)閒話少敘,今天小編給大家介紹的是由簡單到稍微難一些的java遞歸算法的
  • 算法與數據結構入門:棧與遞歸
    在此之前,我們介紹了動態規劃、深度優先搜索等基礎算法,但是,有部分好友評論說,難度太難了,我們知道動態規劃的自頂向下跟深度優先搜索一般都用遞歸實現,今天我們就先來講講算法與數據結構中,基礎中的基礎遞歸。講遞歸之前,我們先來了解下棧。
  • 遞歸算法 | 排列組合
    今天講的是遞歸算法的一種應用:排列組合(permutations),比較小難 ~  假設有字母abc,對它進行排列組合
  • 數據結構與算法的JavaScript實現及應用:Stack/遞歸/漢諾
    算法的偽代碼如下: s = new stack()   while read to c !return ok 算法的原理為,遇到一個結束的括號時,我們總是要查找最後一個開放的括號是否與之匹配,若找不到開放的括號,或最後一個開放的括號不匹配,則報錯。由於總是而且僅需要尋找最後一個元素,所以我們將開放的括號入棧,匹配時則出棧。由於Stack的特性,這個算法簡單明了,且消耗的時間複雜度為線性級O(n)。
  • Python 進階之遞歸函數一點都不難!
    遞歸是指函數/過程/子程序在運行過程序中直接或間接調用自身而產生的重入現象。在計算機編程裡,遞歸指的是一個過程:函數不斷引用自身,直到引用的對象已知。使用遞歸解決問題,思路清晰,代碼少。但是在主流高級語言中(如C語言、Pascal語言等)使用遞歸算法要耗用更多的棧空間,所以在堆棧尺寸受限制時(如嵌入式系統或者內核態編程),應避免採用。所有的遞歸算法都可以改寫成與之等價的非遞歸算法。(來源於百度,看不懂正常,術語就是不說人話)下面是筆者的個人理解:遞歸就是在函數內部調用自己的函數被稱之為遞歸。看不懂?
  • 使用JavaScript 遞歸算法,匯總累加題目的分數值
    所謂遞歸算法,是將問題轉化為規模縮小的同類問題的子問題,每一個子問題都用一個同樣的算法去解決。一般來說,一個遞歸算法就是函數調用自身去解決它的子問題。使用遞歸算法實現大題的分數值匯總。為了實現樹型結構數據的遍歷與處理,免不了要想到使用遞歸函數來實現。我們先來看一下定義,所謂遞歸算法,是將問題轉化為規模縮小的同類問題的子問題,每一個子問題都用一個同樣的算法去解決。一般來說,一個遞歸算法就是函數調用自身去解決它的子問題。遞歸算法的特點:在函數過程中調用自身。
  • Python進階之遞歸函數的用法及其示例
    遞歸是指函數/過程/子程序在運行過程序中直接或間接調用自身而產生的重入現象。在計算機編程裡,遞歸指的是一個過程:函數不斷引用自身,直到引用的對象已知。使用遞歸解決問題,思路清晰,代碼少。但是在主流高級語言中(如C語言、Pascal語言等)使用遞歸算法要耗用更多的棧空間,所以在堆棧尺寸受限制時(如嵌入式系統或者內核態編程),應避免採用。所有的遞歸算法都可以改寫成與之等價的非遞歸算法。
  • 通過一道面試題目,講一講遞歸算法的時間複雜度!
    面試官提示:「考慮一下遞歸算法」。那麼就可以寫出了如下這樣的一個遞歸的算法,使用遞歸解決了這個問題。一些同學可能一看到遞歸就想到了O(logn),其實並不是這樣,遞歸算法的時間複雜度本質上是要看: 「遞歸的次數 * 每次遞歸中的操作次數」。那再來看代碼,這裡遞歸了幾次呢?
  • 計算機編程必備技巧——遞歸使用
    1、引言 今天我們來學習遞歸,如果單說學習算法, 遞歸併不能說是算法,而是一種編程的手法,為什麼現在要學習這個呢?因為後面在學習其他算法時,要牽涉一些遞歸的調用方法,是為以後理解學習的內容做好鋪墊。
  • 計算機編程必備技巧——使用遞歸
    1、引言今天我們來學習遞歸,如果單說學習算法, 遞歸併不能說是算法,而是一種編程的手法,為什麼現在要學習這個呢?因為後面在學習其他算法時,要牽涉一些遞歸的調用方法,是為以後理解學習的內容做好鋪墊。遞歸方法作為一種優雅的解題方法,是大多數程式設計師比較喜歡的編寫方式之一。
  • 如何優雅地使用javascript遞歸畫一棵結構樹
    本文轉載自【微信公眾號:趣談前端,ID:beautifulFront】經微信公眾號授權轉載,如需轉載與原文作者聯繫遞歸和尾遞歸簡單的說,遞歸就是函數自己調用自己,它作為一種算法在程序設計語言中廣泛應用。