數據結構複習篇:用棧實現遞歸

也許大家會疑問:複習完棧應該到隊列了吧。我開始也是這樣想的,但用棧實現遞歸,是一個難點。說實話,我以前學習的時候,就在這一處卡住了,當時我煩躁了好幾天,但可能由於突然被什麼東西轉移了注意力,所以就這樣跳過去了。不知道用棧實現遞歸,也確實不大影響後面的學習,但我可以肯定,如果你覺得世界上有一些東西難以理解而不願面對,那自信將會由此削弱。當然,遇到困難可以適當地把它放下,但逃避應該是暫時的,必須鼓勵自己――-也許是幾天,也許是幾個月――但絕對要攻克它!
很興奮,經過剛纔的思考:我先是在草稿紙上進行了一些“畫畫”的工作,當我把用棧實現漢諾塔的搬運過程,一步步地的畫在紙上的時候,思維由這些具體的步驟而變得清晰起來(“畫畫”確實是一種有助於思考的方法。很可惜我沒有掃描工具,否則我可以把這些畫插在我這篇文章中,將會非常生動)。然後我想到一個不錯的比喻來幫助自己理解這一個過程。這一個比喻,我非常得意,一會與大家分享。現在,我自認爲把這個問題完全攻克了(請大家原諒我的自戀^_^),所以才迫不及待地要把思考的結果寫下來。
我還是按書上那個漢諾塔的例子來表述這個思想,而且把漢諾塔的遞歸用棧實現,也恰好有一定的難度。但我建議,大家看完我這篇文後,不妨試着一個遞歸用棧去實現一下,很容易就檢驗出你是否真的領會了其中的思想。
-、漢諾塔問題:
有三根柱子分別叫A柱、B柱、C柱。現假設有N個圓盤(都是按從大到小依次放入柱中的)已經放在了A柱上,我們的任務就是把這N個圓盤移動到C柱。但移動的過程,必須遵守大盤永遠在小盤的下面這一個原則。
二、移動漢諾塔的遞歸思想:
1、  先把A柱上面的(N-1)個圓盤移到B柱(這一步使問題的規模減少了1)。
2、  再把A柱上剩下的那個最大的圓盤移到C柱。
3、  最後把B柱上的(N-1)圓盤移到C柱。
當我們寫遞歸函數的時候,我們先假設我們即將寫的這個函數已經能解決n個圓盤的漢諾塔問題了,遞歸就是這樣一種思想:它告訴我們程序員,做夢也是一件有意義的事^_^。那麼我們現在假設這個函數的接口是這樣的:
Void TOH(int n, Pole start, Pole goal, Pole temp )(第一次調用時,我們是這樣用這個函數的:
Void TOH(N, A, C, B);N是圓盤數、A是起始柱、B是暫時柱、C是目標柱。)然後,我就利用上面分析的遞歸步驟(當然,遞歸中的初始情況base case也是不能忘記的),在該函數體裏面,繼續調用該函數,便得到了遞歸函數:
void TOH(int n, Pole start, Pole goal, Pole temp)//把n個圓盤從start柱移到goal柱,temp柱作爲輔助柱
{
    
if (n == 0return;    //base case
    TOH(n-1, start, temp, goal);    //把n-1個圓盤從start柱移到temp柱,goal作爲此次的輔助柱
    move(start,goal);        //從start柱移動一塊圓盤到goal柱
    TOH(n-1, temp, goal, start);    //把temp柱中的n-1個圓盤移到goal柱
    return;
}
三、用棧實現遞歸的思想:
現在,我將用一個自認爲得意的比喻,來表達這個思想。我們不妨設想有這樣一個環境:有一家獨特的公司,這家公司的上司是這樣給他們的下屬分配任務的:當有一個任務來臨的時候,一位上司就會把這個任務寫在一張格式統一的紙上(這張紙象徵着棧中的一個元素,但紙上的內容與棧中元素的內容會有一些差異),這張紙上一般會記錄下面這兩個信息:
這位上司A會把這張任務紙放到公司裏一張專門的辦公桌上(它是棧的象徵)。
好了,現假設,上司A把這張任務紙放在了那張專門的辦公桌上,一個下屬B查看辦公桌時,發現了這個任務。他並沒有立刻就去執行這個任務,因爲他們公司有一個奇怪但令人鼓舞的規定:
1、如果你可以把一個任務分解成更小的幾個子任務,你便可以把這些分解後的子任務,留給別人去做。
2、當你把任務分解後,你必須把這些子任務,分別寫在任務紙上,並按照這些子任務的執行順序,從後到先,依次疊放在那張辦公桌上,即保證最上面的那張紙,就是應該最先執行的任務。
那麼下屬B發現,他可以把上司A寫在那張紙上的任務分解成三個子任務:
然後,B把這三張紙依次從上到下地疊放在辦公桌上,那麼他可以下班了^_^。
之後,下屬C來上班,發現了辦公桌上疊放了三張紙,注意,公司有如下規定:
通常(因爲還有一種特別情況,將下面給出),每個員工只需負責辦公桌上,放在最上面的那張紙上的任務。
C拿起最上面那張紙,就是B寫的執行順序爲1的那張紙,他立刻笑了。他也模仿B,把這個任務分解成:
1、  這裏有N-2個圓盤,把這些圓盤從A柱移到C柱。
2、  這裏有1個圓盤,把這個圓盤從A柱移到B柱。
3、  這裏有N-2個圓盤,把這些圓盤從C柱移到B柱。
然後,他把三個子任務的三張任務紙替換掉B寫的那張紙。那麼他又可以下班了。
就這樣,員工們很輕鬆的工作。直到有一個員工,假設他名叫X,比較不幸,他發現辦公桌上最上面的那張紙上寫着:把一個圓盤從某根柱移到另一根柱。因爲這個任務根本就沒辦再分了,所以可憐的X就只好親自動手去完成這個任務,但事情並沒有結束,因爲該公司規定:
如果你無法再分解這個任務,你就要親自完成這個任務,並且如果辦公桌上還有任務紙,那你必須繼續處理下一張任務紙。
正如前面所說,辦公桌上的紙的處理方式,就是棧的後進先出的方式,而任務紙就是棧的元素。這應該很容易理解。難點在於兩點:
1、  棧元素的內部結構如何定義?我們可以把棧元素看作一個結構體,或者看作一個類對象,而任務的規模應該是類對象的一個整形數據成員,但任務描述,就不太好處理了。事實上,我們可以對任務進行分類,然後只要用一個枚舉類型或是其他數據類型,來區分這個任務屬於哪種分類。
2、  如何把上面所分析的過程,用程序表達出來?好了,如果你耐心的閱讀了上面的文字,那麼理解下面這個程序,應該非常容易了: 
書中的程序稍作改寫,也作更豐富的註釋,讀程序的時候,注意聯繫我上文所作的比喻:
#include <iostream>
#include 
<conio.h>
#include 
"StackInterface.h"//這個頭文件,可以在棧那篇文章中找到,也可以自己用標準庫中的stack改一下面的程序即可
using namespace std;
/*
現在我們來定義一個棧的元素類:TaskPaper任務紙
由下屬B、C對子任務的分解情況,很容易看出,
可以把任務分成兩類:
1、移動n-1個圓盤。這說明,這種任務可以繼續分解。
2、移動1個圓盤。這說明,這種任務無法再分,可以執行移動操作。
那麼,我們可以定義一個枚舉類型,這個枚舉類型作爲棧元素
的一個數據成員,用來指示到底是繼續分解,還是作出移動操作。
*/
enum Flag {toMove, toDecompose};//移動、繼續分解
enum Pole {start, goal, temp};    //柱的三種狀態,既然起始柱、目標柱、臨時柱(也叫輔助柱)

class TaskPaper    //任務紙類,將作爲棧的元素類
{
public:
    Flag flg;    
//任務分類
    int num;        //任務規模
    Pole A, B, C;    //三根柱

    TaskPaper(
int n, Pole a, Pole b, Pole c, Flag f)
    {
        flg 
= f;
        num 
= n;
        A 
= a;        
        B 
= b;
        C 
= c;
    }
};

void TOH(int n, Pole s, Pole t, Pole g)//用棧實現的漢諾塔函數
{
    LStack
<TaskPaper *> stk;
    stk.push(
new TaskPaper(n,s,t,g, toDecompose));    //上司A放第一張任務紙到辦公桌上

    TaskPaper 
* paperPtr;
    
long step=0;
    
while (stk.pop(paperPtr))    //如果辦公桌上有任務紙,就拿最上面的一張來看看
    {
        
if (paperPtr->flg == toMove || paperPtr->num == 1)
        {
            
++step;
            
if (paperPtr->== start && paperPtr->== goal)
            {
                cout
<<""<<step<<"步:從A柱移動一個圓盤到B柱。"<<endl;
            }
            
else if (paperPtr->== start && paperPtr->== goal)
            {
                cout
<<""<<step<<"步:從A柱移動一個圓盤到C柱。"<<endl;
            }
            
else if (paperPtr->== start && paperPtr->== goal)
            {
                cout
<<""<<step<<"步:從B柱移動一個圓盤到A柱。"<<endl;
            }
            
else if (paperPtr->== start && paperPtr->== goal)
            {
                cout
<<""<<step<<"步:從B柱移動一個圓盤到C柱。"<<endl;
            }
            
else if (paperPtr->== start && paperPtr->== goal)
            {
                cout
<<""<<step<<"步:從C柱移動一個圓盤到A柱。"<<endl;
            }
            
else if (paperPtr->== start && paperPtr->== goal)
            {
                cout
<<""<<step<<"步:從C柱移動一個圓盤到B柱。"<<endl;
            }
        }
        
else
        {
            
int num = paperPtr->num;
            Pole a 
= paperPtr->A;
            Pole b 
= paperPtr->B;
            Pole c 
= paperPtr->C;
            
            
if (a==start && c==goal) 
            {
                
//書中僅寫了這一種情況,而後面的五種的情況被作者大意地認爲是相同的,
                
//於是程序出錯了。我估計沒有幾個人發現這個問題,因爲只我這種疑心很重的人,
                
//纔會按照書中的思路寫一遍這種程序^_^
                stk.push(new TaskPaper(num-1, b, a, c, toDecompose));//子任務執行順序爲3
                stk.push(new TaskPaper(1,a,b,c,::toMove));    //子任務中執行順序爲2
                stk.push(new TaskPaper(num-1, a, c, b, toDecompose));//子任務中執行順序爲1
            }
            
else if (a==start && b==goal)
            {
                stk.push(
new TaskPaper(num-1, c, b, a, toDecompose));//爲goal的柱狀態不變,其它兩根柱的狀態互換
                stk.push(new TaskPaper(1,a,b,c,::toMove));    //移動操作中,柱的狀態不變
                stk.push(new TaskPaper(num-1, a, c, b, toDecompose));//爲start的柱狀態不變,其它兩根柱的狀態互換
            }
            
else if (b==start && a==goal)
            {            
                stk.push(
new TaskPaper(num-1, a, c, b, toDecompose));
                stk.push(
new TaskPaper(1,a,b,c,::toMove));
                stk.push(
new TaskPaper(num-1, c, b, a, toDecompose));
            }
            
else if (b==start && c==goal)
            {
                
                stk.push(
new TaskPaper(num-1, b, a, c, toDecompose));
                stk.push(
new TaskPaper(1,a,b,c,::toMove));
                stk.push(
new TaskPaper(num-1, c, b, a, toDecompose));
            }
            
else if (c==start && a==goal)
            {
                
                stk.push(
new TaskPaper(num-1, a, c, b, toDecompose));
                stk.push(
new TaskPaper(1,a,b,c,::toMove));
                stk.push(
new TaskPaper(num-1, b, a, c, toDecompose));
            }
            
else if (c==start && b==goal)
            {
                
                stk.push(
new TaskPaper(num-1, c, b, a, toDecompose));
                stk.push(
new TaskPaper(1,a,b,c,::toMove));
                stk.push(
new TaskPaper(num-1, b, a, c, toDecompose));
            }

        }

        delete paperPtr;
        
    }
}

void main()
{
    
    TOH(
3,start,temp,goal);        
    getch();

}
總結下一下用棧實現遞歸的算法
1、進棧初始化:把一張TaskPaper放到辦公桌面上。
2、出棧,即從辦公桌上取一張TaskPaper,如辦公桌上沒有任務紙(出棧失敗),則到第4步。
3、取出任務紙後,根據任務的信息,分兩種情況進行處理:
      A、如果任務不可再分,則執行這個任務(在上面這個程序中,體現爲把搬運動作打印出來),返回到第2步。
      B、否則,劃分成若干個子任務,並把這些子任務,按照執行任務的相反順序放進棧中,保證棧頂的任務永遠是下一次出棧時最應優先處理的,返回到第2步。
4、其它處理。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章