1. 程式人生 > >資料結構複習篇:用棧實現遞迴

資料結構複習篇:用棧實現遞迴

也許大家會疑問:複習完棧應該到隊列了吧。我開始也是這樣想的,但用棧實現遞迴,是一個難點。說實話,我以前學習的時候,就在這一處卡住了,當時我煩躁了好幾天,但可能由於突然被什麼東西轉移了注意力,所以就這樣跳過去了。不知道用棧實現遞迴,也確實不大影響後面的學習,但我可以肯定,如果你覺得世界上有一些東西難以理解而不願面對,那自信將會由此削弱。當然,遇到困難可以適當地把它放下,但逃避應該是暫時的,必須鼓勵自己――-也許是幾天,也許是幾個月――但絕對要攻克它! 很興奮,經過剛才的思考:我先是在草稿紙上進行了一些“畫畫”的工作,當我把用棧實現漢諾塔的搬運過程,一步步地的畫在紙上的時候,思維由這些具體的步驟而變得清晰起來(“畫畫”確實是一種有助於思考的方法。很可惜我沒有掃描工具,否則我可以把這些畫插在我這篇文章中,將會非常生動)。然後我想到一個不錯的比喻來幫助自己理解這一個過程。這一個比喻,我非常得意,一會與大家分享。現在,我自認為把這個問題完全攻克了(請大家原諒我的自戀^_^),所以才迫不及待地要把思考的結果寫下來。 我還是按書上那個漢諾塔的例子來表述這個思想,而且把漢諾塔的遞迴用棧實現,也恰好有一定的難度。但我建議,大家看完我這篇文後,不妨試著一個遞迴用棧去實現一下,很容易就檢驗出你是否真的領會了其中的思想。 -、漢諾塔問題: 有三根柱子分別叫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改一下面的程式即可usingnamespace 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;
            }
            
elseif (paperPtr->== start && paperPtr->== goal)
            {
                cout
<<""<<step<<"步:從A柱移動一個圓盤到C柱。"<<endl;
            }
            
elseif (paperPtr->== start && paperPtr->== goal)
            {
                cout
<<""<<step<<"步:從B柱移動一個圓盤到A柱。"<<endl;
            }
            
elseif (paperPtr->== start && paperPtr->== goal)
            {
                cout
<<""<<step<<"步:從B柱移動一個圓盤到C柱。"<<endl;
            }
            
elseif (paperPtr->== start && paperPtr->== goal)
            {
                cout
<<""<<step<<"步:從C柱移動一個圓盤到A柱。"<<endl;
            }
            
elseif (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            }
            
elseif (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的柱狀態不變,其它兩根柱的狀態互換            }
            
elseif (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));
            }
            
elseif (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));
            }
            
elseif (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));
            }
            
elseif (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、其它處理。 補充:剛才(現在是10月3號)我試著用我上文的思想,用棧實現Fibonacci(斐波那契)數列的遞迴,真的很有用,思路非常清晰。我把這段程式發到這裡來,相信通過上面的漢諾塔程式和這個斐波那契程式,可以更好的幫助大家理解上文的思想(由於昨晚電腦崩潰,用ghost重灌了系統,發現我前幾天寫的程式全丟失了,下面這個程式中用到的棧,是標準庫中的stack): /*
這是我個人的標頭檔案,用來定義各種函式
檔名:zyk.h
*/
#ifndef ZYK_H
#define ZYK_H
#include 
<iostream>
#include 
<stack>usingnamespace std;

namespace zyk
{
    
//Fibonacci數列的遞迴函式long fibr (int n);

    
//用棧實現fibr遞迴long fibr_stack(int n);
}

long zyk::fibr(int n)
{
    
if (n ==1|| n ==2)
    { 
return1;}
    
    
return fibr(n-1+ fibr(n-2);
}

long zyk::fibr_stack(int n)
{
    
long val =0;
    stack
<int> stk;
    stk.push(n);      
//初始化棧while (!stk.empty())    //如果辦公桌上不是空的,即有任務紙    {
        
int nn = stk.top();    //檢視這張紙        stk.pop();            //拿開這張紙if (nn==1|| nn==2)
        {
            val 
+=1;        //累加。        }
        
else
        {
            stk.push(nn
-1);    //分成兩個子任務            stk.push(nn-2);    //對於這兩個子任務,其先後執行順序是無關緊要的。        }

    }

    
return val;
}


#endif