在數學與計算機科學中,遞歸是指在函數的定義中使用函數自身的方法。
遞歸算法是一種直接或者間接地調用自身算法的過程。在計算機編寫程序中,遞歸算法對解決一大類問題是十分有效的,它往往使算法的描述簡潔而且易於理解。
遞歸算法解決問題的特點:
(1) 遞歸就是在過程或函數裏調用自身。
(2) 在使用遞歸策略時,必須有一個明確的遞歸結束條件,稱爲遞歸出口。
(3) 遞歸算法解題通常顯得很簡潔,但遞歸算法解題的運行效率較低。所以一般不提倡用遞歸算法設計程序。
(4) 在遞歸調用的過程當中系統爲每一層的返回點、局部量等開闢了棧來存儲。遞歸次數過多容易造成棧溢出等。所以一般不提倡用遞歸算法設計程序。在實際編程中尤其要注意棧溢出問題。
藉助遞歸方法,我們可以把一個相對複雜的問題轉化爲一個與原問題相似的規模較小的問題來求解,遞歸方法只需少量的程序就可描述出解題過程所需要的多次重複計算,大大地減少了程序的代碼量。但在帶來便捷的同時,也會有一些缺點,也即:通常用遞歸方法的運行效率不高。
遞歸算法實例
1.Fibonacci函數
講到遞歸,我們最先接觸到的一個實例便是斐波那契數列。
斐波那契數列指的是這樣一個數列 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …
特別指出:第0項是0,第1項是第一個1。
這個數列從第二項開始,每一項都等於前兩項之和。
斐波那契數列遞歸法實現:
int Fib(int n) { if(n<1) { return -1; } if(n == 1|| n == 2) { return 1; } return Fib(n-1)+Fib(n-2); }
1 2 3 4 5 6 7 8 9 10 11 12 | int Fib(int n) { if(n<1) { return -1; } if(n == 1|| n == 2) { return 1; } return Fib(n-1)+Fib(n-2); } |
2.漢諾塔問題
漢諾塔示意圖
漢諾塔是根據一個傳說形成的數學問題:
有三根杆子A,B,C。A杆上有N個(N>1)穿孔圓盤,盤的尺寸由下到上依次變小。要求按下列規則將所有圓盤移至C杆:
每次只能移動一個圓盤;
大盤不能疊在小盤上面。
提示:可將圓盤臨時置於B杆,也可將從A杆移出的圓盤重新移回A杆,但都必須遵循上述兩條規則。
問:如何移?最少要移動多少次?
最早發明這個問題的人是法國數學家愛德華·盧卡斯。
傳說印度某間寺院有三根柱子,上串64個金盤。寺院裏的僧侶依照一個古老的預言,以上述規則移動這些盤子;預言說當這些盤子移動完畢,世界就會滅 亡。這個傳說叫做梵天寺之塔問題(Tower of Brahma puzzle)。但不知道是盧卡斯自創的這個傳說,還是他受他人啓發。
若傳說屬實,僧侶們需要264 ? 1步才能完成這個任務;若他們每秒可完成一個盤子的移動,就需要5849億年才能完成。整個宇宙現在也不過137億年。
這個傳說有若干變體:寺院換成修道院、僧侶換成修士等等。寺院的地點衆說紛紜,其中一說是位於越南的河內,所以被命名爲“河內塔”。另外亦有“金盤是創世時所造”、“僧侶們每天移動一盤”之類的背景設定。
佛教中確實有“浮屠”(塔)這種建築;有些浮屠亦遵守上述規則而建。“河內塔”一名可能是由中南半島在殖民時期傳入歐洲的。
以下是漢諾塔問題的遞歸求解實現:
/* * Project : hannoi * File : main.cpp * Author : iCoding * * Date & Time : Sat Oct 06 21:01:55 2012 * */ using namespace std; #include <iostream> #include <cstdio> void hannoi (int n, char from, char buffer, char to) { if (n == 1) { cout << "Move disk " << n << " from " << from << " to " << to << endl; } else { hannoi (n-1, from, to, buffer); cout << "Move disk " << n << " from " << from << " to " << to << endl; hannoi (n-1, buffer, from, to); } } int main() { int n; cin >> n; hannoi (n, 'A', 'B', 'C'); return 0; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | /* * Project : hannoi * File : main.cpp * Author : iCoding * * Date & Time : Sat Oct 06 21:01:55 2012 * */ using namespace std; #include <iostream> #include <cstdio>
void hannoi (int n, char from, char buffer, char to) { if (n == 1) { cout << "Move disk " << n << " from " << from << " to " << to << endl;
} else { hannoi (n-1, from, to, buffer); cout << "Move disk " << n << " from " << from << " to " << to << endl; hannoi (n-1, buffer, from, to); } }
int main() { int n; cin >> n; hannoi (n, 'A', 'B', 'C'); return 0; } |
更詳細解析請參考:編程解決漢諾塔問題
3.二叉樹遍歷
在計算機科學中,二叉樹是每個節點最多有兩個子樹的樹結構。通常子樹被稱作“左子樹”(left subtree)和“右子樹”(right subtree)。
一顆簡單的二叉樹
二叉樹的遍歷分爲三種:前(先)序、中序、後序遍歷。
設L、D、R分別表示二叉樹的左子樹、根結點和遍歷右子樹,則先(根)序遍歷二叉樹的順序是DLR,中(根)序遍歷二叉樹的順序是LDR,後(根)序遍歷二叉樹的順序是LRD。還有按層遍歷二叉樹。這些方法的時間複雜度都是O(n),n爲結點個數。
假設我們有一個包含值的value和指向兩個子結點的left和right的樹結點結構。我們可以寫出這樣的過程:
先序遍歷(遞歸實現):
visit(node) print node.value if node.left != null then visit(node.left) if node.right != null then visit(node.right)
1 2 3 4 | visit(node) print node.value if node.left != null then visit(node.left) if node.right != null then visit(node.right) |
中序遍歷(遞歸實現):
visit(node) if node.left != null then visit(node.left) print node.value if node.right != null then visit(node.right)
1 2 3 4 | visit(node) if node.left != null then visit(node.left) print node.value if node.right != null then visit(node.right) |
後序遍歷(遞歸實現):
visit(node) if node.left != null then visit(node.left) if node.right != null then visit(node.right) print node.value
1 2 3 4 | visit(node) if node.left != null then visit(node.left) if node.right != null then visit(node.right) print node.value |
4.字符串全排列
問題:
寫一個函數返回一個串的所有排列。
解析:
對於一個長度爲n的串,它的全排列共有A(n, n)=n!種。這個問題也是一個遞歸的問題, 不過我們可以用不同的思路去理解它。爲了方便講解,假設我們要考察的串是”abc”, 遞歸函數名叫permu。
思路一:
我們可以把串“abc”中的第0個字符a取出來,然後遞歸調用permu計算剩餘的串“bc” 的排列,得到{bc, cb}。然後再將字符a插入這兩個串中的任何一個空位(插空法), 得到最終所有的排列。比如,a插入串bc的所有(3個)空位,得到{abc,bac,bca}。 遞歸的終止條件是什麼呢?當一個串爲空,就無法再取出其中的第0個字符了, 所以此時返回一個空的排列。代碼如下:
typedef vector<string> vs; vs permu(string s){ vs result; if(s == ""){ result.push_back(""); return result; } string c = s.substr(0, 1); vs res = permu(s.substr(1)); for(int i=0; i<res.size(); ++i){ string t = res[i]; for(int j=0; j<=t.length(); ++j){ string u = t; u.insert(j, c); result.push_back(u); } } return result; //調用result的拷貝構造函數,返回它的一份copy,然後這個局部變量銷燬(與基本類型一樣) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | typedef vector<string> vs;
vs permu(string s){ vs result; if(s == ""){ result.push_back(""); return result; } string c = s.substr(0, 1); vs res = permu(s.substr(1)); for(int i=0; i<res.size(); ++i){ string t = res[i]; for(int j=0; j<=t.length(); ++j){ string u = t; u.insert(j, c); result.push_back(u); } } return result; //調用result的拷貝構造函數,返回它的一份copy,然後這個局部變量銷燬(與基本類型一樣) } |
思路二:
我們還可以用另一種思路來遞歸解這個問題。還是針對串“abc”, 我依次取出這個串中的每個字符,然後調用permu去計算剩餘串的排列。 然後只需要把取出的字符加到剩餘串排列的每個字符前即可。對於這個例子, 程序先取出a,然後計算剩餘串的排列得到{bc,cb},然後把a加到它們的前面,得到 {abc,acb};接着取出b,計算剩餘串的排列得到{ac,ca},然後把b加到它們前面, 得到{bac,bca};後面的同理。最後就可以得到“abc”的全序列。代碼如下:
vs permu1(string s){ vs result; if(s == ""){ result.push_back(""); return result; } for(int i=0; i<s.length(); ++i){ string c = s.substr(i, 1); string t = s; vs res = permu1(t.erase(i, 1)); for(int j=0; j<res.size(); ++j){ result.push_back(c + res[j]); } } return result; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | vs permu1(string s){ vs result; if(s == ""){ result.push_back(""); return result; } for(int i=0; i<s.length(); ++i){ string c = s.substr(i, 1); string t = s; vs res = permu1(t.erase(i, 1)); for(int j=0; j<res.size(); ++j){ result.push_back(c + res[j]); } } return result; } |
更詳細講解請參考:寫一個函數返回一個串的所有排列
5.八皇后問題
問題:
經典的八皇后問題,即在一個8*8的棋盤上放8個皇后,使得這8個皇后無法互相***( 任意2個皇后不能處於同一行,同一列或是對角線上),輸出所有可能的擺放情況。
解析:
8皇后是個經典的問題,如果使用暴力法,每個格子都去考慮放皇后與否,一共有264 種可能。所 以暴力法並不是個好辦法。由於皇后們是不能放在同一行的, 所以我們可以去掉“行”這個因素,即我第1次考慮把皇后放在第1行的某個位置, 第2次放的時候就不用去放在第一行了,因爲這樣放皇后間是可以互相***的。 第2次我就考慮把皇后放在第2行的某個位置,第3次我考慮把皇后放在第3行的某個位置, 這樣依次去遞歸。每計算1行,遞歸一次,每次遞歸裏面考慮8列, 即對每一行皇后有8個可能的位置可以放。找到一個與前面行的皇后都不會互相***的位置, 然後再遞歸進入下一行。找到一組可行解即可輸出,然後程序回溯去找下一組可靠解。
我們用一個一維數組來表示相應行對應的列,比如c[i]=j表示, 第i行的皇后放在第j列。如果當前行是r,皇后放在哪一列呢?c[r]列。 一共有8列,所以我們要讓c[r]依次取第0列,第1列,第2列……一直到第7列, 每取一次我們就去考慮,皇后放的位置會不會和前面已經放了的皇后有衝突。 怎樣是有衝突呢?同行,同列,對角線。由於已經不會同行了,所以不用考慮這一點。 同列:c[r]==c[j]; 同對角線有兩種可能,即主對角線方向和副對角線方向。 主對角線方向滿足,行之差等於列之差:r-j==c[r]-c[j]; 副對角線方向滿足, 行之差等於列之差的相反數:r-j==c[j]-c[r]。 只有滿足了當前皇后和前面所有的皇后都不會互相***的時候,才能進入下一級遞歸。
代碼如下:
#include <iostream> using namespace std; int c[20], n=8, cnt=0; void print(){ for(int i=0; i<n; ++i){ for(int j=0; j<n; ++j){ if(j == c[i]) cout<<"1 "; else cout<<"0 "; } cout<<endl; } cout<<endl; } void search(int r){ if(r == n){ print(); ++cnt; return; } for(int i=0; i<n; ++i){ c[r] = i; int ok = 1; for(int j=0; j<r; ++j) if(c[r]==c[j] || r-j==c[r]-c[j] || r-j==c[j]-c[r]){ ok = 0; break; } if(ok) search(r+1); } } int main(){ search(0); cout<<cnt<<endl; return 0; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | #include <iostream> using namespace std;
int c[20], n=8, cnt=0; void print(){ for(int i=0; i<n; ++i){ for(int j=0; j<n; ++j){ if(j == c[i]) cout<<"1 "; else cout<<"0 "; } cout<<endl; } cout<<endl; } void search(int r){ if(r == n){ print(); ++cnt; return; } for(int i=0; i<n; ++i){ c[r] = i; int ok = 1; for(int j=0; j<r; ++j) if(c[r]==c[j] || r-j==c[r]-c[j] || r-j==c[j]-c[r]){ ok = 0; break; } if(ok) search(r+1); } } int main(){ search(0); cout<<cnt<<endl; return 0; } |