由一道騰訊面試題引發的關於遞歸函數使用的各種情況總結

        一直有一個感受就是,當我們在某些問題抽象的定義解答時候感到疑惑是,不妨看一下具體問題的解答,更加有助於我們理解問題。首先看一下騰訊的一道招聘的測試題。

       1、面試題是一道程序編程題,要求使用遞歸的方法生成一個N位長度的格雷碼,對格雷罵的定義是,相鄰的兩個格雷碼只有一位的數字有差別。首先看一看我寫的關於該題目的C程序代碼:#include "stdio.h"

#include"stdlib.h"
void gray(int * a,int pos,int length);
void main()
{
int *a,n;
printf("輸入格雷碼的長度\n");
scanf("%d",&n);
a=(int *)malloc(n*sizeof(int));
gray(a,0,n);  //算法思想是,從數組的第1位開始爲數組依次賦值0和1,
//在每一種賦值情況下,都向下遞歸,給下一位賦值,直到數組的第n位賦值完畢,則輸出數組中的數字串。
}
void gray(int * a,int pos,int length)
{
int i=0;
if(pos<0||pos>length)
{
printf("數組下標操作越界!\n");
exit(1);
}
else if(pos==length)  //數組已經向下遞歸深度爲length,此時滿足輸出條件。
{
for(i=0;i<length;i++)
{
printf("%d",a[i]);
}
printf("\n");
}
else            //分別在不同的情況下,向下遞歸。
{
a[pos]=0;
gray(a,pos+1,length); //此處使用的是pos+1而不是pos++後,再調用遞歸函數自身,這是有區別的。
a[pos]=1;
gray(a,pos+1,length);
}

}

在該遞歸函數中主體分爲了三部分,第一部分是用來判斷下標是否越界,用以增強程序的魯棒性。第二部分是遞歸函數的唯一出口,即當數組a的N位都完成賦值,即輸出存儲的格雷碼。第三部分是遞歸函數未達到出口條件,還需要修改相關的變量,繼續向下遞歸。在此處分別對a【pos】,進行賦值0和1,然後分別遞歸下去。

       其實在這個gray()函數中,修改第三部分else中的相關內容,就可以生成一個N位長度,十進制數字的全排列組合。處理方法即是分別對a【pos】賦值0.....9,然後在每一種賦值情況下分別遞歸下去。

2、下面看一個關於遞歸方法求二叉樹的深度。

int DepthBiTree_digui(SBT * T,int n,int * depth) // n表示此時遞歸深度,即樹的深度,depth用來記錄樹的最終深度
{
if(!T)
* depth=0;
else
{
if(T->lchild)
DepthBiTree_digui(1,T->lchild,n+1,&(* depth));
if(T->rchild)
DepthBiTree_digui(1,T->rchild,n+1,&(* depth));
if(* depth<n)
* depth=n;
}
return ok;
}

在這個函數中,遞歸是否向下繼續遞歸,則是根據所在根節點是否有左右子樹爲依據,而函數的出口有兩個,一個是空樹,即返回深度爲0,一個是所在根節點沒有左右子樹,此刻檢查此時深度,是否比以depth記錄的更深,如果更深,則更新depth的值。在這種方法中,是通過先序遍歷,來完成整個二叉樹的遍歷,在遍歷過程中求得二叉樹的深度。

3、二叉樹的三種遞歸遍歷算法

void PreOrder(BiTree T)  //先序遍歷算法
{
if(T)
{
visit(T);  //訪問根節點
PreOrder(BiTree T->lchild);  //遞歸遍歷左子樹
        PreOrder(BiTree T->rchild);  //遞歸遍歷右子樹
}
}
void InOrder(BiTree T)  //中序遍歷算法
{
if(T)
{
InOrder(BiTree T->lchild);  //遞歸遍歷左子樹
visit(T);  //訪問根節點
        InOrder(BiTree T->rchild);  //遞歸遍歷右子樹
}
}
void PostOrder(BiTree T) //後序遍歷算法
{
if(T)
{
PostOrder(BiTree T->lchild);  //遞歸遍歷左子樹
        PostOrder(BiTree T->rchild);  //遞歸遍歷右子樹
visit(T);  //訪問根節點
}
}

4、記得以前看過一個農夫過河的問題,該問題也可以很輕易的用遞歸算法來求解,每次在經過判斷後,對可行解分別進行遞歸向下繼續深入,當達到了過河的目的後,即輸出整個路勁即可,在該問題的解答中,即是對遞歸方法的深入和出口都做了限定處理。

總結:Func()
{
if(/*滿足一定條件,不再遞歸*/)
{
//此處即是遞歸函數的出口,可以根據具體問題做相應處理
}
else
{
//在這裏是函數遞歸未達到一定要求,還需要向下繼續遞歸,在遞歸前,可對相應遞歸條件進行處理。
}
}

由此可見,遞歸方法的使用是很靈活的,根據我們解決的不同問題,我們可以對相應部分進行修改。但是可以總結出遞歸函數大體包括兩個部分,如上述func函數代碼,包括一個遞歸深入的入口,和一個滿足條件的出口。這兩部分是具有很大的靈活性,我們做不同的處理,可以得到不同的效果。現在反過來看遞歸函數的定義:直接或間接調用函數本身,則該函數稱爲遞歸函數。是不是就好理解一些。

注意:但也應該注意,遞歸函數雖然使用起來易於理解和形式簡單,但其實現原理是程序內部的棧,因此大量複雜的遞歸調用,會消耗很多資源,影響程序的效率。通常能用遞歸方法處理的問題,也可以使用循環來處理,只是循環處理在形式上更加複雜和難以理解,但循環方法沒有函數調用傳值這些,因此效率要高一些。

發佈了28 篇原創文章 · 獲贊 35 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章