實用貼丨正確的「遞歸」打開方式:讓計算機像計算機一樣去計算

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"調用通常發生在彼此不同的函數之間。其實,函數還有一種特殊的調用方式,那就是自己調用自己,這種方式稱爲函數遞歸調用。遞歸,在程序設計中也是一個常用的技巧,甚至是一種思維方式,非常值得我們掌握。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文選自"},{"type":"text","marks":[{"type":"strong"}],"text":"《Python極簡講義:一本書入門數據分析與機器學習》"},{"type":"text","text":"一書,將與我們一同探討函數的遞歸,並在文末與大家分析了谷歌經典遞歸面試題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"01"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"  感性認識遞歸  "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在講解“遞歸”這個抽象概念之前,讓我們來重溫一下昔日往事。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"小時候,當我們在纏着長輩講故事時,長輩們可能就用下面的故事來“忽悠”我們:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從前有座山,山裏有座廟,廟裏有個老和尚,正在給小和尚講故事!故事是什麼呢?從前有座山,山裏有座廟,廟裏有個老和尚正在給小和尚講故事!故事是什麼呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"……"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除非講故事的人自己停下來不講了,不然這個故事可以“無限”講下去,原因就是“故事”嵌套的“故事”就是“故事”本身,"},{"type":"text","marks":[{"type":"strong"}],"text":"這就是語言上“遞歸”的例子。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是,由於這個故事並沒有一個終止的條件,因此,它實際上是陷入了一種有頭無尾的死循環,因此並不符合程序設計領域中定義的“遞歸”。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在程序設計領域,遞歸是指函數(或方法)直接或間接調用自身的一種操作,如下圖所示。遞歸調用的好處在於,它能夠大大減少代碼量,將原本複雜的問題簡化成一個簡單的基礎操作來完成。在編寫程序的過程中,“遞歸調用”是一個非常實用的技巧。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/17/1765a3dc17894fe9bdb811665a39606e.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遞歸示意圖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從上圖中可以看出,函數不論是直接調用自身,還是間接調用自身,都是一種無終止的過程。在程序設計中,顯然不能出現這種無終止的調用。因此,在編寫遞歸算法時,讀者要特別注意,"},{"type":"text","marks":[{"type":"strong"}],"text":"所有遞歸一定要有終止條件,這又被稱作遞歸出口。"},{"type":"text","text":"如果一個遞歸函數缺少遞歸出口,執行時就會陷入死循環。遞歸出口通常可用if語句來設置,在滿足某種條件時不再繼續,調用某個值,結束遞歸。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"谷歌公司有世界上最聰明的程序員。他們不光聰明,還很有自己的“冷幽默”,別出心裁。比如說,假設你不懂得什麼是“遞歸”,不妨去谷歌搜索一下這個關鍵詞。然後你會發現,除了給出必要的搜索結果,谷歌還給出了一條提示語“您是不是要找:遞歸”,如下圖所示。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/fa/faef3a962fabff45f38d9eda945e4b51.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"谷歌程序員的“冷幽默”乍一看,你可能會覺得,這谷歌搜索是不是有問題啊?我的確、明明、絲毫無誤地查詢的就是“遞歸”,還提示什麼啊?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其實,這正是谷歌搜索引擎背後程序員們的“冷幽默”所在:如果你點擊了那個提示“遞歸”,"},{"type":"text","marks":[{"type":"strong"}],"text":"搜索引擎將再次搜索“遞歸”——相當於自己調用自己——這不正是遞歸的精髓嗎?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"或許你懂了,會心一笑,但可能還會疑惑:這也不對啊,所有的遞歸都有終止條件,如果我們一直點擊這個提示詞“遞歸”,查詢豈不是會無限循環下去?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"放心,你一定不會一直點擊下去。因爲這個遞歸的出口正是,查詢的人終於懂得什麼是遞歸而不再查詢。而你就是那個懂得的人。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/6b/6bc02c2d9361926af77ada33ed87e31c.png","alt":null,"title":"","style":[{"key":"width","value":"25%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"02"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"遞推思維與遞歸思維"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 遞歸 (recurse)在計算機領域被廣泛應用,它不僅是一種計算方法,更是一種思維方式。科技作家吳軍博士認爲:"},{"type":"text","marks":[{"type":"italic"}],"text":"遞歸思維是人與計算機思維最大的差別之一"},{"type":"text","text":"。著名計算機科學家彼得·多伊奇(L. Peter Deutsch)甚至認爲,"},{"type":"text","marks":[{"type":"italic"}],"text":"To iterate is human, torecurse divine(迭代是人,遞歸是神)"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"對於計算機從業者來說,想成爲頂級人才,在做計算機相關工作時,必須具有遞歸思維。"},{"type":"text","text":"對於普通人來講,這種思維方式也很有啓發。因此,不論從哪個角度,遞歸思維都值得我們培養和掌握。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"人的常規思維被稱爲遞推思維"},{"type":"text","text":"。在中文裏,“遞推”和“遞歸”只有一字之差,但在英文世界裏,它們的差別可大了去了,可謂“差之毫釐,謬以千里”。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們先來說說 遞推 (iterate)。比如小時候我們學習數數,從1、2、3一直數到100,就是典型的遞推。類似地,我們在學習過程中循序漸進,如水到而渠成,出發點都是正向的,由易到難,由小到大,由局部到整體。"},{"type":"text","marks":[{"type":"strong"}],"text":"遞推是人類本能的正向思維"},{"type":"text","text":",於我們而言,可謂熟稔於心。而“遞歸”則有一定的反常識。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面我們以計算一個整數的階乘爲例來說明兩種思維的差別。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果用人類常用遞推方式計算一個整數的階乘,比如5!=1×2×3×4×5,那麼做法是從小到大一個數一個數接連相乘。如果計算10的階乘(10!),過程也是類似的,即從1乘到10。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在生活中,這種做法不僅合情合理,而且渾然天成。事實上,在中學裏學的數學歸納法(利用當n成立時的結論,推導n+1)就是遞推方法。爲了簡單起見,我們還是用前面求階乘的簡單例子來說明遞歸的原理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"計算機是怎麼計算階乘的呢?它是倒着來的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"比如要算5!,計算機就把它變成5×4!(即5乘以4的階乘)。當然,我們可能會質疑,4!還不知道呢!但沒有關係,計算機會採用同樣的方法,把4!變成4×3!。至於3!,則用同樣的算法處理。最後做到1!時,計算機知道1!=1(這就是遞歸的終止條件),自此便不再往下擴展了。接下來,就是倒推回所有的結果。因爲知道了1!,順水推舟,就知道了2!,然後可知3!、4!和5!。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從上面描述的遞歸過程可以看出,遞歸的方法論可歸結爲兩步:"},{"type":"text","marks":[{"type":"strong"}],"text":"先從上向下層層展開,再從下到上一步步回溯。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"03"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"遞歸調用的函數"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你可能會問,計算機爲何要這麼算?這麼算有何優勢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"答案並不複雜,利用遞歸可以使算法的邏輯變得非常簡單。因爲遞歸過程的每一步用的都是同一個算法,計算機只需要自頂向下不斷重複即可。具體到階乘的計算,無非就是某個數字n的階乘,變成這個數乘以n-1的階乘。因此,"},{"type":"text","marks":[{"type":"underline"},{"type":"strong"}],"text":"遞歸的法則就兩條:一是自頂而下(從目標直接出發),二是不斷重複。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遞歸的另一個特點在於,它只關心自己下一層的細節,而並不關心更下層的細節。你可以理解爲,遞歸的簡單源自它只關注“當下”,把握“小趨勢”,雖然每一步都簡單,但一直追尋下去,也能獲得自己獨特的精彩。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面我們就以計算階乘爲例,分別使用遞推和遞歸方式實現,大家可體會二者的區別。"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"【範例】利用遞推和遞歸方式分別計算n!"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 1#用正向遞推的方式計算階乘 2def iterative_fact( n ):   3    fact = 1 4    for i in range(1, n + 1): 5        fact *= i 6    return fact 7 8#用逆向遞歸的方式計算階乘 9def recursive_fact( n ):10    if n <= 1 :11        return n;12    return n * recursive_fact(n - 1)1314#調用遞推方法計算15num = 516result = iterative_fact( num );17print(\"遞推方法:{}!= {}\".format(num, result))18#調用遞歸方法計算19result = recursive_fact(num)20print(\"遞歸方法:{}!= {}\".format(num, result))"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"【運行結果】"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1遞推方法:5!= 1202遞歸方法:5!= 120"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遞歸函數的優點在於,定義簡單,邏輯清晰。理論上,所有的遞歸函數都可以寫成循環的方式,但正向遞推(即循環)的邏輯不如逆向遞歸的邏輯清晰。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"04"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"谷歌公司的遞歸面試題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有這麼一個遊戲:有兩個人,第一個人先從1和2中挑一個數字,第二個人可以在對方的基礎上選擇加1或者加2,然後又輪到第一個人,他也可以選擇加1或者加2,之後再把選擇權交給對方,就這樣雙方交替地選擇加1或者加2,誰先加到20,誰就贏了。對於這個遊戲,你用什麼策略保證一定能贏?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"underline"},{"type":"strong"}],"text":"✔ 案例分析   "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果用正向的遞推思維(比如說窮舉法),並不容易想清楚,而且還容易漏掉合理的解。但如果用逆向的遞歸思維,問題的解就非常容易推導出來。我們先從結果出發,如果要想搶到20,就需要搶到17,因爲搶到了17,無論對方是加1還是加2,你都可以加到20。而要想搶到17,就要搶到14,以此類推,就必須搶到11、8、5和2。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因此對於這道題,只要第一個人搶到了2,他就贏定了。這是因爲,無論對方選擇加1還是加2,他都可以讓這一輪兩個人加起來的數值等於5。同樣的道理,在當前和爲5的基礎上,無論對方選擇加1或加2,他都能讓和向着8進發。以此類推,整個過程都被他牢牢控制,最終的數列之和,毫無懸念地被他鎖定在20。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當然谷歌的面試題並非這麼簡單,如果你答對第一道題,那麼緊接着就會有下一道題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"按照上述方法,在不考慮誰輸誰贏的情況下,從開始(以1或2爲起點)加到20,有多少種不同的遞加過程?比如1,4,7,10,12,15,18,20算一種;2,5,8,11,14,17,20又是一種。那麼一共會有多少種這樣的過程呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"underline"},{"type":"strong"}],"text":"✔ 案例分析  "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這道題顯然並不簡單,通過正向的窮舉法很難完備遍歷。解這道題的技巧還是要使用遞歸。我們假定數到20有F(20)種不同的路徑,那麼到達20這個數字,前一步只有兩個可能的情況,即從18直接跳到20,或者從19數到20。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於從18跳到20和從19到20是不同的,因此達到20的路徑數量,其實就是達到18的路徑數量,加上達到19的路徑數量,也就是說,F(20)=F(18)+F(19)。類似地,F(19)=F(18)+F(17)。這就是遞推公式。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後,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)即可。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"聰慧如你,你一定看出來了,這就是著名的斐波那契數列,如果我們認爲F(0)也等於1,那麼這個數列就長成這樣:1(F(0)),1,2,3,5,8,13,21,……這個數列幾乎按照幾何級數的速度增長,到了F(20),就已經是10946了。因此,僅僅靠正向的窮舉法,基本上是不可能把所有情況都列舉出來的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述面試題來自曾就職於谷歌公司的吳軍博士。吳軍博士在分析這道面試題時指出,在數學和計算機上,等價性原則是一個非常重要的原則。很多問題的表象看起來紛繁複雜,但抽絲剝繭之後,其本質是等價的。比如說,如果一個樓梯有20階,你每次可以爬一階歇一會,也可以爬兩階歇一會,爬到20階一共有多少種歇息法?這個問題的解,其實和“誰先搶到20”是一樣的,也是一個斐波那契數列。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從某種程度上來看,遞歸思維是一種以結果爲導向,反向追尋,直到追尋到原點(遞歸的終止條件)的思維方式,一旦原點問題得以解決,其後的問題都會迎刃而解。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文節選自博文視點新書"},{"type":"text","marks":[{"type":"strong"}],"text":"《Python極簡講義:一本書入門數據分析與機器學習》。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"張玉宏 著"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"詳情:https://u.jd.com/wvJvUn"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本書秉承零入門、可讀性高和注重實戰的理念,全面覆蓋 NumPy、Pandas、Matplolib、Seaborn、sklearn 等入門數據科學的必備知識。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"破“門”而入,有如神助!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章