漢諾塔問題的非遞歸實現及其思考
有關問題的遞歸實現和非遞歸實現其實是我們理解計算機,或者說編程語言中關於函數調用的方式最好的方式之一,它讓我們知道了某種編程語言在實現函數調用的方式,也是計算機進程切換的一種思想的體現。
我們先來說說漢諾塔問題:
漢諾塔問題是一個經典的問題。
漢諾塔(Hanoi Tower),又稱河內塔,源於印度一個古老傳說。大梵天創造世界的時候做了三根金剛石柱子,在一根柱子上從下往上按照大小順序摞着64片黃金圓盤。大梵天命令婆羅門把圓盤從下面開始按大小順序重新擺放在另一根柱子上。並且規定,任何時候,在小圓盤上都不能放大圓盤,且在三根柱子之間一次只能移動一個圓盤。問應該如何操作?
遞歸實現
我們先來看看漢諾塔問題的遞歸解決思路:
- 將 a 上的 n-1 個盤子通過以 c 為輔助移動到 b
- 將 a 上剩余的最大的盤子直接移動到 c
- 將 b 上的 n-1 個盤子通過 a 為輔助移動到 c
這其實是我們說過無數次的遞歸思路,代碼實現很簡單:
// 遞歸實現漢諾塔
// n 為漢諾塔圓盤編號,從小到大為 1,2,3,……
void hanoi(int n, char A, char B, char C) {
if(n == 1) {
printf("%c -> %c\n", A, C); // 如果只有一個盤子,從 A 直接移動到 C
}else {
hanoi(n-1, A, C, B); // A 上的盤子,以 C 為輔助,移動到 B
hanoi(1, A, B, C); // 移動 A 上的最大的盤子到 C 上
hanoi(n-1, B, A, C); // 將 B 上的盤子以 A 為輔助,移動到 C
}
}
遞歸時候的思路很清晰明了,我們可以通過 debug 看到函數棧的調用,每個函數幀保存了當前函數所需要的參數,當函數棧頂層的函數都執行完畢時,這個函數被彈出pop,然后根據所保存的信息來執行。
非遞歸實現
而非遞歸實現,其實也就是需要我們手動模擬出函數棧,需要用函數棧保存一些參數。
我們可以定一個結構體 Status
來保存當前狀態的參數,隨后當 pop 到這個函數時就可以直接讀取使用。
不光是漢諾塔的非遞歸實現,包括我們的 BFS 的實現,其實也是使用棧來保存當前的狀態信息,我們都可以使用一個 Status
來保存。
// 保存函數狀態
struct Status {
int n;
char start, mid, end; // 初始塔,輔助中間塔,最終要移動到的塔
Status(int _n, char _A, char _B, char _C): n(_n), start(_A), mid(_B), end(_C) {}
};
而棧的使用又有一個特點,那就是 FILO,也就是先進后出,所以我們需要按照遞歸函數調用的反向來保存狀態,也就是先調用的后 push 到棧中。
從而我們得到了非遞歸的實現。
// 采用非遞歸實現漢諾塔問題
// 由於棧的特殊性質,FILO,所以我們需要將原來函數的調用方式反過來進行
void hanoiStack(int n, char A, char B, char C) {
stack<Status> myS;
myS.push(Status(n, A, B, C));
while (!myS.empty())
{
Status ns = myS.top();
myS.pop();
if(ns.n == 1) {
printf("%c -> %c\n", ns.start, ns.end);
}else {
myS.push(Status(ns.n-1, ns.mid, ns.start, ns.end));
myS.push(Status(1, ns.start, ns.mid, ns.end));
myS.push(Status(ns.n-1, ns.start, ns.end, ns.mid));
}
}
}
思考
我們通過非遞歸解決方式得到了一種遞歸問題的另外一種解決方法,在我們手動去寫遞歸函數的時候,其實是操作系統或者說編程語言幫助我們實現了函數棧,幫助我們保存了每次使用所需要的參數。
而且在很多時候,非遞歸的實現方式可以幫助我們節省系統資源,幫助我們節約寶貴的系統資源。