1. 程式人生 > 實用技巧 >【資料結構】棧

【資料結構】棧

基礎知識

關於棧你需要知道的基礎知識:

  • LIFO(後進先出的線性表)
  • 通常使用順序表表示棧(方便定位)
  • 常用操作:push(入棧),pop(出棧),empty(判斷是否為空),top(取棧頂)

手寫棧

許多情況下我們可以使用C++STL庫的#include棧,但我們仍要學會手寫棧。

  1. 初始化操作InitStack(SqStack &S)
//構造空棧S
S.base=(ElemType*)malloc(STACK_INIT_SIZE*sizeof(ElemType));
//為順序棧動態分配儲存空間
if(!S.base)exit(OVERFLOW);
S.top=S.base;
S.stacksize=STACK_INIT_SIZE;
return OK;
  1. 銷燬棧操作DestroyStack(SqStak &S)
//若未分配棧空間
if(!S.base)return ERROR;
free(S.base);//回收棧空間
S.base=S.top=NULL;
S.stacksize=0;
return OK;

3.置空棧操作ClearStack(SqStack &S)

//若未分配棧空間
if(!S.base)return ERROR;
S.top=S.base;
return OK;

4.取棧頂元素GetTop(SqStack S,ElemType &e)

//棧空
if(S.top==S.base)return ERROR;
e=*(S.top-1);
return OK;

5.進棧操作Push(SqStack &S,ElemType e)

if ( S.top-S.base >= S.stacksize ) // 若棧滿則追加儲存空間
{
    S.base = (ElemType * ) realloc( S.base,
( S.stacksize + STACK_INCREMENT ) * sizeof( ElemType ) );  
    if  ( ! S. base )exit(OVERFLOW);   //儲存分配失敗
    S.top = S.base + S.stacksize;
    S.stacksize += STACK_INCREMENT; 
}
* S.top ++ = e;//元素e 插入棧頂,後修改棧頂指標
 return OK;

6.出棧操作Pop(SqStack &S,ElemType &e)

if(S.top==S.base)return ERROR;//棧空
e=*--S.top;
return OK;

備註:
棧空:top==base

棧滿:top-base==STACKSIZE

出棧序列

判斷出棧序列

已知入棧序列,判斷出棧序列是否正確。無論是手寫判斷,還是程式碼實現,原理均為:

  • 依據給定的出棧序列,在每次將入棧序列中的元素壓入棧之後嘗試進行出棧(出棧直到棧頂!=即將出棧元素為止),若最終所有元素都能成功出棧(棧大小為0),則出棧序列合法。
  • 程式碼實現中,設定一個指標,每次成功出棧後,指向下一個即將出棧的元素。

卡特蘭數

一個棧進棧序列為1,2,3……n,問可能的出棧序列有多少個?

  • 結論:C(2n,n)/(n+1)=(2n!)/(n!*n!)/(n+1)
  • 推導:
#輸出序列的總數目=由左而右掃描由n個1和n個0組成的2n位二進位制數,其中:任何一個位置上之前出現的1的累計數不小於0的累計數的方案種數。
#所以有一半的方案不可行
輸出序列總數目:C(2n,n)-C(2n,n+1)=C(2n,n)/(n+1)

應用

數制轉換

題目:對於輸入的任意一個非負十進位制整數,列印輸出與其等值的八進位制數。

思路:計算過程是由低位到高位順序產生八進位制數的各個數位,而列印輸出恰好與計算過程相反。

stack <int>S;
int N;
void conversion()
{
  while(!S.empty())S.pop();
  cin >> N;
  while(N)
  {
    S.push(N%8);
    N=N/8;
  }
  while(!S.empty())
  {
    cout << S.top();
    S.pop();
  }
}

括號匹配

題目:給定一串括號序列,判斷其是否正確匹配。

思路:設定一個棧,每讀入一個括號:

  • 若是右括號:要麼與棧頂對應的左括號抵消,要麼不匹配。
  • 若是左括號:入棧。
  • 若棧在開始、結束時都為空,則合法匹配。

表示式求值

波蘭表示式

*後續將專門開一個帖子補充。

算符優先演算法

遇運算元——儲存;

遇運算子號aj——與前面的剛掃描過的運算子ai比較:

  • 若ai<aj 則儲存aj。(因此已儲存的運算子的優先關係為a1<a2<a3<a4…)
  • 若ai>aj 則說明ai是已掃描的運算子中優先順序最高者,可進行運算。
  • 若ai=aj 則說明括號內的式子已計算完,需要消去括號。

棧與遞迴

漢諾塔問題

  • 遞迴實現:
// 遞迴實現漢諾塔
// 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
    }
}
  • 非遞迴實現(棧)
// 儲存函式狀態
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,所以我們需要將原來函式的呼叫方式反過來進行
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));   
        }
    }
}

參考連結:

迷宮問題

思路:從每一個位置出發,下一步都有四種選擇(上右下左),先選擇一個方向,如果該方向能夠走下去,那麼就往這個方向走,當前位置切換為下一個位置。如果不能走,那麼換個方向走,如果所有方向都走不了,那麼退出當前位置,到上一步的位置去,當前位置切換為上一步的位置。一致這樣執行下去,如果當前位置是終點,那麼結束。如果走過了所有的路徑都沒能到達終點,那麼無解。

8皇后問題

問題:八皇后問題,是一個古老而著名的問題,是回溯演算法的典型案例。該問題是國際西洋棋棋手馬克斯·貝瑟爾於1848年提出:在8×8格的國際象棋上擺放八個皇后,使其不能互相攻擊,即任意兩個皇后都不能處於同一行、同一列或同一斜線上,問有多少種擺法。 高斯認為有76種方案。1854年在柏林的象棋雜誌上不同的作者發表了40種不同的解,後來有人用圖論的方法解出92種結果。計算機發明後,有多種計算機語言可以解決此問題。

思路

  1. 從棋盤的第一行開始,從第一個位置開始,依次判斷當前位置是否能夠放置皇后,判斷的依據為:同該行之前的所有行中皇后的所在位置進行比較,如果在同一列,或者在同一條斜線上(斜線有兩條,為正方形的兩個對角線),都不符合要求,繼續檢驗後序的位置。
  2. 如果該行所有位置都不符合要求,則回溯(跳出這一層遞迴,即出棧)到前一行,改變皇后的位置,繼續試探。
  3. 如果試探到最後一行,所有皇后擺放完畢,則直接打印出 8*8 的棋盤。最後一定要記得將棋盤恢復原樣,避免影響下一次擺放。