递归函数python有什么特点_Python中的递归
在前面的講解中,函數的調用通常發生在彼此不同的函數之間。其實,函數還有一種特殊的調用方式,那就是自己調用自己,這種方式稱為函數遞歸調用。
遞歸,在程序設計中也是一個常用的技巧,甚至是一種思維方式,非常值得我們掌握。
4.3.1 感性認識遞推
在講解“遞歸”這個抽象概念之前,讓我們來重溫一下昔日往事。小時候,當我們在纏著長輩講故事時,長輩們可能就用下面的故事來“忽悠”我們:從前有座山,山里有座廟,廟里有個老和尚,正在給小和尚講故事!故事是什么呢?從前有座山,山里有座廟,廟里有個老和尚正在給小和尚講故事!故事是什么呢……
除非講故事的人自己停下來不講了,不然這個故事可以“無限”講下去,原因就是“故事”嵌套的“故事”就是“故事”本身,這就是語言上“遞歸”的例子。
但是,由于這個故事并沒有一個終止的條件,因此,它實際上是陷入了一種有頭無尾的死循環,因此并不符合程序設計領域中定義的“遞歸”。在程序設計領域,遞歸是指函數(或方法)直接或間接調用自身的一種操作,如圖4-4所示。遞歸調用的好處在于,它能夠大大減少代碼量,將原本復雜的問題簡化成一個簡單的基礎操作來完成。在編寫程序的過程中,“遞歸調用”是一個非常實用的技巧。
圖4-4 遞歸示意圖
從圖4-4中可以看出,函數不論是直接調用自身,還是間接調用自身,都是一種無終止的過程。
在程序設計中,顯然不能出現這種無終止的調用。因此,在編寫遞歸算法時,讀者要特別注意,所有遞歸一定要有終止條件,這又被稱作遞歸出口。
如果一個遞歸函數缺少遞歸出口,執行時就會陷入死循環。遞歸出口通常可用if語句來設置,在滿足某種條件時不再繼續,調用某個值,結束遞歸。
谷歌公司有世界上最聰明的程序員。他們不光聰明,還很有自己的“冷幽默”,別出心裁。比如說,假設你不懂得什么是“遞歸”,不妨去谷歌搜索一下這個關鍵詞。
然后你會發現,除了給出必要的搜索結果,谷歌還給出了一個提示語“您是不是要找:遞歸”,如圖4-5所示。
圖4-5 谷歌程序員的“冷幽默”
咋一看,你可能會覺得,這谷歌搜索是不是有問題啊?我的確、明明、絲毫無誤地查詢的就是“遞歸”,還提示什么啊?其實,這正是谷歌搜索引擎背后程序員們的“冷幽默”所在:如果你點擊了那個提示“遞歸”,搜索引擎將再次搜索“遞歸”——相當于自己調用自己——這不正是遞歸的精髓嗎?
或許你懂了,會心一笑,但可能還會疑惑:這也不對啊,所有的遞歸都有終止條件,如果我們一直點擊這個提示詞“遞歸”,查詢豈不是會無限循環下去?
放心,你一定不會一直點擊下去。因為這個遞歸的出口正是,查的人終于懂得什么是遞歸而不再查詢。而你就是那個懂得的人。
4.3.2 思維與遞歸思維
遞歸(recurse)在計算機領域被廣泛應用,它不僅是一種計算方法,更是一種思維方式。科技作家吳軍博士認為:遞歸思維是人與計算機思維最大的差別之一。著名計算機科學家彼得·多伊奇(L. Peter Deutsch)甚至認為,To iterate is human, to recurse divine(迭代是人,遞歸是神)。
對于計算機從業者來說,想成為頂級人才,在做計算機相關工作時,必須具有遞歸思維。對于普通人來講,這種思維方式也很有啟發。因此,不論從哪個角度,遞歸思維都值得我們培養和掌握。
人的常規思維被稱為遞推(iterate)思維。在中文里,“遞推”和“遞歸”只有一字之差,但在英文世界里,它們的差別可大了去了,可謂“差之毫厘,謬以千里”。
我們先來說說遞推。比如小時候我們學習數數,從1、2、3一直數到100,就是典型的遞推。類似地,我們在學習過程中循序漸進,如水到而渠成,出發點都是正向的,由易到難,由小到大,由局部到整體。
遞推是人類本能的正向思維,于我們而言,可謂熟稔于心。而“遞歸”則有一定的反常識。
下面我們以計算一個整數的階乘為例來說明兩種思維的差別。如果用人類常用遞推方式計算一個整數的階乘,比如5!=1×2×3×4×5,那么做法是從小到大一個數一個數接連相乘。如果計算10的階乘(10!),過程也是類似的,即從1乘到10。
在生活中,這種做法不僅合情合理,而且渾然天成。事實上,在中學里學的數學歸納法(利用當n成立時的結論,推導n+1)的方法論就是遞推。
為了簡單起見,我們還是用前面求階乘的簡單例子來說明遞歸的原理。計算機是怎么計算階乘的呢?它是倒著來的。比如要算5!,計算機就把它變成5x4!(即5乘以4的階乘)。當然,我們可能會質疑,4!還不知道呢!
但沒有關系,計算機會采用同樣的方法,把4!變成4x3!。至于3!,則用同樣的算法處理。最后做到1!時,計算機知道1!=1(這就是遞歸的終止條件),自此便不再往下擴展了。
接下來,就是倒推回所有的結果。因為由于知道了1!,順水推舟,就知道了2!,然后可知3!、4!和5!從上面描述的遞歸過程可以看出,遞歸的方法論可歸結為兩步:先從上向下層層展開,再從下到上一步步回溯。
4.3.3 遞歸調用的函數
你可能會問,計算機為何要這么算?這么算有何優勢?答案并不復雜,因為利用遞歸可以使算法的邏輯變得非常簡單。因為遞歸過程的每一步用的都是同一個算法,計算機只需要自頂向下不斷重復即可。
具體到階乘的計算,無非就是某個數字n的階乘,變成這個數乘以n-1的階乘。因此,遞歸的法則就兩條:一是自頂而下(從目標直接出發),二是不斷重復。
遞歸的另一個特點在于,它只關心自己的下一層的細節,而并不關心更下層的細節。你可以理解遞歸的簡單,源自它只關注“當下”,把握“小趨勢”,雖然每一步都簡單,但一直追尋下去,也能獲得自己獨特的精彩。
下面我們就以計算階乘為例,分別使用遞推和遞歸方式實現,見【范例4-7】,讀者可體會二者的區別。
【范例4-7】利用遞推和遞歸方式分別計算n!(iterative-recursive.py)。01 #用正向遞推的方式計算階乘
02 def iterative_fact( n ):
03 fact = 1
04 for i in range(1, n + 1):
05 fact *= i
06 return fact
07
08 # 用逆向遞歸的方式計算階乘
09 def recursive_fact( n ):
10 if n <= 1 :
11 return n;
12 return n * recursive_fact(n - 1)
13
14 #調用非遞歸方法計算
15 num = 5
16 result = iterative_fact( num );
17 print("遞推方法:{}!= {}".format(num, result))
18 #調用遞歸方法計算
19 result = recursive_fact(num)
20 print("遞歸方法:{}!= {}".format(num, result))
【運行結果】遞推方法:5!= 120
遞歸方法:5!= 120
【代碼分析】
第02~06行定義了一個遞推計算階乘的函數iterative_fact(),函數內部采用for循環的方式來計算結果。在for循環控制過程中使用了range()函數,由于range的取值區間是左閉右開的,最后一個值取不到,所以在第04行執行了n+1操作。
第09~12行定義一個遞歸函數recursive_fact,采用遞歸的方式計算結果。
第17行和第20行用到了Python的格式化輸出。在Python中,一切皆對象。用雙引號引起來的字符串“遞歸方法:{}!= {}”,實際上是一個str對象。既然是對象,它就會有相應的方法成員,format()就是用于格式化輸出的方法,因此可以通過“對象.方法名”的格式來調用合適的方法。字符串中的花括號{}表示輸出占位符,第1個占位符{}用于輸出format()函數中第1個變量,第2個占位符{}用于輸出format()函數中第2個變量,以此類推。
遞歸函數的優點在于,定義簡單,邏輯清晰。理論上,所有的遞歸函數都可以寫成循環的方式,但正向遞推(即循環)的邏輯不如逆向遞歸的邏輯清晰。
對于遞推的實現,這里用到了前面章節中講到的for循環語句,以1為基數不斷循環相乘,最終得出階乘的結果。而在遞歸實現的操作中,這里通過對方法本身的壓棧和彈棧的方式,將每一層的結果逐級返回,通過逐步累加求得結果。
recursive_fact(5)的計算過程如下。===> recursive_fact (5)
===> 5 * recursive_fact (4)
===> 5 * (4 * recursive_fact (3))
===> 5 * (4 * (3 * recursive_fact (2)))
===> 5 * (4 * (3 * (2 * recursive_fact (1))))
===> 5 * (4 * (3 * (2 * 1)))
===> 5 * (4 * (3 * 2))
===> 5 * (4 * 6)
===> 5 * 24
===> 120
需要注意的是,雖然遞歸有許多的優點,但缺點也很明顯。那就是,使用遞歸方式需要函數做大量的壓棧和彈棧操作,由于壓棧和彈棧涉及函數執行上下文(context)的現場保存和現場恢復,所以程序的運行速度比不用遞歸實現要慢。
此外,大量的堆棧操作消耗的內存資源要比非遞歸調用多。而且,過深的遞歸調用還可能會導致堆棧溢出。如果操作不慎,還容易出現死循環。因此讀者編寫代碼過程中需要多加注意,一定要設置遞歸操作的終止條件。
思考與練習:一道關于遞歸的面試題(谷歌公司)
(1)有這么一個游戲:有兩個人,第一個人先從1和2中挑一個數字,第二個人可以在對方的基礎上選擇加1或者加2,然后又輪到第一個人,他也可以選擇加1或者加2,之后再把選擇權交給對方,就這樣雙方交替地選擇加1或者加2,誰先加到20,誰就贏了。對于這個游戲,你用什么策略保證一定能贏?
【案例分析】
如果用正向的遞推思維(比如說窮舉法),并不容易想清楚,而且還容易漏掉合理的解。但如果用逆向的遞歸思維,問題的解就非常容易推導出來。我們先從結果出發,如果要想搶到20,就需要搶到17,因為搶到了17,無論對方是加1還是加2,你都可以加到20。而要想搶到17,就要搶到14,以此類推,就必須搶到11、8、5和2。
圖4-6 計算一下共有多少種上樓梯的方法
因此對于這道題,只要第一個人搶到了2,他就贏定了。這是因為,無論對方選擇加1還是加2,他都可以讓這一輪兩個人加起來的數值等于5。同樣的道理,在當前和為5的基礎上,無論對方選擇加1或加2,他都能讓和向著8進發。以此類推,整個過程都被他牢牢控制,最終的數列之和,毫無懸念地被他鎖定在20。
當然谷歌的面試題并非這么簡單,如果你答對第一道題,那么緊接著就會有下一道題。
(2)按照上述方法,在不考慮誰輸誰贏的情況下,從一開始(以1或2為起點)加到20,有多少種不同的遞加過程?比如1,4,7,10,12,15,18,20算一種;2,5,8,11,14,17,20又是一種。那么一共會有多少種這樣的過程呢?
【案例分析】
這道題顯然并不簡單,通過正向的窮舉法很難完備遍歷。解這道題的技巧還是要使用遞歸。我們假定數到20有F(20)種不同的路徑,那么到達20這個數字,前一步只有兩個可能的情況,即從18直接跳到20,或者從19數到20。
由于從18跳到20和從19到20是不同的,因此達到20的路徑數量,其實就是達到18的路徑數量,加上達到19的路徑數量,也就是說,F(20)=F(18)+F(19)。類似地,F(19)=F(18)+F(17)。這就是遞推公式。
最后,F(1)只有一個可能,就是1,F(2)有兩個可能,要么直接跳到2,要么從1達到2。知道了F(1)=1和F(2)=2,就可以知道F(3)。知道F(3),就可以知道F(4),因為F(4)= F(3)+ F(2),以此類推,一直到F(20)即可。
聰慧如你,你一定看出來了,這就是著名的斐波那契數列,如果我們認為F(0)也等于1,那么這個數列就長成這樣:1(F(0)),1,2,3,5,8,13,21,……這個數列幾乎按照幾何級數的速度增長,到了F(20),就已經是10946了(可利用前面的【范例3-13】來測試)。因此,僅僅靠正向的窮舉法,基本上是不可能把所有情況都列舉出來的。
上述面試題來自于曾在就職于谷歌公司的吳軍博士。吳軍博士在分析這道面試題時指出,在數學和計算機上,等價性原則是一個非常重要的原則。很多問題的表象看起來紛繁復雜,但抽絲剝繭之后,其本質是等價的。
比如說,如果一個樓梯有20階,你每次可以爬一階歇一會,也可以兩階歇一會,爬到20階一共有多少種歇息法?這個問題的解,其實和“誰先搶到20”是一樣的,也是一個斐波那契數列。
除了前面講解的技巧,本章涉及的一些思維方式也值得讀者注意。從某種程度上來看,遞歸思維是一種以結果為導向,反向追尋,直到追尋到原點(遞歸的終止條件)的思維方式,一旦原點問題得以解決,其后的問題都會迎刃而解。
你看看,這是不是和埃隆·馬斯克(Elon Musk)等人常說的“第一性原理”思想有著類似之處呢?
本文部分節選自《Python極簡講義:一本書入門數據分析與機器學習》(張玉宏)【摘要 書評 試讀】- 京東圖書?item.jd.com
(張玉宏著,電子工業出版社,2020年5月出版)。更多理論推導及實戰環節,請參閱該書。
總結
以上是生活随笔為你收集整理的递归函数python有什么特点_Python中的递归的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 奥拉星的星灵怎么用
- 下一篇: python合并两个文本文件内容_用Py