C/C++ 递归
遞歸
當一個函數調用它自己來定義時稱它為遞歸函數。(什么叫它自己調用它自己呢?)
1.1、引出遞歸
從一個簡單的問題考慮遞歸,求0,1,2, 3,4,5......n的和。
首先定義一個求和公式:sum(n);
顯然對于(n > 0): sum(n) = sum(n - 1) + n ;
? (n = 0 ) : sum(0) = 0;
? 成立。
將上述公式翻譯成C++函數:
unsigned int sum(unsigned int n)
{
if(0 == n)
{
return 0; //基準情況(遞歸的出口),sum不能一直調用它自己吧,總歸要有一個出口結束遞歸吧
}
else
{
return sum(n - 1) + n; //sum(unsigned int)調用了它自己
}
}
假設 n = 5 分析一下計算過程:
sum(5) = sum(4) + 5;
sum(4) = sum(3) + 4;
sum(3) = sum(2) + 3;
sum(2) = sum(1) + 2;
sum(1) = sum(0) + 1;
sum(0) = 0; 當sum(0)時,sum()不再調用它自己,作為遞歸的出口結束遞歸。
假設沒有n = 0, sum(0) = 0 這個基準情況作為遞歸的出口跳出遞歸,遞歸就會一直遞歸下去,沒完沒了直至崩潰。因此遞歸函數必須有一個基準情況作為遞歸出口。
1.2、失敗的遞歸
給出一個所謂的遞歸函數:
int bad(unsigned int n)
{
if(0 == n)
{
return 0;
}
else
{
return bad(n/3 + 1) + n - 1;
}
}
分析一下以上函數,函數給出了 n = 0 的情況作為遞歸的出口,看似沒什么問題。
還是假設n = 5;
bad(5) : 調用bad(5/3 + 1), 即bad(2);
bad(2) : 調用bad(2/3 + 1), 即bad(1);
bad(1) : 調用bad(1/3 + 1), 即bad(1);
bad(1) : 調用bad(1/3 + 1), 即bad(1)..........
bad(1)一直調用bad(1), 一直調用到程序崩潰。很明顯bad()函數定義雖然給出了 n = 0 作為遞歸出口,但是bad()函數根本不會推進到n = 0 的這種情況。因此遞歸調用必須總能夠朝著產生基準情況(遞歸出口)的方向推進。
1.3、遞歸和歸納
考慮一個問題:現在需要將一個正整數 n 打印出來,但是I/O給出的函數接口(printDigit)只能處理單個數字(即n < 10)。
我們隨便假設一個n值:n = 2019,那么單個數字打印的順序就是2, 0, 1, 9。換句話說,9是最后一個打印的,在打印9之前要先打印201,即先打印“201”,再打印“9”;依次類推對于“201”先打印“20”,再打印“1”;對于“20”先打印“2”,再打印“0”;對于2已經是單個數字,可以直接打印了, 不需要再劃分,再遞歸了,也就是說單個數字n < 10即為遞歸的出口。
我們按上述思路細致的分析一下:
對2019分成2部分: 201 = 2019 / 10; 9 = 2019 % 10;
對201分成2部分:20 = 201 / 10; 1 = 201 % 10;
對20分成2部分:2 = 20 / 10; 0 = 20 % 10;
對于 2 滿足 n < 10 的條件,不再遞歸,直接打印。
現在遞歸已經很明顯了,嘗試編寫一下代碼:
//假設printDigit((unsigned int n)如下,
void printDigit(unsigned int n)
{
std::cout << n;
}
void print(unsigned int n)
{
if(n >= 10)
{
print(n / 10);
}
printDigit(n % 10);
}
代碼編寫好了,現在需要證明以下代碼是否正確:對于n >= 0,數的遞歸打印算法總是正確的。
證明:用k表示數字n的包含單個數字的個數。當k = 1,即 n < 10 時,很明顯程序是正確的,因為它不需要遞歸,print()只調用一次printDigit(), 不調用它自己。然后假設print()對于所有k位數都能正常工作,任何k + 1位的數字n都可以通過它的前k位的數字和最低1位數字來表示。前k 位的數字恰好是[ n / 10], 歸納假設它能正常工作,而最低1位數字是[ n % 10],因此該程序能夠正確的打印出任意k + 1位。于是根據歸納法[1],所有數字都能被正確打印出來。
由以上實例總結可以出一條遞歸的設計法則:假設所有遞歸調用都能運行。
1.4、遞歸的合成效益法則
用遞歸實現一個斐波那契數列:
//斐波納契數列:1、1、2、3、5、8、13、21、34
int f(int n)
{
if(n < 1)
{
return 0;
}
else if(n <= 2)
{
return 1;
}
return f(n-1) + f(n-2);
}
假設n = 8, 函數調用f(8), 遞歸調用如下圖:
8-->7;
7-->6;
6-->5;
5-->4;
4-->3;
3-->2;
8-->id0(6);
id0(6)-->id1(5);
id1(5)-->id2(4);
id2(4)-->id3(3);
id3(3)-->id4(2);
7-->id5(5);
id5(5)-->id6(4);
id6(4)-->id7(3);
id7(3)-->id8(2);
6-->id9(4);
id9(4)-->id10(3);
id10(3)-->id11(2);
5-->id12(3);
id12(3)-->id13(2);
4-->id14(2);
3-->id15(1);
id12(3)-->id16(1);
id9(4)-->id17(2);
id10(3)-->id18(1);
id5(5)-->id19(3);
id19(3)-->id20(2);
id19(3)-->id21(1);
id6(4)-->id22(2);
id7(3)-->id23(1);
id0(6)-->id24(4);
id24(4)-->id25(3);
id24(4)-->id28(2);
id25(3)-->id26(2);
id25(3)-->id27(1);
id1(5)-->id29(3);
id29(3)-->id30(2);
id29(3)-->id31(1);
id2(4)-->id32(2);
id3(3)-->id33(1);
由上圖我們不厭其煩的數一下:
n = 1時,f()調用1次;
n = 2時,f()調用1次;
n = 3時,f()調用3次;
n = 4時,f()調用5次;
n = 5時,f()調用9次;
n = 6時,f()調用15次;
n = 7時,f()調用25次;
n = 8時,f()調用41次;
增長的是不是太快了,在f()里加一個計數器測試一下,可以看到在n = 30 的時候,f()的調用次數大約在160萬。
究其原因,是因為我們在求解的過程時,重復了大量的計算過程, 在n = 8 的時候單單是f(3)就重復調用了8次。
由上我們可以得出一個結論:在求解一個問題的同一實例時,在不同的遞歸中做重復性的工作,對資源的消耗可能是災難性的。
最后歸納一下要牢記的遞歸四條基本法則:
- 基準情形。必須總有某些基準情況,它無須遞歸就能求解,即遞歸必須有出口。
- 不斷推進。對于那些需要遞歸求解的情形,每一次遞歸調用都必須要使求解狀態朝基準情形的方向推進。
- 設計法則。假設所有的遞歸調用都能運行。
- 合成效益法則。在求解一個問題的同一實例時,切勿在不同的遞歸中做重復性的工作。
1、證明當n= 1時命題成立。2、假設n=m時命題成立,那么可以推導出在n=m+1時命題也成立。(m代表任意自然數)。3、歸納結論。 ??
總結
- 上一篇: AJAX的写法
- 下一篇: linux 安装简洁的 zsh