遞迴:夢中夢
“方其夢也,不知其夢也。夢之中又佔其夢焉,覺而後知其夢也。”
—— 《莊子·齊物論》
遞迴是很神奇的,但是在大多數的程式設計類書藉中對遞迴講解的並不好。它們只是給你展示一個遞迴階乘的實現,然後警告你遞迴執行的很慢,並且還有可能因為棧緩衝區溢位而崩潰。“你可以將頭伸進微波爐中去烘乾你的頭髮,但是需要警惕顱內高壓並讓你的頭髮生爆炸,或者你可以使用毛巾來擦乾頭髮。”難怪人們不願意使用遞迴。但這種建議是很糟糕的,因為在演算法中,遞迴是一個非常強大的思想。
我們來看一下這個經典的遞迴階乘:
#include <stdio.h>
int factorial(int n)
{
int previous = 0xdeadbeef;
if (n == 0 || n == 1) {
return 1;
}
previous = factorial(n-1);
return n * previous;
}
int main(int argc)
{
int answer = factorial(5);
printf("%d\n", answer);
}
遞迴階乘 - factorial.c
函式呼叫自身的這個觀點在一開始是讓人很難理解的。為了讓這個過程更形象具體,下圖展示的是當呼叫 factorial(5)
並且達到 n == 1
這行程式碼 時,棧上 端點的情況:
每次呼叫 factorial
都生成一個新的 棧幀。這些棧幀的建立和 銷燬 是使得遞迴版本的階乘慢於其相應的迭代版本的原因。在呼叫返回之前,累積的這些棧幀可能會耗盡棧空間,進而使你的程式崩潰。
而這些擔心經常是存在於理論上的。例如,對於每個 factorial
的棧幀佔用 16 位元組(這可能取決於棧排列以及其它因素)。如果在你的電腦上執行著現代的 x86 的 Linux 核心,一般情況下你擁有 8 GB 的棧空間,因此,factorial
程式中的 n
最多可以達到 512,000 左右。這是一個 巨大無比的結果,它將花費 8,971,833 位元來表示這個結果,因此,棧空間根本就不是什麼問題:一個極小的整數 —— 甚至是一個 64 位的整數 —— 在我們的棧空間被耗盡之前就早已經溢位了成千上萬次了。
過一會兒我們再去看 CPU 的使用,現在,我們先從位元和位元組回退一步,把遞迴看作一種通用技術。我們的階乘演算法可歸結為:將整數 N、N-1、 … 1 推入到一個棧,然後將它們按相反的順序相乘。實際上我們使用了程式呼叫棧來實現這一點,這是它的細節:我們在堆上分配一個棧並使用它。雖然呼叫棧具有特殊的特性,但是它也只是又一種資料結構而已,你可以隨意使用。我希望這個示意圖可以讓你明白這一點。
當你將棧呼叫視為一種資料結構,有些事情將變得更加清晰明瞭:將那些整數堆積起來,然後再將它們相乘,這並不是一個好的想法。那是一種有缺陷的實現:就像你拿螺絲刀去釘釘子一樣。相對更合理的是使用一個迭代過程去計算階乘。
但是,螺絲釘太多了,我們只能挑一個。有一個經典的面試題,在迷宮裡有一隻老鼠,你必須幫助這隻老鼠找到一個乳酪。假設老鼠能夠在迷宮中向左或者向右轉彎。你該怎麼去建模來解決這個問題?
就像現實生活中的很多問題一樣,你可以將這個老鼠找乳酪的問題簡化為一個圖,一個二叉樹的每個結點代表在迷宮中的一個位置。然後你可以讓老鼠在任何可能的地方都左轉,而當它進入一個死衚衕時,再回溯回去,再右轉。這是一個老鼠行走的 迷宮示例:
每到邊緣(線)都讓老鼠左轉或者右轉來到達一個新的位置。如果向哪邊轉都被攔住,說明相關的邊緣不存在。現在,我們來討論一下!這個過程無論你是呼叫棧還是其它資料結構,它都離不開一個遞迴的過程。而使用呼叫棧是非常容易的:
#include <stdio.h>
#include "maze.h"
int explore(maze_t *node)
{
int found = 0;
if (node == NULL)
{
return 0;
}
if (node->hasCheese){
return 1;// found cheese
}
found = explore(node->left) || explore(node->right);
return found;
}
int main(int argc)
{
int found = explore(&maze);
}
遞迴迷宮求解 下載
當我們在 maze.c:13
中找到乳酪時,棧的情況如下圖所示。你也可以在 GDB 輸出 中看到更詳細的資料,它是使用 命令 採集的資料。
它展示了遞迴的良好表現,因為這是一個適合使用遞迴的問題。而且這並不奇怪:當涉及到演算法時,遞迴是規則,而不是例外。它出現在如下情景中——進行搜尋時、進行遍歷樹和其它資料結構時、進行解析時、需要排序時——它無處不在。正如眾所周知的 pi 或者 e,它們在數學中像“神”一樣的存在,因為它們是宇宙萬物的基礎,而遞迴也和它們一樣:只是它存在於計算結構中。
Steven Skienna 的優秀著作 演算法設計指南 的精彩之處在於,他通過 “戰爭故事” 作為手段來詮釋工作,以此來展示解決現實世界中的問題背後的演算法。這是我所知道的擴充你的演算法知識的最佳資源。另一個讀物是 McCarthy 的 關於 LISP 實現的的原創論文。遞迴在語言中既是它的名字也是它的基本原理。這篇論文既可讀又有趣,在工作中能看到大師的作品是件讓人興奮的事情。
回到迷宮問題上。雖然它在這裡很難離開遞迴,但是並不意味著必須通過呼叫棧的方式來實現。你可以使用像 RRLL
這樣的字串去跟蹤轉向,然後,依據這個字串去決定老鼠下一步的動作。或者你可以分配一些其它的東西來記錄追尋乳酪的整個狀態。你仍然是實現了一個遞迴的過程,只是需要你實現一個自己的資料結構。
那樣似乎更復雜一些,因為棧呼叫更合適。每個棧幀記錄的不僅是當前節點,也記錄那個節點上的計算狀態(在這個案例中,我們是否只讓它走左邊,或者已經嘗試向右)。因此,程式碼已經變得不重要了。然而,有時候我們因為害怕溢位和期望中的效能而放棄這種優秀的演算法。那是很愚蠢的!
正如我們所見,棧空間是非常大的,在耗盡棧空間之前往往會遇到其它的限制。一方面可以通過檢查問題大小來確保它能夠被安全地處理。而對 CPU 的擔心是由兩個廣為流傳的有問題的示例所導致的:啞階乘和可怕的無記憶的 O( 2n ) Fibonacci 遞迴。它們並不是棧遞迴演算法的正確代表。
事實上棧操作是非常快的。通常,棧對資料的偏移是非常準確的,它在 快取 中是熱資料,並且是由專門的指令來操作它的。同時,使用你自己定義的在堆上分配的資料結構的相關開銷是很大的。經常能看到人們寫的一些比棧呼叫遞迴更復雜、效能更差的實現方法。最後,現代的 CPU 的效能都是 非常好的 ,並且一般 CPU 不會是效能瓶頸所在。在考慮犧牲程式的簡單性時要特別注意,就像經常考慮程式的效能及效能的測量那樣。
下一篇文章將是探祕棧系列的最後一篇了,我們將瞭解尾呼叫、閉包、以及其它相關概念。然後,我們就該深入我們的老朋友—— Linux 核心了。感謝你的閱讀!
via:https://manybutfinite.com/post/recursion/
作者:Gustavo Duarte 譯者:qhwdw 校對:FSSlc
相關文章
- 解密中國AI夢解密AI
- 白日夢
- 記夢
- JavaScript中的遞迴JavaScript遞迴
- 中國人的3A夢
- 遞迴和尾遞迴遞迴
- 《夢中的你 》:一部跨越千年夢境的懸疑劇
- SQL中的遞迴用法SQL遞迴
- 織夢資料庫_織夢還原資料庫_織夢資料庫很卡資料庫
- 《紅樓夢》中的宗教信仰
- 夢幻布丁
- 怪夢解析
- 思夢phpPHP
- 夢筆記筆記
- 達夢DIsqlSQL
- 快速排序【遞迴】【非遞迴】排序遞迴
- 寶可夢大電影《寶可夢:超夢的逆襲 進化》X《寶可夢大探險》聯動決定!
- 遞迴中Return例項分析遞迴
- CAD夢想畫圖中的“延伸命令”
- CAD夢想畫圖中的“分解命令”
- 創夢天地獲得夢工場授權 打造自研遊戲《夢工場大冒險》遊戲
- 夢境結構
- 達夢安裝
- 繪夢之卷
- 1222夢筆記筆記
- 關於夢想
- 遞迴遞迴
- 織夢手機網站模板修改,如何在織夢CMS中修改手機網站模板網站
- 【達夢】Docker安裝達夢資料庫 dm8Docker資料庫
- dedecms(織夢內容管理系統),又稱織夢cms
- “夢想江湖,從新出發”《新夢想世界》正式開啟
- 《美國逃亡者》讓人夢迴俯視視角 GTA 年代
- 體測,中國大學生的噩夢
- Unity實現“籠中窺夢”的渲染效果Unity
- html中的基本織夢標籤用方HTML
- 新世界的智慧,舊夢中的暖氣
- 論《紅樓夢》中的詩詞曲賦
- 織夢php資料庫配置檔案 織夢cms快速搭建PHP資料庫