什麼是遞迴?
之前說到,遞迴是一種將大問題分解為小問題的解決方案。一般來說,遞迴被稱為函式自身的呼叫。這麼說可能聽起來很奇怪,事實上在遞迴中,函式確實必須呼叫自己。
一個栗子
例如在數學中,我們都知道“階乘”的概念。例如5的階乘就是5*4*3*2*1
。
- 5!= 5 * 4!
- 4!= 4 * 3!
- 3!= 3 * 2!
- 2!= 2 * 1!
- 1!= 1 * 0!
- 0!= 1
我們可以總結出求n的階乘的規律,即 n! = n * (n -1) !
這就體現了遞迴。你可以從中發現,我們把求5的階乘一步一步轉化成了另外一個個的小問題。
遞迴演算法的特性
- 每一個遞迴呼叫都必須基於一個小的子問題。例如5的階乘就是5乘4的階乘。
- 遞迴必須有一個Base case。例如階乘的Base case就是0,當條件是0的時候,就停止遞迴。
- 遞迴中避免迴圈呼叫,否則最後計算機會顯示棧溢位的錯誤。
function factorial(int $n): int
{
if ($n = 0) {
return 1;
}
return $n * factorial($n - 1);
}
看上面的程式碼,我們可以看到對於階乘問題的解決方案我們有一個基礎的條件就是當n為0的時候,我們返回1。如果不符合這個條件,我們返回n
乘 factorial(n)
,這符合遞迴特性的第一條和第三條。我們避免了迴圈呼叫,因為我們把每一次的遞迴呼叫都分解成了大問題的一個小的子問題。上面的演算法思想可以表達成:
遞迴Vs迭代
上面的遞迴程式碼我們同樣可以使用迭代的方法實現
function factorial(int $n): int
{
$result = 1;
for ($i = $n; $i > 0; $i--) {
$result*= $n;
}
return $result;
}
如果一個問題可以很容易的使用迭代來解決,我們為何要使用遞迴?
遞迴是用來處理更加複雜的問題的,不是所有的問題都可以簡單的使用迭代來解決的。遞迴使用函式呼叫來管理呼叫棧,所以相比於迭代遞迴會使用更多和時間以及記憶體。此外,在迭代中,我們每一步都會有一個結果,但是在遞迴中我們必須等到base case執行結束才會有任何結果。看上面的例子,我們發現在遞迴演算法中我們沒有任何變數或者宣告來儲存結果,而在迭代演算法中,我們每一次都用$result來儲存了返回結果。
斐波那契數列
在數學中,斐波那契數列是一個特殊的整數數列,數列中的每一個數的是由另外兩個數求和產生的。規則如下:
function fibonacci($n)
{
if ($n == 0) {
return 0;
}
if ($n == 1) {
return 1;
}
return fibonacci($n - 1) + fibonacci($ - 2);
}
最大公因數
另外一個使用遞迴演算法的常見問題是求兩個數的最大公因數。
function gcd(int $a, int $b)
{
if ($b == 0) {
return $a;
}
return gcd($b, $a % $b);
}
遞迴型別
- 線性遞迴
在每一次遞迴呼叫中,函式只呼叫自己一次,這就叫做線性遞迴。
- 二分遞迴
在二分遞迴中,每一次遞迴呼叫函式呼叫自己兩次。求解斐波那契數列的演算法就是二分遞迴,除此之外還有二分查詢、分治演算法、歸併排序等也使用了二分遞迴。
- 尾遞迴
當一個遞迴返回的時候沒有等待的操作的時候就稱為尾遞迴。斐波那契演算法中,返回值需要乘以前一個遞迴的返回值,因此他不是尾遞迴,而求解最大公因式的演算法是尾遞迴。尾遞迴是線性遞迴的一種形式。
- 相互遞迴
例如在每一次遞迴呼叫中 有 A() 呼叫 B(), B() 呼叫 A() ,這樣的遞迴就叫做相互遞迴。
- 巢狀遞迴
當一個遞迴函式把自己作為一個引數進行遞迴呼叫時,就叫做巢狀遞迴。一個常見的栗子就是阿克曼函式,看下面的表達。
看最後一行的,可以看到第二個引數就是遞迴函式自己。
下一節
下一篇內容會使用遞迴解決一些實際開發中會遇到的問題,例如構建N級分類、構建巢狀評論、目錄檔案的遍歷等等。
更多內容
PHP基礎資料結構專題系列目錄地址:地址 主要使用PHP語法總結基礎的資料結構和演算法。還有我們日常PHP開發中容易忽略的基礎知識和現代PHP開發中關於規範、部署、最佳化的一些實戰性建議,同時還有對Javascript語言特點的深入研究。
本作品採用《CC 協議》,轉載必須註明作者和本文連結