在知乎上面搜索遞歸,但是普遍的回答是業務開發中不常涉及,和for循環差不多,消耗性能太大,不推薦使用。本着不服管的性格,我差了一些有用的資料,和大家分享下,遞歸的算法和使用場景。
爲什麼要用遞歸
編程裏面估計最讓人摸不着頭腦的基本算法就是遞歸了。很多時候我們看明白一個複雜的遞歸都有點費時間,尤其對模型所描述的問題概念不清的時候,想要自己設計一個遞歸那麼就更是有難度了。
很多不理解遞歸的人(今天在csdn裏面看到一個初學者的留言),總認爲遞歸完全沒必要,用循環就可以實現,其實這是一種很膚淺的理解。因爲遞歸之所以在程序中能風靡並不是因爲他的循環,大家都知道遞歸分兩步,遞和歸,那麼可以知道遞歸對於空間性能來說,簡直就是造孽,這對於追求時空完美的人來說,簡直無法接接受,如果遞歸僅僅是循環,估計現在我們就看不到遞歸了。遞歸之所以現在還存在是因爲遞歸可以產生無限循環體,也就是說有可能產生100層也可能10000層for循環。例如對於一個字符串進行全排列,字符串長度不定,那麼如果你用循環來實現,你會發現你根本寫不出來,這個時候就要調用遞歸,而且在遞歸模型裏面還可以使用分支遞歸,例如for循環與遞歸嵌套,或者這節枚舉幾個遞歸步進表達式,每一個形成一個遞歸。
用歸納法來理解遞歸
數學都不差的我們,第一反應就是遞歸在數學上的模型是什麼。畢竟我們對於問題進行數學建模比起代碼建模拿手多了。 (當然如果對於問題很清楚的人也可以直接簡歷遞歸模型了,運用數模做中介的是針對對於那些問題還不是很清楚的人)
自己觀察遞歸,我們會發現,遞歸的數學模型其實就是歸納法,這個在高中的數列裏面是最常用的了。回憶一下歸納法。
歸納法適用於想解決一個問題轉化爲解決他的子問題,而他的子問題又變成子問題的子問題,而且我們發現這些問題其實都是一個模型,也就是說存在相同的邏輯歸納處理項。當然有一個是例外的,也就是遞歸結束的哪一個處理方法不適用於我們的歸納處理項,當然也不能適用,否則我們就無窮遞歸了。這裏又引出了一個歸納終結點以及直接求解的表達式。如果運用列表來形容歸納法就是:
- 步進表達式:問題蛻變成子問題的表達式
- 結束條件:什麼時候可以不再是用步進表達式
- 直接求解表達式:在結束條件下能夠直接計算返回值的表達式
- 邏輯歸納項:適用於一切非適用於結束條件的子問題的處理,當然上面的步進表達式其實就是包含在這裏面了。
void func( mode)
{
if(endCondition)
{
constExpression //基本項
}
else
{
accumrateExpreesion //歸納項
mode=expression //步進表達式
func(mode) //調用本身,遞歸
}
}
最典型的就是N!算法,這個最具有說服力。理解了遞歸的思想以及使用場景,基本就能自己設計了,當然要想和其他算法結合起來使用,還需要不斷實踐與總結了。
#include "stdio.h"
#include "math.h"
int main(void)
{
int n, rs;
printf("請輸入需要計算階乘的數n:");
scanf("%d",&n);
rs = factorial(n);
printf("%d ", rs);
}
// 遞歸計算過程
int factorial(n){
if(n == 1) {
return 1;
}
return n * factorial(n-1);
}
再來兩個遞歸的例子
返回一個二叉樹的深度:
depth(Tree t){
if(!t) return 0;
else {
int a=depth(t.right);
int b=depth(t.left);
return (a>b)?(a+1):(b+1);
}
}
判斷一個二叉樹是否平衡:
int isB(Tree t){
if(!t) return 0;
int left=isB(t.left);
int right=isB(t.right);
if( left >=0 && right >=0 && left - right <= 1 || left -right >=-1)
return (left < right)? (right +1) : (left + 1);
else return -1;
}
第一個算法還是比較好理解的,但第二個就不那麼好理解了。第一個算法的思想是:如果這個樹是空,則返回0;否則先求左邊樹的深度,再求右邊數的深度,然後對這兩個值進行比較哪個大就取哪個值+1。而第二個算法,首先應該明白isB函數的功能,它對於空樹返回0,對於平衡樹返回樹的深度,對於不平衡樹返回-1。明白了函數的功能再看代碼就明白多了,只要有一個函數返回了-1,則整個函數就會返回-1。(具體過程只要認真看下就明白了)
對於遞歸,最好的理解方式便是從函數的功能意義的層面來理解。瞭解一個問題如何被分解爲它的子問題,這樣對於遞歸函數代碼也就理解了。這裏有一個誤區(我也曾深陷其中),就是通過分析堆棧,分析一個一個函數的調用過程、輸出結果來分析遞歸的算法。這是十分要不得的,這樣只會把自己弄暈,其實遞歸本質上也是函數的調用,調用的函數是自己或者不是自己其實沒什麼區別。在函數調用時總會把一些臨時信息保存到堆棧,堆棧只是爲了函數能正確的返回,僅此而已。我們只要知道遞歸會導致大量的函數調用,大量的堆棧操作就可以了。
小結
遞歸的基本思想是把規模大的問題轉化爲規模小的相似的子問題來解決。在函數實現時,因爲解決大問題的方法和解決小問題的方法往往是同一個方法,所以就產生了函數調用它自身的情況。另外這個解決問題的函數必須有明顯的結束條件,這樣就不會產生無限遞歸的情況了。