生活随笔
收集整理的這篇文章主要介紹了
漫谈递归:从斐波那契开始了解尾递归
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
尾遞歸(tail recursive),看名字就知道是某種形式的遞歸。簡單的說遞歸就是函數自己調用自己。那尾遞歸和遞歸之間的差別就只能體現在參數上了。
尾遞歸wiki解釋如下:
尾部遞歸是一種編程技巧。遞歸函數是指一些會在函數內調用自己的函數,如果在遞歸函數中,遞歸調用返回的結果總被直接返回,則稱為尾部遞歸。尾部遞歸的函數有助將算法轉化成函數編程語言,而且從編譯器角度來說,亦容易優化成為普通循環。這是因為從電腦的基本面來說,所有的循環都是利用重復移跳到代碼的開頭來實現的。如果有尾部歸遞,就只需要疊套一個堆棧,因為電腦只需要將函數的參數改變再重新調用一次。利用尾部遞歸最主要的目的是要優化,例如在Scheme語言中,明確規定必須針對尾部遞歸作優化。可見尾部遞歸的作用,是非常依賴于具體實現的。
我們還是從簡單的斐波那契開始了解尾遞歸吧。
用普通的遞歸計算Fibonacci數列:
| 10 | ????printf("請輸入斐波那契數n:"); |
| 13 | ????rs = factorial(n); |
| 14 | ????printf("%d \n", rs); |
| 28 | ????????return?factorial(n-1) + factorial(n-2); |
程序員運行結果如下:
| 4 | Process returned 0 (0x0)?? execution?time?: 3.502 s |
| 5 | Press any key to?continue. |
在i5的CPU下也要花費 3.502 秒的時間。
下面我們看看如何用尾遞歸實現斐波那契數。
| 10 | ????printf("請輸入斐波那契數n:"); |
| 13 | ????rs = factorial_tail(n, 1, 1); |
| 14 | ????printf("%d ", rs); |
| 19 | int?factorial_tail(int?n,int?acc1,int?acc2) |
| 27 | ????????return?factorial_tail(n-1,acc2,acc1+acc2); |
程序員運行結果如下:
| 3 | Process returned 0 (0x0)?? execution?time?: 1.460 s |
| 4 | Press any key to?continue. |
快了一倍有多。當然這是不完全統計,有興趣的話可以自行計算大規模的值,這里只是介紹尾遞歸而已。
我們可以打印一下程序的執行過程,函數加入下面的打印語句:
| 01 | int?factorial_tail(int?n,int?acc1,int?acc2) |
| 09 | ????????printf("factorial_tail(%d, %d, %d) \n",n-1,acc2,acc1+acc2); |
| 10 | ????????return?factorial_tail(n-1,acc2,acc1+acc2); |
程序運行結果:
| 02 | factorial_tail(9, 1, 2) |
| 03 | factorial_tail(8, 2, 3) |
| 04 | factorial_tail(7, 3, 5) |
| 05 | factorial_tail(6, 5, 8) |
| 06 | factorial_tail(5, 8, 13) |
| 07 | factorial_tail(4, 13, 21) |
| 08 | factorial_tail(3, 21, 34) |
| 09 | factorial_tail(2, 34, 55) |
| 10 | factorial_tail(1, 55, 89) |
| 12 | Process returned 0 (0x0)?? execution?time?: 1.393 s |
| 13 | Press any key to?continue. |
從上面的調試就可以很清晰地看出尾遞歸的計算過程了。acc1就是第n個數,而acc2就是第n與第n+1個數的和,這就是我們前面講到的“迭代”的精髓,計算結果參與到下一次的計算,從而減少很多重復計算量。
fibonacci(n-1,acc2,acc1+acc2)真是神來之筆,原本樸素的遞歸產生的棧的層次像二叉樹一樣,以指數級增長,但是現在棧的層次卻像是數組,變成線性增長了,實在是奇妙,總結起來也很簡單,原本棧是先擴展開,然后邊收攏邊計算結果,現在卻變成在調用自身的同時通過參數來計算。
小結
尾遞歸的本質是:將單次計算的結果緩存起來,傳遞給下次調用,相當于自動累積。
在Java等命令式語言中,尾遞歸使用非常少見,因為我們可以直接用循環解決。而在函數式語言中,尾遞歸卻是一種神器,要實現循環就靠它了。
很多人可能會有疑問,為什么尾遞歸也是遞歸,卻不會造成棧溢出呢?因為編譯器通常都會對尾遞歸進行優化。編譯器會發現根本沒有必要存儲棧信息了,因而會在函數尾直接清空相關的棧。
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀
總結
以上是生活随笔為你收集整理的漫谈递归:从斐波那契开始了解尾递归的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。