常見的基本數據結構——棧


棧ADT

棧(stack)是限制插入和刪除只能在一個位置上進行的表,該位置是表的末端,叫做棧頂。棧的基本操作有進棧(push)和出棧(pop),前者相當於插入,后者相當於刪除最后的元素。在最后插入的元素可以通過使用Top例程在執行Pop之前進行考查。對空棧進行的Pop或Top一般被認為是棧ADT的錯誤。另一方面,當運行Push時空間用盡是一種實現錯誤,但不是ADT的錯誤。

棧有時又叫做LIFO(后進先出表)。

 

棧的實現

由於棧是一個表,因此任何實現表的方法都能夠實現棧。兩種流行的方法:一種是使用指針實現,一種是使用數組實現。

棧的鏈表實現

在表的頂端插入實現Push,在表的頂端刪除實現Pop,Top只是返回頂端元素,有時Top和Pop兩個也可以合二為一。

棧ADT鏈表的聲明實現

struct Node;
typedef struct Node *PtrTONode;
typedef PtrToNode Stack;

struct Node{
  ElementType Node;
  PtrToNode Next;
};

測試棧是否為空

int

IsEmpty(Stack S){

  return S->Next == NULL;

}

創建一個空棧也很簡單,我們只要建立一個頭結點,MakeEmpty設置Next指針指向NULL。Push是作為向鏈表前端進行插入而實現的,其中,表的前端作為棧頂。Top的實現是返回表的前端的元素,Pop是通過刪除表的前端元素實現。

創建一個空棧的過程

Stack
CreateStack(void){
  Stack S;
  S = malloc(sizeof(struct Node));
  if(S == NULL){
    printf(” out of space”);
  }
  S->Next == NULL;
  MakeEmpty(S);
  return S;
}


void MakeEmpty(Stack S){   if(S == NULL){     Error();   }else{   while(!IsEmpty(S)){     Pop(s);   } }

Push進棧例程

void
Push(ElememtType X, Stack S){
  PtrToNode TemCell;
  TemCell = malloc(sizeof(struct Node));
  if(TemCell == NULL){
    Error();
  }else{
    TemCell->ElementType = X;
    TemCell->Next = S->Next;
    S->Next = TemCell;
  }
}

Pop操作實現

ElementType
Top(Stack S){
  if(!IsEmpty(S))
    return S->Next->Element;
  Error();
  return 0;
}

對於鏈表的實現,所有的操作基本上都只花費常數的時間,上述的操作出了空棧之外都沒有涉及到棧的大小,更沒有依賴棧進行循環了。這種實現的缺點是對於malloc和free操作是昂貴的開銷。有的缺點可以通過兩個棧進行避免,第二個棧初始化為空棧,當單元彈出時,它只是被放入到第二個棧,此后當需要新空間時,首先檢查第二個空棧。

棧的數組實現

數組實現避免了指針操作並且是更流行的實現,唯一的不足是它先要聲明一個數組的大小。通常棧的實際個數並不會太大,聲明一個合理的空間沒有什么困難。如果不能的話,那就采用鏈表實現。數組實現棧是非常簡單的,每一個棧都有一個TopOfStack,空棧時為-1,當某個元素壓入棧時,將TopOfStack加1,然后至Stack[TopOfStack] = X;其中,Stack就是具體棧的數組。出棧時,我們返回Stack[TopOfStack]的值,然后TopOfStack減1,為了Stack和TopOfStack相對應,它們應該是棧結構的一部分。

上述的操作不僅以常數時間運行,而且是以非常快的時間運行。在現代化的計算機中,棧已經成為操作系統指令的一部分。一個影響棧執行效率的問題是錯誤檢查。

 

棧的聲明

struct StackRecord;
tepedef struct StructRecord * Stack;

struct StackRecord{
    int Capacity;
    int TopOfStack;
    int ElementType *Array;
}
Stack 
CreateStack(int MaxElement){
  Stack S;
  if(MaxElement < MinStackSize)
  Error();
  S = malloc(sizeof(struct StackRecord));
  if(S == NULL)
    Error();
  S->Array = malloc(sizeof(ElementType) * MaxElement);
  if(S->Array == NULL)
    Error();
  S->Capacity = MaxElements;
  MakeEmpty(S);
  return S;
}

檢測棧是否為空

int 
IsEmpty(Stack S){
  return S->TopOfStack == EmptyTOS;
}

創建一個空棧

void 
MakeEmpty(Stack S){
  S->TopOfStack = EmptyTOS;
}

進棧操作

void
Push(ElementType S, Stack S){
  if(IsFull(S))
    Error();
  else
    S->Array[++S->TopOfStack] = X;
}

返回棧頂元素

ElementType
Top(Stack S){
  if(!IsEmpty(S))
  return S->Array[S->TopOfStack];
  Error();
  return 0;
}

從棧頂彈出元素

void
Pop(Stack S){
  if(IsEmpty)
    Error();
  else
    S-TopOfStack—;
}

將Top和Pop進行合並

ElementType
TopAndPop(Stack S){
  if(!IsEmpty(S)){
    return S->Array[S->TopOfStack]; 
  }
  Error();
  return 0;
}

應用

平衡符號

編譯器檢查你的程序的語法錯誤,當時常常由於缺少一個符號造成上百行的錯誤。在這種情況下,就需要一個工具檢驗成對出現,每一個雙符號都要有對應的符號,一個簡單的算法就用到棧,如下描述:

做一個空棧。讀入字符直到文件尾。如果字符是一個開放字符,則將其推入棧中,如果字符是一個封閉符號,則當棧空時報錯。否則,將棧元素彈出,如果彈出的符號不是對應的開放符號,則報錯。在文件尾,如果棧非空則報錯。

上述的算法是線性的,事實上,它只要對輸入進行一趟檢驗。因此,它是在線的,速度非常的快。

 

后綴表達式

在一個由優先級構成的算術表達式中,我們通常要根據運算符的有限級進行計算結果。請下面的例子:

4.99 + 5.99 + 6.99 * 1.06 = 18.69

如果沒有考慮優先級的話,計算的結果將是19.37.我們可以通過下面的方法進行計算,操作順序如下:

4.99 1.06 * 5.99 +6.99 1.06 * +

上面的記發叫做后綴或者逆波蘭記法。計算這個問題最容易的辦法就是使用一個棧:當遇見數時,就把它放入棧中,在遇到運算符時就作用於棧中彈出的兩個數,並將結果推入棧中。

計算一個后綴表達式的時間是線性的O(N),對輸入的元素由一些棧操作組成從未花費常數的時間,並且不必要知道任何的有限順序。

 

中綴到后綴的轉換

棧不僅可以計算后綴表達式,而且還可以將一個標准的表達式(中綴表達式)轉換成后綴表達式。如下中綴表達式:

a + b * c + (d * e + f) * g

轉換成后綴表達式:

a b c * + d e * f + g * +

具體操作是:當讀到一個操作數的時候,立即把它放到輸出中,操作符不立即輸出,保存在某個地方,正確的做法是將遇到的操作符保存在棧中,遇到左括號也放入棧中。

如果遇見一個右括號,那么就將棧元素彈出,將彈出的符號輸出直到遇見相匹配的左括號,但是左括號不進行輸出。

如果我們遇見任何其他的符號,那么我們從棧中彈出棧元素直到發現優先級更低的元素為止。有一個例外,除非是一個)的時候,否則我們絕不從棧中移除(。對於這種操作,+的優先級最低,(優先級最高。當彈出元素結束后,我們在將操作符移入棧中。

當到達末尾時,我們將棧中元素彈出,變成空棧,將符號輸出。

同樣,這種轉換只需要O(N)的,對於運算符時是從左到右的結合的,上面的算法是正確的,不然就需要重新設計。

 

函數調用

當存在函數調用時,需要存儲重要的信息,諸如寄存器的值,和返回的地址,都要以抽象的方式存在一張紙上並被置於一個堆的頂部。

遞歸的不當使用:打印一個鏈表

void
PrintList(List L){
  if(L != NULL){
    PrintElement(L->Element);
    PrintList(L->Next);
  }
}

這個程序是尾遞歸,是使用極端不當的例子,尾部涉及在最后一步的遞歸。

尾遞歸可以通過將遞歸調用變成goto語句並在其前加上對函數每個參數的賦值語句而手工刪除。它模擬了遞歸調用,因為沒有什么需要存儲的值,在遞歸調用之后,實際上沒有必要知道存儲的值。下面是通過goto改造的while循環實現:

void 
PrintList(List L){
  top:
  if(L != NULL){
    PrintElement(L->Element);
    L = L->Next;
    goto top;
  }
}

遞歸總是能夠徹底除去,但是有時是相當冗長復雜的。一般方法是使用一個棧來消除,雖然非遞歸確實比遞歸程序要快,但是速度的優勢代價確實由於去除而使得程序的清晰度不足。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM