遞歸算法總結(jié)

之前分享了一篇隨機(jī)算法,這次再把以前寫的遞歸算法的文章梳理一下,這篇文章主要是受到宋勁松老師寫的《Linux C編程》的遞歸章節(jié)啟發(fā)寫的屑柔。最能體現(xiàn)算法精髓的非遞歸莫屬了沉帮,希望這篇文章對(duì)初學(xué)遞歸或者對(duì)遞歸不甚了解的筒子們能有所幫助锈死,也懇請(qǐng)各路大牛指正。

1 遞歸算法初探

本段內(nèi)容大部分摘自《linux C一站式編程》穆壕,作者是宋勁松老師待牵,我認(rèn)為這是目前看到的國(guó)內(nèi)關(guān)于linux C編程的最好的一本技術(shù)書籍,強(qiáng)烈推薦喇勋!

關(guān)于遞歸的一個(gè)簡(jiǎn)單例子是求整數(shù)階乘缨该,n!=n*(n-1)!,0!=1 川背。則可以寫出如下的遞歸程序:

int factorial(int n)
{
    if (n == 0)
        return 1;
    else {
        int recurse = factorial(n-1);
        int result = n * recurse;
        return result;
    }
}

factorial這個(gè)函數(shù)就是一個(gè)遞歸函數(shù)贰拿,它調(diào)用了它自己。自己直接或間接調(diào)用自己的函數(shù)稱為遞歸函數(shù)渗常。如果覺得迷惑壮不,可以把factorial(n-1)這一步看成是在調(diào)用另一個(gè)函數(shù)--另一個(gè)有著相同函數(shù)名和相同代碼的函數(shù),調(diào)用它就是跳到它的代碼里執(zhí)行皱碘,然后再返回factorial(n-1)這個(gè)調(diào)用的下一步繼續(xù)執(zhí)行询一。

為了證明遞歸算法的正確性,我們可以一步步跟進(jìn)去看執(zhí)行結(jié)果。記得剛學(xué)遞歸算法的時(shí)候健蕊,老是有丈二和尚摸不著頭腦的感覺菱阵,那時(shí)候總是想著把遞歸一步步跟進(jìn)去看執(zhí)行結(jié)果。遞歸層次少還算好辦缩功,但是層次一多晴及,頭就大了,完全不知道自己跟到了遞歸的哪一層嫡锌。比如求階乘虑稼,如果只是factorial(3)跟進(jìn)去問題還不大,但是若是factorial(100)要跟進(jìn)去那真的會(huì)煩死人势木。

事實(shí)上蛛倦,我們并不是每個(gè)函數(shù)都需要跟進(jìn)去看執(zhí)行結(jié)果的,比如我們?cè)谧约旱暮瘮?shù)中調(diào)用printf函數(shù)時(shí)啦桌,并沒有鉆進(jìn)去看它是怎么打印的溯壶,因?yàn)槲覀兿嘈潘芡瓿纱蛴」ぷ鳌?/strong>我們?cè)趯慺actorial函數(shù)時(shí)有如下代碼:

...
int recurse = factorial(n-1);
int result = n * recurse;
...

這時(shí),如果我們相信factorial是正確的甫男,那么傳遞參數(shù)為n-1它就會(huì)返回(n-1)!且改,那么result=n*(n-1)!=n!,從而這就是factorial(n)的結(jié)果板驳。

當(dāng)然這有點(diǎn)奇怪:我們還沒寫完factorial這個(gè)函數(shù)又跛,憑什么要相信factorial(n-1)是正確的?如果你相信你正在寫的遞歸函數(shù)是正確的笋庄,并調(diào)用它效扫,然后在此基礎(chǔ)上寫完這個(gè)遞歸函數(shù),那么它就會(huì)是正確的直砂,從而值得你相信它正確菌仁。

這么說還是有點(diǎn)玄乎,我們從數(shù)學(xué)上嚴(yán)格證明一下factorial函數(shù)的正確性静暂。剛才說了济丘,factorial(n)的正確性依賴于factorial(n-1)的正確性,只要后者正確洽蛀,在后者的結(jié)果上乘個(gè)n返回這一步顯然也沒有疑問摹迷,那么我們的函數(shù)實(shí)現(xiàn)就是正確的。因此要證明factorial(n)的正確性就是要證明factorial(n-1)的正確性郊供,同理峡碉,要證明factorial(n-1)的正確性就是要證明factorial(n-2)的正確性,依此類推下去驮审,最后是:要證明factorial(1)的正確性就是要證明factorial(0)的正確性鲫寄。而factorial(0)的正確性不依賴于別的函數(shù)調(diào)用吉执,它就是程序中的一個(gè)小的分支return 1;這個(gè)1是我們根據(jù)階乘的定義寫的,肯定是正確的地来,因此factorial(1)的實(shí)現(xiàn)是正確的戳玫,因此factorial(2)也正確,依此類推未斑,最后factorial(n)也是正確的咕宿。

其實(shí)這就是在中學(xué)時(shí)學(xué)的數(shù)學(xué)歸納法,用數(shù)學(xué)歸納法來證明只需要證明兩點(diǎn):Base Case正確蜡秽,遞推關(guān)系正確府阀。寫遞歸函數(shù)時(shí)一定要記得寫B(tài)ase Case,否則即使遞推關(guān)系正確芽突,整個(gè)函數(shù)也不正確肌似。如果factorial函數(shù)漏掉了Base Case,那么會(huì)導(dǎo)致無限循環(huán)诉瓦。

2 遞歸經(jīng)典問題

從上一節(jié)的一個(gè)關(guān)于求階乘的簡(jiǎn)單例子的論述,我們可以了解到遞歸算法的精髓:要從功能上理解函數(shù)力细,同時(shí)你要相信你正在寫的函數(shù)是正確的睬澡,在此基礎(chǔ)上調(diào)用它,那么它就是正確的眠蚂。下面就從幾個(gè)常見的算法題來看看如何理解遞歸煞聪,這是我的一些理解,歡迎大家提出更好的方法逝慧。

2.1)漢諾塔問題

漢諾塔問題是個(gè)常見問題昔脯,就是說有n個(gè)大小不等的盤子放在一個(gè)塔A上面,自底向上按照從小到大的順序排列笛臣。要求將所有n個(gè)盤子搬到另一個(gè)塔C上面云稚,可以借助一個(gè)塔B中轉(zhuǎn),但是要滿足任何時(shí)刻大盤子不能放在小盤子上面沈堡。

基本思想分三步静陈,先把上面的N-1個(gè)盤子經(jīng)C移到B,然后將最底下的盤子移到C诞丽,再講B上面的N-1個(gè)盤子經(jīng)A移動(dòng)到C鲸拥。總的時(shí)間復(fù)雜度f(n)=2f(n-1)+1僧免,所以f(n)=2^n-1刑赶。

void hano(char a, char b, char c, int n) {
    if (n > 0) {
        hano(a, c, b, n-1);
        move(a, c);
        hano(b, a, c, n-1);
    }
}

void move(char a, char b)
{
    cout << a << "->" << b << endl;
}

2.2)求二叉樹的深度

這里的深度指的是二叉樹從根結(jié)點(diǎn)到葉結(jié)點(diǎn)最大的高度,比如只有一個(gè)結(jié)點(diǎn)懂衩,則深度為1撞叨,如果有N層金踪,則高度為N。

int depth(struct node* root)  
{  
    if (root == NULL)  
        return 0;  
    else {  
        int lDepth = depth(root->left);  //獲取左子樹深度  
        int rDepth = depth(root->right); //獲取右子樹深度  
        return lDepth>rDepth? lDepth+1: rDepth+1; //取較大值+1即為二叉樹深度  
    }  
}  

那么如何從功能上理解depth函數(shù)呢谒所?我們可以知道定義該函數(shù)的目的就是求二叉樹深度热康,也就是說我們要是完成了函數(shù)depth,那么depth(root)就能正確返回以root為根結(jié)點(diǎn)的二叉樹的深度劣领。因此我們的代碼中depth(root->left)返回左子樹的深度姐军,而depth(root->right)返回右子樹的深度。盡管這個(gè)時(shí)候我們還沒有寫完depth函數(shù)尖淘,但是我們相信depth函數(shù)能夠正確完成功能奕锌。因此我們得到了lDepth和rDepth,而后通過比較返回較大值加1為二叉樹的深度村生。如果不好理解惊暴,可以想象在depth中調(diào)用的函數(shù)depth(root->left)為另外一個(gè)同樣名字完成相同功能的函數(shù),這樣就好理解了趁桃。注意Base Case辽话,這里就是當(dāng)root==NULL時(shí),則深度為0卫病,函數(shù)返回0油啤。

2.3)判斷二叉樹是否平衡

一顆平衡的二叉樹是指其任意結(jié)點(diǎn)的左右子樹深度之差不大于1。判斷一棵二叉樹是否是平衡的蟀苛,可以使用遞歸算法來實(shí)現(xiàn)益咬。

bool is_balanced(BinaryTreeNode* pRoot)
{
    if(pRoot == NULL) //基本情況,為空的話帜平,返回true
        return true;
 
    int left = depth(pRoot->m_pLeft);
    int right = depth(pRoot->m_pRight);
    int diff = left - right; //計(jì)算左右子樹深度之差
    if(diff > 1 || diff < -1) //如果深度之差大于1返回false
        return false;
 
    return is_balanced(pRoot->m_pLeft) && is_balanced(pRoot->m_pRight); //遞歸判斷左右子樹幽告,注意是&&,即左右子樹都必須是平衡的這棵二叉樹才是平衡的
}

該函數(shù)的功能定義是二叉樹pRoot是平衡二叉樹裆甩,即它所有結(jié)點(diǎn)的左右子樹深度之差不大于1冗锁。首先判斷根結(jié)點(diǎn)是否滿足條件,如果不滿足嗤栓,則直接返回false蒿讥。如果滿足,則需要判斷左子樹和右子樹是否都是平衡二叉樹抛腕,若都是則返回true芋绸,否則false。

上面代碼性能不高担敌,會(huì)重復(fù)遍歷結(jié)點(diǎn)摔敛,一個(gè)改進(jìn)的算法是采用后序遍歷的方式遍歷樹的結(jié)點(diǎn),在遍歷到本結(jié)點(diǎn)前我們已經(jīng)遍歷完了它的左右子樹全封,我們只需要在遍歷的時(shí)候記錄結(jié)點(diǎn)的深度马昙,就可以一邊遍歷一邊判斷該結(jié)點(diǎn)是否是平衡的桃犬。代碼如下:

bool is_balanced_2(BinaryTreeNode* pRoot, int* pDepth)
{
    if(pRoot == NULL)
    {
        *pDepth = 0;
        return true;
    }
 
    int left, right;
    if(is_balanced_2(pRoot->m_pLeft, &left) //左子樹平衡
        && is_balanced_2(pRoot->m_pRight, &right)) //右子樹平衡
    {
        int diff = left - right;
        if(diff <= 1 && diff >= -1)
        {
            *pDepth = 1 + (left > right ? left : right);
            return true;
        }
    }
 
    return false;
}

該函數(shù)功能定義是返回以pRoot為根的二叉樹是否是平衡二叉樹,同時(shí)把樹的深度保存在pDepth指向的值中行楞≡芟荆基本情況是樹為NULL,則深度為0子房,返回true形用。否則只有左右子樹都是平衡的情況下,深度分別存在變量left和right中证杭,判斷左右子樹的深度之差是否不大于1田度,如果是則返回true,注意還要設(shè)置樹的深度值解愤。

調(diào)用的函數(shù)定義如下:

bool IsBalanced(BinaryTreeNode* pRoot)
{
    int depth = 0;
    return is_balanced_2(pRoot, &depth);
}

2.4)排列算法

排列算法也是遞歸的典范镇饺,記得當(dāng)初第一次看時(shí)一層層跟代碼,頭都大了送讲,現(xiàn)在從函數(shù)功能上來看確實(shí)好理解多了奸笤。先看代碼:

void perm(int a[], int k, int N) { //k為起始位置,N為數(shù)組大小
    if (k == N-1) { 
        output(a, N); //輸出排列
    } else {
        for (int i=k; i<N; i++) {
            swap(a, i, k); //交換
            perm(a, k+1, N); //下一次排列
            swap(a, i, k); //恢復(fù)原來的序列
        }
    }
}

首先明確的是perm(a, k, N)函數(shù)的功能:輸出數(shù)組a從位置k開始的所有排列哼鬓,數(shù)組長(zhǎng)度為N揭保。這樣我們?cè)谡{(diào)用程序的時(shí)候,調(diào)用格式為perm(a, 0, N)魄宏,即輸出數(shù)組從位置0開始的所有排列,也就是該數(shù)組的所有排列存筏〕杌ィ基礎(chǔ)條件是k==N-1,此時(shí)已經(jīng)到達(dá)最后一個(gè)元素椭坚,一次排列已經(jīng)完成予跌,直接輸出。否則善茎,從位置k開始的每個(gè)元素都與位置k的值交換(包括自己與自己交換)券册,然后進(jìn)行下一次排列,排列完成后記得恢復(fù)原來的序列垂涯。

假定數(shù)組a大小N=3烁焙,則程序調(diào)用perm(a, 0, 3)可以如下理解:
第一次交換0,0耕赘,并執(zhí)行perm(a, 1, 3)骄蝇,執(zhí)行完再次交換0,0操骡,數(shù)組此時(shí)又恢復(fù)成初始值九火。
第二次交換1赚窃,0(注意數(shù)組此時(shí)是初始值),并執(zhí)行perm(a, 1, 3), 執(zhí)行完再次交換1岔激,0勒极,數(shù)組此時(shí)又恢復(fù)成初始值。
第三次交換2虑鼎,0辱匿,并執(zhí)行perm(a, 1, 3),執(zhí)行完成后交換2震叙,0掀鹅,數(shù)組恢復(fù)成初始值。

也就是說媒楼,從功能上看乐尊,首先確定第0個(gè)位置,然后調(diào)用perm(a, 1, 3)輸出從1開始的排列划址,這樣就可以輸出所有排列扔嵌。而第0個(gè)位置可能的值為a[0], a[1],a[2],這通過交換來保證第0個(gè)位置可能出現(xiàn)的值夺颤,記得每次交換后要恢復(fù)初始值痢缎。

如數(shù)組a={1,2,3},則程序運(yùn)行輸出結(jié)果為:1 2 3 世澜,1 3 2 独旷,2 1 3 ,2 3 1 寥裂,3 2 1 嵌洼,3 1 2 。即先輸出以1為排列第一個(gè)值的排列封恰,而后是2和3為第一個(gè)值的排列麻养。

2.5)組合算法

組合算法也可以用遞歸實(shí)現(xiàn),只是它的原理跟0-1背包問題類似诺舔。即要么選要么不選鳖昌,注意不能選重復(fù)的數(shù)。完整代碼如下:

#include<iostream>
using namespace std;
#define N 3  //數(shù)組大小為3
int select[N] = {0}; //選擇數(shù)組低飒,用于存儲(chǔ)數(shù)組哪些數(shù)字被選中许昨。
/*輸出數(shù)組中選中的數(shù)*/
void output(int a[], int n)
{
    for (int i=0; i<n; i++) {
        if (select[i])
            cout << a[i] << " ";
    }
    cout << endl;
}
/*數(shù)組a從位置i開始選取k個(gè)數(shù)*/
void combination(int a[], int i, int k)
{
    if (i > N) return; //位置超出數(shù)組范圍直接返回,否則非法訪問會(huì)出段錯(cuò)誤
    if (k == 0) {  //選取完了褥赊,輸出選取的數(shù)字
        output(a, N);
    } else {
        select[i] = 1;  
        combination(a, i+1, k-1); //第i個(gè)數(shù)字被選取车要,從后續(xù)i+1開始選取k-1個(gè)數(shù)
        select[i] = 0;
        combination(a, i+1, k); //第i個(gè)數(shù)字不選,則從后續(xù)i+1位置開始還要選取k個(gè)數(shù)
    }
}

/*組合主函數(shù)崭倘,包括選取1到n個(gè)數(shù)字*/
void combination_helper(int a[], int n) {
    for (int k=1; k<=n; k++) {
        combination(a, 0, k);
    }
}

int main()
{
    int a[N] = {1, 2, 3};
    combination_helper(a, N);
    return 0;
}

2.6) 逆序打印字符串

這個(gè)比較簡(jiǎn)單翼岁,代碼如下:

void printReverse(const char *str) {
  if (!*str)
    return;
  printReverse(str + 1);
  putchar(*str);
}

3 多說一句

對(duì)遞歸有興趣的类垫,還可以看看這篇文章,用遞歸實(shí)現(xiàn)鏈表逆序琅坡。

參考資料

  • 宋勁松《Linux C編程》遞歸章節(jié)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末悉患,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子榆俺,更是在濱河造成了極大的恐慌售躁,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件茴晋,死亡現(xiàn)場(chǎng)離奇詭異陪捷,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)诺擅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門市袖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人烁涌,你說我怎么就攤上這事苍碟。” “怎么了撮执?”我有些...
    開封第一講書人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵微峰,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我抒钱,道長(zhǎng)蜓肆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任谋币,我火速辦了婚禮仗扬,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘瑞信。我一直安慰自己,他們只是感情好穴豫,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開白布凡简。 她就那樣靜靜地躺著,像睡著了一般精肃。 火紅的嫁衣襯著肌膚如雪秤涩。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,208評(píng)論 1 299
  • 那天司抱,我揣著相機(jī)與錄音筐眷,去河邊找鬼。 笑死习柠,一個(gè)胖子當(dāng)著我的面吹牛匀谣,可吹牛的內(nèi)容都是我干的照棋。 我是一名探鬼主播,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼武翎,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼烈炭!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起宝恶,我...
    開封第一講書人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤符隙,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后垫毙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體霹疫,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年综芥,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了丽蝎。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡毫痕,死狀恐怖征峦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情消请,我是刑警寧澤栏笆,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站臊泰,受9級(jí)特大地震影響蛉加,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜缸逃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一针饥、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧需频,春花似錦丁眼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至挪丢,卻和暖如春蹂风,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背乾蓬。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工惠啄, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓撵渡,卻偏偏與公主長(zhǎng)得像融柬,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子姥闭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 定義指針變量丹鸿,如果不賦給它地址,系統(tǒng)會(huì)隨機(jī)給它分配一個(gè)地址棚品。 C++標(biāo)準(zhǔn)庫(kù) C++ Standard Librar...
    縱我不往矣閱讀 290評(píng)論 0 1
  • B樹的定義 一棵m階的B樹滿足下列條件: 樹中每個(gè)結(jié)點(diǎn)至多有m個(gè)孩子靠欢。 除根結(jié)點(diǎn)和葉子結(jié)點(diǎn)外,其它每個(gè)結(jié)點(diǎn)至少有m...
    文檔隨手記閱讀 13,219評(píng)論 0 25
  • 課程介紹 先修課:概率統(tǒng)計(jì)铜跑,程序設(shè)計(jì)實(shí)習(xí)门怪,集合論與圖論 后續(xù)課:算法分析與設(shè)計(jì),編譯原理锅纺,操作系統(tǒng)掷空,數(shù)據(jù)庫(kù)概論,人...
    ShellyWhen閱讀 2,283評(píng)論 0 3
  • 第一章 緒論 什么是數(shù)據(jù)結(jié)構(gòu)囤锉? 數(shù)據(jù)結(jié)構(gòu)的定義:數(shù)據(jù)結(jié)構(gòu)是相互之間存在一種或多種特定關(guān)系的數(shù)據(jù)元素的集合坦弟。 第二章...
    SeanCheney閱讀 5,771評(píng)論 0 19
  • 1.把二元查找樹轉(zhuǎn)變成排序的雙向鏈表 題目: 輸入一棵二元查找樹,將該二元查找樹轉(zhuǎn)換成一個(gè)排序的雙向鏈表官地。 要求不...
    曲終人散Li閱讀 3,313評(píng)論 0 19