Python 算法之递归与尾递归,斐波那契数列以及汉诺塔的实现
文章目錄
- 遞歸概念
- 遞歸要素
- 遞歸與迭代的區(qū)別
- 示例一:階乘
- 示例二:斐波那契數(shù)列
- 示例三:漢諾塔問題
- 尾遞歸
- Python 中尾遞歸的解決方案
遞歸概念
遞歸:程序調(diào)用自身的編程技巧稱為遞歸( recursion)。用一種通俗的話來(lái)說(shuō)就是自己調(diào)用自己,它通常把一個(gè)大型復(fù)雜的問題層層轉(zhuǎn)化為一個(gè)與原問題相似的、但是規(guī)模較小的問題來(lái)求解,當(dāng)問題小到一定規(guī)模的時(shí)候,需要一個(gè)遞歸出口返回。遞歸策略只需少量的程序就可描述出解題過程所需要的多次重復(fù)計(jì)算,大大地減少了程序的代碼量。遞歸的能力在于用有限的語(yǔ)句來(lái)定義對(duì)象的無(wú)限集合。
遞歸函數(shù):在編程語(yǔ)言中,函數(shù)直接或間接調(diào)用函數(shù)本身,則該函數(shù)稱為遞歸函數(shù);在數(shù)學(xué)上的定義如下:對(duì)于某一函數(shù) f(x)f(x)f(x),其定義域是集合 A,那么若對(duì)于 A 集合中的某一個(gè)值 X0X_0X0?,其函數(shù)值 f(x0)f(x_0)f(x0?) 由 f(f(x0))f(f(x_0))f(f(x0?)) 決定,那么就稱 f(x)f(x)f(x) 為遞歸函數(shù)。
遞歸要素
-
遞歸必須包含一個(gè)基本的出口(結(jié)束條件),否則就會(huì)無(wú)限遞歸,最終導(dǎo)致棧溢出;
-
遞歸必須包含一個(gè)可以分解的問題,例如要想求得 fact(n)fact(n)fact(n),就需要用 n?fact(n?1)n * fact(n-1)n?fact(n?1);
-
遞歸必須必須要向著遞歸出口靠近,例如每次遞歸調(diào)用都會(huì) n?1n-1n?1,向著遞歸出口 n==0n == 0n==0 靠近。
遞歸與迭代的區(qū)別
-
遞歸(recursion):遞歸則是一步一步往前遞推,直到遞歸基礎(chǔ),尋找一條路徑, 然后再由前向后計(jì)算。(A調(diào)用A)
-
迭代(iteration):迭代是重復(fù)反饋過程的活動(dòng),其目的通常是為了逼近所需目標(biāo)或結(jié)果。每一次對(duì)過程的重復(fù)稱為一次“迭代”,而每一次迭代得到的結(jié)果會(huì)作為下一次迭代的初始值,因此迭代是從前往后計(jì)算的。(A重復(fù)調(diào)用B)
示例一:階乘
一個(gè)正整數(shù)的階乘(factorial)是所有小于及等于該數(shù)的正整數(shù)的積,并且 0 的階乘為 1。即 n!=1×2×3×...×(n?1)×nn!=1×2×3×...×(n-1)×nn!=1×2×3×...×(n?1)×n,以遞歸方式定義:n!=(n?1)!×nn!=(n-1)!×nn!=(n?1)!×n
def factorial(n):if n == 0:return 1else:return n * factorial(n-1)示例二:斐波那契數(shù)列
斐波那契數(shù)列(Fibonacci sequence),又稱黃金分割數(shù)列、因數(shù)學(xué)家萊昂納多·斐波那契以兔子繁殖為例子而引入,故又稱為“兔子數(shù)列”。
有一個(gè)數(shù)列:0、1、1、2、3、5、8、13、21、34、55、89…,這個(gè)數(shù)列從第3項(xiàng)開始,每一項(xiàng)都等于前兩項(xiàng)之和。以遞推的方法定義:F(n)=F(n?1)+F(n?2)(n≥3,n∈N?)F(n)=F(n - 1)+F(n - 2)(n ≥ 3,n ∈ N^*)F(n)=F(n?1)+F(n?2)(n≥3,n∈N?)
def fibonacc(n):if n == 1 or n == 2:return 1else:return fibonacc(n-1) + fibonacc(n-2)以上方法的時(shí)間復(fù)雜度為O(2n)O(2^n)O(2n),稍微大一點(diǎn)的數(shù)都會(huì)算很久,有一個(gè)簡(jiǎn)單的解決方案,使用 lru_cache 緩存裝飾器,緩存一些中間結(jié)果:
from functools import lru_cache# 緩存斐波那契函數(shù)已經(jīng)計(jì)算出的結(jié)果,最多占用1024字節(jié)內(nèi)存 @lru_cache(maxsize=1024) def fibonacc(n):if n == 1 or n == 2:return 1else:return fibonacc(n-1) + fibonacc(n-2)另外還有更加節(jié)省時(shí)間和空間的方法:
def fibonacc(n, current=0, next=1):if n == 0:return currentelse:return fibonacc(n-1, next, current+next)示例三:漢諾塔問題
漢諾塔(又稱河內(nèi)塔)問題是源于印度一個(gè)古老傳說(shuō)的益智玩具。大梵天創(chuàng)造世界的時(shí)候做了三根金剛石柱子,在一根柱子上從下往上按照大小順序摞著64片黃金圓盤。大梵天命令婆羅門把圓盤從下面開始按大小順序重新擺放在另一根柱子上。并且規(guī)定,在小圓盤上不能放大圓盤,在三根柱子之間一次只能移動(dòng)一個(gè)圓盤。64片黃金圓盤移動(dòng)完畢之日,就是世界毀滅之時(shí)。
對(duì)于 n 個(gè)盤子,移動(dòng)步驟如下:
- 把 n-1 個(gè)盤子由 A 經(jīng)過 C 移動(dòng)到 B
- 把最后一個(gè)盤子移動(dòng)到 C
- 把 n-1 個(gè)盤子由 B 經(jīng)過 A 移動(dòng)到 C
遞歸代碼實(shí)現(xiàn):
def hanoi(n, a, b, c): # n 個(gè)盤子,a,b,c三個(gè)柱子if n > 0:hanoi(n-1, a, c, b) # 把 n-1 個(gè)盤子由 a 經(jīng)過 c 移動(dòng)到 bprint('moving from {0} to {1}'.format(a, c)) # 把最后一個(gè)盤子移動(dòng)到 chanoi(n-1, b, a, c) # 把 n-1 個(gè)盤子由 b 經(jīng)過 a 移動(dòng)到 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尾遞歸
如果一個(gè)函數(shù)中所有遞歸形式的調(diào)用都出現(xiàn)在函數(shù)的末尾,我們稱這個(gè)遞歸函數(shù)是尾遞歸的。通俗來(lái)講就是遞歸調(diào)用放在了函數(shù)的最后。
# 一般遞歸 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 = nif n > 0:a += 1print(a, n)return func(n-1)對(duì)于普通的遞歸,每一級(jí)遞歸都產(chǎn)生了新的局部變量,必須創(chuàng)建新的調(diào)用棧,隨著遞歸深度的增加,創(chuàng)建的棧越來(lái)越多,容易造成爆棧。
def normal_recursion(n):if n == 1:return 1else:return n + normal_recursion(n-1)normal_recursion(5) 執(zhí)行:
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尾遞歸基于函數(shù)的尾調(diào)用,每一級(jí)調(diào)用直接返回遞歸函數(shù)更新調(diào)用棧,沒有新局部變量的產(chǎn)生,類似迭代的實(shí)現(xiàn)。
def tail_recursion(n, total=0):if n == 0:return totalelse:return tail_recursion(n-1, total+n)normal_recursion(5) 執(zhí)行:
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 等語(yǔ)言中是無(wú)法實(shí)現(xiàn)尾遞歸優(yōu)化的,所以采用了 for,while,goto 等特殊結(jié)構(gòu)以迭代的方式來(lái)代替尾遞歸。
Python 中尾遞歸的解決方案
使用普通的遞歸來(lái)實(shí)現(xiàn)斐波那契數(shù)列的計(jì)算,代碼段如下:
def fibonacc(n, current=0, next=1):if n == 0:return currentelse:return fibonacc(n-1, next, current+next)a = fibonacc(1000) print(a)此時(shí)會(huì)報(bào)錯(cuò),因?yàn)槌^了最大遞歸深度(默認(rèn)深度900-1000左右):
Traceback (most recent call last):File "F:/PycharmProjects/algorithm/fibonacc_test.py", line 57, in <module>a = fibonacc(1000)File "F:/PycharmProjects/algorithm/fibonacc_test.py", line 47, in fibonaccreturn fibonacc(n-1, next, current+next)File "F:/PycharmProjects/algorithm/fibonacc_test.py", line 47, in fibonaccreturn fibonacc(n-1, next, current+next)File "F:/PycharmProjects/algorithm/fibonacc_test.py", line 47, in fibonaccreturn fibonacc(n-1, next, current+next)[Previous line repeated 995 more times]File "F:/PycharmProjects/algorithm/fibonacc_test.py", line 44, in fibonaccif n == 0: RecursionError: maximum recursion depth exceeded in comparison如果是遞歸深度不是很大的情況,可以手動(dòng)重設(shè)遞歸深度來(lái)解決:
import sys sys.setrecursionlimit(10000) # 遞歸深度設(shè)置為 10000如果遞歸深度非常大,那么就可以采用尾遞歸優(yōu)化,但是 Python 官方是并不支持尾遞歸的(不知道為啥),然而這難不到廣大的程序員們,早在 2006 年 Crutcher Dunnavant 就想出了一個(gè)解決辦法,實(shí)現(xiàn)一個(gè) tail_call_optimized 裝飾器,原文鏈接:https://code.activestate.com/recipes/474088/,原代碼是 Python 2.4 實(shí)現(xiàn)的,用 Python 3.x 實(shí)現(xiàn)如下:
# This program shows off a python decorator # which implements tail call optimization. It # does this by throwing an exception if it is # it's own grandparent, and catching such # exceptions to recall the stack.import sysclass TailRecurseException(BaseException):def __init__(self, args, kwargs):self.args = argsself.kwargs = kwargsdef tail_call_optimized(g):"""This function decorates a function with tail calloptimization. It does this by throwing an exceptionif it is it's own grandparent, and catching suchexceptions to fake the tail call optimization.This function fails if the decorated5function recurses in a non-tail context."""def func(*args, **kwargs):f = sys._getframe()if f.f_back and f.f_back.f_back and f.f_back.f_back.f_code == f.f_code:raise TailRecurseException(args, kwargs)else:while 1:try:return g(*args, **kwargs)except TailRecurseException as e:args = e.argskwargs = e.kwargsfunc.__doc__ = g.__doc__return func使用該裝飾器再來(lái)實(shí)現(xiàn)比較大的斐波那契數(shù)列的計(jì)算:
@tail_call_optimized def fibonacc(n, current=0, next=1):if n == 0:return currentelse:return fibonacc(n-1, next, current+next)a = fibonacc(1000) print(a)輸出結(jié)果:
43466557686937456435688527675040625802564660517371780402481729089536555417949051890403879840079255169295922593080322634775209689623239873322471161642996440906533187938298969649928516003704476137795166849228875tail_call_optimized 實(shí)現(xiàn)尾遞歸優(yōu)化的原理:當(dāng)遞歸函數(shù)被該裝飾器修飾后,遞歸調(diào)用在裝飾器while循環(huán)內(nèi)部進(jìn)行,每當(dāng)產(chǎn)生新的遞歸調(diào)用棧幀時(shí),f.f_back.f_back.f_code == f.f_code: 就捕獲當(dāng)前尾調(diào)用函數(shù)的參數(shù),并拋出異常,從而銷毀遞歸棧并使用捕獲的參數(shù)手動(dòng)調(diào)用遞歸函數(shù),所以遞歸的過程中始終只存在一個(gè)棧幀對(duì)象,達(dá)到優(yōu)化的目的。
這里是一段防爬蟲文本,請(qǐng)讀者忽略。 本文原創(chuàng)首發(fā)于 CSDN,作者 TRHX?鮑勃。 博客首頁(yè):https://itrhx.blog.csdn.net/ 本文鏈接:https://itrhx.blog.csdn.net/article/details/109322815 未經(jīng)授權(quán),禁止轉(zhuǎn)載!惡意轉(zhuǎn)載,后果自負(fù)!尊重原創(chuàng),遠(yuǎn)離剽竊! 創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來(lái)咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)
總結(jié)
以上是生活随笔為你收集整理的Python 算法之递归与尾递归,斐波那契数列以及汉诺塔的实现的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【Python 必会技巧】利用 utf-
- 下一篇: Python 数据分析三剑客之 Matp