導言
隨着生活水平的不斷提高,越來越多的轎車走進千家萬戶,不過這也帶來了一個嚴重的問題——停車位的尋找變得困難,因此在生活中我們經常會遇到把車停在不應該停的位置,導致半夜接到電話要求挪車或者收了罰單。現在我們來想象一個情景,我要在一個只有一個出口的窄巷子停車,那么停在內部的車想要開出來,就必須等在最外面的車開走,新的車停進來,只能停在窄巷子的最外面,最里面的車想要開出來就必須讓其他所有的車都開走。
這真是一種我們很不願意見到的情景,好在現實中司機一般不會做這種事情。如果我們把這個窄巷子抽象成一個線性表,車當做表中的元素,我們會發現這個線性表只能對表尾操作,放入新的元素就必須從表尾放入,由於尾部的元素把表的唯一出口堵死了,因此想要把表中的元素拿出,就只能拿出表尾,即最后一個元素。那么這種特殊的順序表就是一種新的數據結構——棧,它的特點是先進后出,后進先出。棧在計算機相關領域中使用廣泛,舉個大家熟悉的例子,例如瀏覽器的后退功能,同個這個按鍵,我們可以查看單個網頁的頁面之前查看過的連接,而且這個按鍵的操作也是單向的,后查看的鏈接會被先查看。
什么是棧?
棧(stack)又名堆棧,它是一種運算受限的線性表,受限於該結構僅能在表尾進行元素的插入和刪除操作。首先棧本質上還是一個線性表,只是有一些操作上較為特殊,棧中的元素具有仍然具有線性關系。在允許進行插入和刪除的一段被稱之為棧頂,表的另一端被稱為棧底,若在棧中沒有任何元素,棧就被稱為空棧,棧結構的插入操作被稱為壓棧,刪除操作被稱為退棧或出棧。棧最鮮明的特點就是先進后出,后進先出,出棧的元素一定是位於棧頂的元素,在棧頂的元素出棧之后,下一個元素就成為新的棧頂,當棧底的元素執行出棧操作之后棧就成為了空棧。
棧的抽象數據類型
ADT Stack
{
Data:
D = {ai | 1 ≤ i ≤ n, n ≥ 0, ai 為 ElemType 類型} //同線性表
Relation:
R = { <ai,ai+1> | ai,ai+1 ∈ D, i = 1, i ∈ (0,n)} //同線性表
Operation:
InitStack(&s); //初始化棧,開辟一個空間給棧 s
StackEmpty(*s); //判斷棧是否為空棧,若為空棧返回 true,否則返回 false
Push(&s,e); //進棧操作,將元素 e 加入棧結構中並使其作為棧頂
Pop(&s,&e); //出棧操作,將位於棧頂的元素刪除,並賦值給變量 e
GetTop(s,&e); //取棧頂操作,若棧不為空棧,返回棧頂元素並賦值給變量 e
ClearStack(&s); //清空棧,將棧中的所有元素清空,即將棧變為空棧
DestroyStack(&s); //銷毀棧,將釋放棧的空間
}
順序棧及其基本操作
順序棧
棧是一種特殊的線性表,也自然可以使用順序存儲結構來實現。在 C\C++ 中,我們對於順序存儲往往使用數組來描述,因此我們需要為一個數組選擇棧底和棧頂,為了方便描述空棧判定和棧滿判定,我們使用下標為 0 的位置作為棧底,當棧頂的下標為數組元素上限時即為棧滿,為了時刻定位棧頂的位置,需要定義一個棧頂指針作為游標來輔助。
順序棧的結構體定義
#define MAXSIZE 100
typedef struct
{
ElemType data[MAXSIZE];
int top; //棧頂指針
}SqStack;
初始化棧
為一個新建立的棧 s 分配足夠的空間,由於空棧沒有任何元素,因此棧頂指針將初始化為 -1。
void InitStack(SqStack s)
{
s = new SqStack;
s->top = -1; //棧頂指針將初始化為 -1,表示沒有任何元素
}
空棧判斷
某個結構為空一直是一個顯然而敏感的狀態,如果不妥善處理就會出現嚴重的異常,就例如對空棧執行出棧操作,就會出現非法讀取的情況。因此雖然空棧判斷的代碼簡單,但是值得我們重視。函數在棧為空棧時返回 true,反之返回 false。
bool StackEmpty(SqStack *s)
{
if(s->top == -1)
{
return true;
}
return false;
}
進棧操作
由於棧是一種操作受限的線性表,因此進棧操作是其核心操作之一。進棧的關鍵在於只能在表尾進行插入,並且當棧的空間為滿的時候,不能入棧。函數將在棧不為滿棧的情況下,在棧頂指針 top 處插入元素 e 並使其自增 1,插入成功返回 true,否則返回 false。時間復雜度 O(1)。
bool Push(SqStack &s,ElemType e)
{
if(s->top == MAXSIZE - 1) //判斷是否棧滿
{
return false;
}
s->data[s->top++] = e; //入棧
return true;
}
出棧操作
同進棧,出棧也是很重要的操作,出棧的關鍵在於只能在表尾進行插入,並且當棧的空間為空的時候,不能出棧。函數將在棧不為空棧的情況下,將位於棧頂指針 top 處的元素出棧並賦值給變量 e ,top 需要並使其自減 1,退棧成功返回 true,否則返回 false。時間復雜度 O(1)。
bool Pop(SqStack &s,ElemType e)
{
if(StackEmpty(s)) //判斷是否為空棧
{
return false;
}
e = s->data[s->top--]; //退棧
return true;
}
取棧頂操作
取棧頂操作與出棧操作不同的是,取棧頂操作只需把棧頂元素賦值給變量 e,無需對棧進行修改。時間復雜度 O(1)。
bool GetTop(SqStack &s,ElemType e)
{
if(StackEmpty(s)) //判斷是否為空棧
{
return false;
}
e = s->data[s->top]; //取棧頂
return true;
}
鏈棧及其基本操作
鏈棧
當棧使用鏈式存儲結構來存儲時,可以建立單鏈表來描述,顯然以鏈表的表頭結點作為棧頂是最方便的。使用連式存儲結構的優點在於,棧的空間在一般情況下不需要考慮上限。對於鏈棧來說,我們可以不設置頭結點。
鏈棧的結構體定義
typedef struct StackNode
{
ElemType data;
struct StackNode *next;
}Node,*Stack;
初始化棧
初始化的操作是為了構造一個空棧,在不設置頭結點的情況下,我們把棧頂指針搞成 NULL 即可。
bool InitStack(Stack &s)
{
s = NULL;
return true;
}
空棧判斷
某個結構為空一直是一個顯然而敏感的狀態,如果不妥善處理就會出現嚴重的異常,就例如對空棧執行出棧操作,就會出現非法讀取的情況。因此雖然空棧判斷的代碼簡單,對於鏈棧值得我們重視。函數在棧為空棧時返回 true,反之返回 false。
bool StackEmpty(Stack *s)
{
if(s == NULL)
{
return true;
}
return false;
}
進棧操作
對於鏈棧的進棧操作,我們不需要判斷是否出現棧滿的情況,只需要用頭插法引入新結點即可,插入成功返回 true,否則返回 false。時間復雜度 O(1)。
bool Push(Stack &s,ElemType e)
{
Stack ptr = new Node; //為新結點申請空間
ptr->next = s; //修改新結點的后繼為 s 結點,入棧
ptr->data = e;
s = ptr; //修改棧頂為 ptr
return true;
}
出棧操作
同順序棧,當棧的空間為空的時候,不能出棧,函數將在棧不為空棧的情況下,需要把棧頂結點的空間釋放掉,退棧成功返回 true,否則返回 false。時間復雜度 O(1)。
bool Pop(Stack &s,ElemType e)
{
Stack ptr;
if(StackEmpty(s)) //判斷是否為空棧
{
return false;
}
e = s->data; //將棧頂元素賦值給 e
ptr = S; //拷貝棧頂元素
S = S->next; //退棧
delete ptr; //釋放原棧頂元素結點的空間
return true;
}
取棧頂操作
當棧非空時,把棧頂元素賦值給變量 e,時間復雜度 O(1)。
bool GetTop(SqStack &s,ElemType e)
{
if(StackEmpty(s)) //判斷是否為空棧
{
return false;
}
e = s->data; //取棧頂
return true;
}
雙端棧
實現目標
復雜的操作由基本操作組合而成
我們這么去理解,假設我們已經定義了兩個棧,開辟了一定的空間,那么會不會出現一個棧滿了,而另一個棧還有很多空間呢?那么我們在這個時候就很希望能夠讓第一個棧使用第二個棧的空間,從理論上講,這樣是完全可行的,因為我們只需要讓這兩個棧能夠分別找到自己的棧頂和棧底即可。例如在一個數組中,我們可以讓數組的始端和末端分別為兩個棧的棧底,再通過操作游標來實現對棧頂的描述。對於棧滿的判斷呢?只要兩個棧的棧頂不見面,棧就不為滿棧。
代碼實現
建立雙端棧
Stack CreateStack(int MaxSize) //建立雙端棧
{
Stack sak = (Stack)malloc(sizeof(struct SNode));
sak->MaxSize = MaxSize;
sak->Data = (ElementType*)malloc(MaxSize * sizeof(ElementType));
sak->Top1 = -1;
sak->Top2 = MaxSize;
return sak;
}
入棧操作
bool Push(Stack S, ElementType X, int Tag) //入棧
{
if (S->Top2 - 1 == S->Top1)
{
printf("Stack Full\n");
return false;
}
if (Tag == 1)
{
S->Data[++S->Top1] = X;
}
else
{
S->Data[--S->Top2] = X;
}
return true;
}
出棧操作
ElementType Pop(Stack S, int Tag) //出棧
{
if (Tag == 1)
{
if (S->Top1 < 0)
{
printf("Stack %d Empty\n",Tag);
return ERROR;
}
else
{
return S->Data[S->Top1--];
}
}
else
{
if (S->Top2 == S->MaxSize)
{
printf("Stack %d Empty\n",Tag);
return ERROR;
}
else
{
return S->Data[S->Top2++];
}
}
}
棧的應用-符號配對
應用情景
情景分析
由於我們只關注表達式的括號是否是成雙成對的,因此只需要獲取我們所需即可。當我獲取第一個括號時,雖然后面可能會有賊多括號,但是我們只繼續接受下一個括號,若下一個括號仍然為左括號,那么這個括號需要配對的優先級是高於第一個左括號的。繼續讀取,若下一個括號為右括號,就拿來和配對優先級較高的第二個括號比對,若成功配對則消解第二個括號,而第一個括號需要配對的優先級就提升了。經過分析我們發現,使用棧結構來描述這個過程極為合適。
偽代碼
代碼實現
#include <iostream>
#include <stack>
#include <string>
using namespace std;
int main()
{
string equation;
stack<char> brackets; //存儲被配對的左括號
int flag = 0;
cin >> equation;
for (int i = 0; equation[i] != 0; i++)
{
if (equation[i] == '(' || equation[i] == '[' || equation[i] == '{') //第 i 個字符是左括號
{
brackets.push(equation[i]);
}
else if (brackets.empty() && (equation[i] == ')' || equation[i] == ']' || equation[i] == '}'))
{
flag = 1; //第 i 個字符是右括號但棧是空棧
break;
}
else if (equation[i] == ')' && brackets.top() == '(') //棧頂括號與右括號配對
{
brackets.pop();
}
else if (equation[i] == ']' && brackets.top() == '[')
{
brackets.pop();
}
else if (equation[i] == '}' && brackets.top() == '{')
{
brackets.pop();
}
}
if (flag == 1) //輸出配對結果
{
cout << "no";
}
else if (brackets.empty() == true)
{
cout << "yes";
}
else
{
cout << brackets.top() << "\n" << "no";
}
return 0;
}
棧的應用-逆波蘭式的轉換
逆波蘭式
眾所周知,對於一個算式而言,不同的運算符有優先級之分,例如“先乘除,后加減”,如果是我們人工進行計算的話,可以用肉眼觀察出算式的運算順序進行計算。可是對於計算機而言,如果是一個一個讀取算式進行計算的話,可能不能算出我們想要的答案,因為這么做是沒有優先級可言的。想要讓計算機實現考慮優先級的算式計算,我們首先要先找到一種算式的描述方式,這種方式不需要考慮運算符優先級。
逆波蘭式(Reverse Polish notation,RPN,或逆波蘭記法),也叫后綴表達式(將運算符寫在操作數之后的表達式),是波蘭邏輯學家盧卡西維奇提出的,例如“2 + 3 * (7 - 4) + 8 / 4”這樣一個表達式,它對應的后綴表達式是“2 3 7 4 - * + 8 4 / +”,這種表達式的計算方法是遇到運算符就拿前面的兩個數字來計算,用這個數字替換掉計算的兩個數字和運算符,直到得出答案。
應用情景
偽代碼
代碼實現
#include <iostream>
#include <stack>
#include <queue>
#include <string>
#include <map>
using namespace std;
int main()
{
string str;
stack<char> sign; //存儲符號
queue<char> line; //存儲轉換好的逆波蘭式,便於后續實現計算
map<char, int> priority;
priority['('] = 3; //為符號設置優先級
priority[')'] = 3;
priority['*'] = 2;
priority['/'] = 2;
priority['+'] = 1;
priority['-'] = 1;
int flag = 0;
cin >> str;
for (int i = 0; i < str.size(); i++)
{ //讀取到數字
if (((i == 0 || str[i - 1] == '(') && (str[i] == '+' || str[i] == '-')) || (str[i] >= '0' && str[i] <= '9'))
{
line.push('#');
if (str[i] != '+')
{
line.push(str[i]);
}
while ((str[i + 1] >= '0' && str[i + 1] <= '9') || str[i + 1] == '.')
{
line.push(str[++i]);
}
}
else //讀取到運算符
{
if (str[i] == ')') //運算符是右括號
{
while (!sign.empty() && sign.top() != '(') //左括號之后的運算符全部出棧
{
line.push('#');
line.push(sign.top());
sign.pop();
}
sign.pop();
continue;
}
else
{
while (!sign.empty() && sign.top() != '(' && priority[str[i]] <= priority[sign.top()])
{
line.push('#');
line.push(sign.top());
sign.pop();
}
}
sign.push(str[i]);
}
}
while (!sign.empty()) //將棧內剩余的符號出棧
{
line.push('#');
line.push(sign.top());
sign.pop();
}
while (!line.empty())
{
if (flag == 0 && line.front() == '#')
{
flag++;
}
else if(line.front() == '#')
{
cout << ' ';
}
else
{
cout << line.front();
}
line.pop();
}
return 0;
}
迷宮尋路(深度優先)
左轉博客——棧和隊列應用:迷宮問題
八皇后問題(棧實現)
左轉我另一篇博客八皇后問題——回溯法思想運用
參考資料
《大話數據結構》—— 程傑 著,清華大學出版社
《數據結構教程》—— 李春葆 主編,清華大學出版社
《數據結構與算法》—— 王曙燕 主編,人民郵電出版社
《數據結構(C語言版|第二版)》—— 嚴蔚敏 李冬梅 吳偉民 編著,人民郵電出版社