@[TOC]
數據結構和算法(五)棧的操作和實現
- 本篇博客的Demo下載:
1. 棧的簡介
棧是一種后進先出的結構,有一個棧底指針,一個棧頂指針,入棧只能從棧頂入棧,出棧也只能從棧頂出棧。它的結構示意圖如下:
我們可以對比一下隊列:隊列是一種先進先出的數據結構,只能從隊尾入隊,從對頭出隊,隊列的結構圖如下圖:
2. 順序棧的實現
2.1 順序棧的基本操作
2.1.1 順序棧結構
//順序棧結構
typedef struct KSqueueStack{
KStackElementType data[MAXSIZE];
int top; //用于棧頂指針
}SqStack;
2.1.2 順序棧建棧
//1. 構建一個空棧S
KStatus initStack(SqStack *S) {
S->top = -1;
return OK;
}
2.1.3 順序棧置空
//2. 將棧置空
KStatus clearStack(SqStack *S) {
S->top = -1;
return OK;
}```
#### 2.1.4 順序棧判空
```swift
//3. 判斷順序棧是否為空
KStatus isEmpty(SqStack S) {
return S.top == -1 ;
}
2.1.5 順序棧獲取長度
//4. 獲取棧長度
int getLength(SqStack S) {
return S.top + 1;
}
2.1.6 順序棧獲取棧頂元素
//5. 獲取棧頂
KStatus getTop(SqStack S, KStackElementType *e) {
//棧空,則返回錯誤
if (S.top == -1) return ERROR;
*e = S.data[S.top];
return OK;
}
2.1.7 順序棧壓棧
入棧前如下圖所示:
入棧后如下圖所示:
入棧代碼實現:
//6. 壓棧
KStatus push(SqStack *S, KStackElementType e) {
//判斷是否 棧滿
if (S->top == MAXSIZE -1) return ERROR;
//1. 棧頂指針+1;
//2. 將新插入的元素賦值給棧頂空間
//S->top ++;
//S->data[S->top] = e;
S->data[++(S->top)] = e;
return OK;
}
2.1.8 順序棧出棧
//7. 出棧
KStatus pop(SqStack *S, KStackElementType *e) {
//判斷是否棧空
if(S->top == -1) return ERROR;
//1. 將要刪除的棧頂元素賦值給e
//2. 棧頂指針--;
//*e = S->data[S->top];
//S->top--;
*e = S->data[S->top--];
return OK;
}
2.1.9 順序棧遍歷
//8. 棧遍歷
KStatus traverse(SqStack S) {
int i = 0;
printf("棧所有元素:");
while (i < S.top) {
printf("%d ",S.data[i++]);
}
printf("\n");
return OK;
}
2.1.10 順序棧單元測試
//9. 測試
void test() {
SqStack S;
int e;
if (initStack(&S) == OK) {
for (int j = 1 ; j < 10; j++) {
push(&S, j);
}
}
printf("順序棧中元素為:\n");
traverse(S);
pop(&S, &e);
printf("彈出棧頂元素為: %d\n",e);
traverse(S);
printf("是否為空棧:%d\n",isEmpty(S));
getTop(S, &e);
printf("棧頂元素:%d \n棧長度:%d\n",e,getLength(S));
clearStack(&S);
printf("是否已經清空棧 %d, 棧長度為:%d\n",isEmpty(S),getLength(S));
}
- 輸出結果
Hello, World!
順序棧中元素為:
棧所有元素:1 2 3 4 5 6 7 8
彈出棧頂元素為: 9
棧所有元素:1 2 3 4 5 6 7
是否為空棧:0
棧頂元素:8
棧長度:8
是否已經清空棧 1, 棧長度為:0
Program ended with exit code: 0
3. 鏈式棧的實現
鏈式棧是有鏈表來實現的一種棧結構,它的結構示意圖如下圖:
3.1 鏈式棧的基本操作
3.1.1 鏈式棧結構
//鏈棧結點
typedef struct KStackNode {
KStackElementType data; //結點數據
struct KStackNode *next; //指向下一個結點的指針
}StackNode, *LinkStackPtr;
//鏈棧結構
typedef struct KLinkStack {
LinkStackPtr top; //棧頂結點
int count; //棧大小
}LinkStack;
3.1.2 鏈式棧建棧
//1. 構造一個空棧S
KStatus initStack(LinkStack *S) {
S->top = NULL;
S->count = 0;
return OK;
}
3.1.3 鏈式棧置空
//2. 鏈棧置空
KStatus clearStack(LinkStack *S) {
LinkStackPtr p,q;
//p指向棧頂結點
p = S->top;
while (p) {
//保存要刪除的結點p
q = p;
//然p指向它的下一個結點
p = p->next;
//刪除 p結點
free(q);
}
return OK;
}
3.1.4 鏈式棧判空
//3. 判斷棧是否為空
KStatus isEmpty(LinkStack S) {
return S.count == 0;
}
3.1.5 鏈式棧獲取長度
//4. 獲取棧長度
int getLength(LinkStack S) {
return S.count;
}
3.1.6 鏈式棧獲取棧頂元素
//5. 獲取棧頂元素
KStatus getTop(LinkStack S, KStackElementType *e) {
//判斷是否棧空
if (S.top == NULL) return ERROR;
*e = S.top->data;
return OK;
}
3.1.7 鏈式棧壓棧
鏈式棧結構,入棧示意圖如下:
//6. 壓棧
KStatus push(LinkStack *S, KStackElementType e) {
//1. 創建一個新結點,
LinkStackPtr newNode = (LinkStackPtr)malloc(sizeof(StackNode));
//2. 賦值給新結點
newNode->data = e;
//3. 插入新結點到棧頂結點后面
//3.1 把當前的棧頂元素的結點指針指向直接后繼結點
newNode->next = S->top;
//3.2 將新結點賦值給棧頂指針
S->top = newNode;
//棧大小+1
S->count++;
return OK;
}
3.1.8 鏈式棧出棧
鏈式棧結構,出棧示意圖如下圖:
//7. 出棧
KStatus pop(LinkStack *S, KStackElementType *e) {
LinkStackPtr p;
if (isEmpty(*S)) return ERROR;
//1. 將棧頂元素賦值給*e
*e = S->top->data;
//2. 將棧頂結點賦值給p
p = S->top;
//3. 使得棧頂指針下移一位, 指向后一結點
S->top = S->top->next;
//4. 釋放p結點
free(p);
//棧大小減1
S->count--;
return OK;
}
3.1.9 鏈式棧遍歷
//8. 遍歷棧
KStatus traverse(LinkStack S) {
LinkStackPtr p = S.top;
printf("遍歷棧元素:");
while (p) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
return OK;
}
3.1.10 鏈式棧單元測試
//9. 單元測試
void test() {
int j;
LinkStack s;
int e;
if(initStack(&s)==OK)
for(j=1;j<=10;j++)
push(&s,j);
printf("棧中元素依次為:");
traverse(s);
pop(&s,&e);
printf("彈出的棧頂元素 e=%d\n",e);
traverse(s);
printf("棧空否:%d(1:空 0:否)\n",isEmpty(s));
getTop(s,&e);
printf("棧頂元素 e=%d 棧的長度為%d\n",e,getLength(s));
clearStack(&s);
printf("清空棧后,棧空否:%d(1:空 0:否)\n",isEmpty(s));
}
- 輸出結果
Hello, World!
棧中元素依次為:遍歷棧元素:10 9 8 7 6 5 4 3 2 1
彈出的棧頂元素 e=10
遍歷棧元素:9 8 7 6 5 4 3 2 1
棧空否:0(1:空 0:否)
棧頂元素 e=9 棧的長度為9
清空棧后,棧空否:0(1:空 0:否)
Program ended with exit code: 0
3. 棧的應用
3.1 遞歸
3.1.1 函數調用及遞歸實現
下?面3種情況下,我們會使?用到遞歸來解決問題
- 定義是遞歸的
- 數據結構是遞歸的
- 問題的解法是遞歸的
3.1.2 深度優先搜索
3.1.3 回溯算法
3.1.4 Hanoi塔問題
問題描述: 假如有3個分別命名為A,B,C的塔座,在塔座A上插有n個直接?大?小各不不相同的,從?小到?大的 編號為1,2,3...n的圓盤. 現在要求將塔座A上的n個圓盤移動到塔座C上. 并仍然按照同樣的順序疊 排. 圓盤移動時必須按照以下的規則:1. 每次只能移動?一個圓盤;2. 圓盤可以插在A,B,C的任?一塔座 上;3. 任何時刻都不不能將?一個較?大的圓盤壓在?小的圓盤之上.
求解過程圖如下:
3.2 表達式求值
在編譯系統中,算術表達式可以分為三類:算術表達式,關系表達式,邏輯表達式。
任何一個算術表達式都是由:操作數,運算符和分界符組成。我們把操作數,運算符和分界符(分界符標志了一個算術表達式的結束)稱為一個算術表達式的單詞。
-
中綴表達式:算術表達式中的運算符總是出現在兩個操作數之間(除單目運算符外)例如:
A+(B-C/D)*E
-
后綴表達式:表達式中的運算符出現在操作數之后。編譯系統對于中綴表達式處理方法是將其變成后綴表達式,例如:
ABCD/-E*+
后綴表達式的特點:
- 后綴表達式的操作數和中綴表達式的操作數先后次序完全相同(上面ABCDE),只是運算符的先后次序改變了(+-/*);
- 后綴表達式中沒有括號,后綴表達式的運算次序就是其執行次序
應用堆棧實現后綴表達式求值的基本過程:
從左到右讀入后綴表達式的各項(運算符或運算數):
- 運算數:入棧
- 運算符:從堆棧中彈出適當數量的運算數,計算并結果入棧
- 最后,堆棧頂上的元素就是表達式的結果值
3.2.1 中綴表達式求值
基本策略:將中綴表達式轉換為后綴表達式,然后求值。
- 如何將中綴表達式轉換為后綴表達式?
例如:2+9/3-5
->2 9 3 / +5 -
過程:
- 運算數相對順序不變
- 運算符號順序發生改變
- 需要存儲“等待中”的運算符號
- 要將當前運算符號與“等待中”的最后一個運算符號比較
3.2.2 中綴表達式如何轉換為后綴表達式
從頭到尾讀取中綴表達式的每個對象,對不同對象按不同的情況處理。
- 運算數:直接輸出
- 左括號:壓入堆棧
- 右括號:將棧頂的運算符彈出并輸出,直到遇到左括號(出棧,不輸出)
- 運算符:
(1) 若優先級大于棧頂運算符時,則把它壓棧
(2) 若優先級小于等于棧頂運算符時,將棧頂運算符彈出并輸出;再比較新的棧頂運算符,直到該運算符大于棧頂運算符優先級為止,然后將該運算符壓棧- 若各對象處理完畢,則把堆棧中存留的運算符一并輸出
3.2.3 后綴表達式的實現過程
編譯系統設置一個存放運算符的堆棧,初始時棧頂置一個分界符“#”。編譯系統從左到右依次掃描中綴表達式,每讀到一個操作數就把它作為后綴表達式的一部分輸出,每讀到一個運算符(分界符也看作運算符)就將其優先級與棧頂運算符優先級運算符進行比較,以決定是就所讀到的運算符進棧,還是將棧頂運算符作為最為后綴算術表達式的一部分輸出。
- 運算符優先級別注意: 若把O1看成棧頂運算符,O2看成當前掃描讀到的運算符。
- 當O1為“+”或“-”,O2為“*”或“/”時,O1的優先級 < O2的優先級(滿足先乘除,后加減)
- 當O1為“+”“-”“*”或“/”,O2為“(”時,O1的優先級 < O2的優先級(滿足先括號內,后括號外的規則)
- 當O1的運算符和O2的運算符同級別時,O1的優先級 > O2的優先級別(同級別先左后右規則)
- 由于后綴表達式無括號,當O1為“(”,O2為“)”時,用標記“=”使算法在此時去掉該對算法;
- 當O1為“#”時,O2為“#”時,用標記“=”使算法在此時結束處理
- 若表中的值為空,則不允許出現這種情況,一旦出現即為中綴算術表達式語法出錯,如O1為“)”,而O2為“(”情況,即為中綴表達式語法錯誤!)。
- 算法步驟:
- 設置一個堆棧,初始時將棧頂元素置為#
- 順序讀入中綴算術表達式,當讀到的單詞為操作數是就將其輸出,并接著讀下一個單詞
- 單讀到的單詞為運算符時,令a為當前棧頂運算符的變量,b為當前掃描讀到運算符的變量,把當前讀到的運算符賦給b,然后比較變量a的優先級和b的優先級。若a的優先級高于b的優先級,則將a退棧并作為后綴表達式的一個單詞輸出,,然后比較新的棧頂元素運算符a的優先級與b的優先級。
若優先級 a<b,則將b的值進棧,然后接著讀下一個單詞
若優先級 a>b,則將a退棧并作為后綴表達式的一個單詞輸出,然后比較新的棧頂元素運算符a的優先級與b的優先級。
若優先級 a=b且a為“(”,b為“)”。則將a退棧,接著讀下一個單詞
若優先級 a=b且a為“#”,b為“#”。算法結束。
- 代碼實現:
int PostExp(char str[]) //借助堆棧計算后綴表達式str的值
{
KStackElementType x,x1,x2;
int I;
KNode *head; //定義頭指針變量head
initStack(&head); //初始化鏈式堆棧head
for(i-0;str[i]!=#;i++) //循環直到輸入為#
{
if(isdigit(str[i])) //當str[i]為操作數時
{
x=(int)(str[i]-48); //轉換成int類型數據存于變量x中
push(head,x); //x入棧
}
else //當str[i]為運算符時
{
pop(head,&x2); //退棧的操作數,存于變量x2中
pop(head,&x1); //退棧的被操作數,存于變量x1中
switch(str[i]) //執行str[i]所表示的運算
{
case '+':
{
x1+=x2; break;
}
case '-':
{
x1-=x2; break;
}
case '*':
{
x1*=x2; break;
}
case '/':
{
if(x2==0.0)
{
printf("除數為0錯誤!\n");
exit(0);
}
else
{
x1/=x2;
break;
}
}
}
push(head,x1); //運算結果入棧
}
}
pop(head,&x); //得到計算結果存于x
return x; //返回計算結果
}
4. 隊列
隊列:具有一定操作約束的線性表
有以下特點:
- 插入和刪除操作:只能在一端插入,而在另一端刪除
- 數據插入:入隊(AddQ)
- 數據刪除:出隊列(DeleteQ)
- 先進先出:FIFO
4.1 隊列基本操作
4.2 循環隊列
- 循環隊列中頭尾指針和元素之間的關系