數據結構與算法(四)—— 棧與隊列

目錄

一、棧的定義

二、棧的抽象數據類型

三、棧的順序存儲結構及實現

1、棧的順序存儲結構

2、進棧操作

3、出棧操作

四、兩棧共享空間

五、棧的鏈式存儲結構及實現

1、棧的鏈式存儲結構

2、棧的鏈式存儲結構——進棧操作

3、棧的鏈式存儲結構——出棧操作

六、棧的應用——遞歸

七、棧的應用——四則運算表達式求值

1、後綴(逆波蘭)表示法定義

2、後綴表達式計算結果

3、中綴表達式轉後綴表達式

八、隊列

1、隊列的定義

2、隊列的抽象數據類型

3、循環隊列

1) 什麼是循環隊列?

2) 循環隊列的代碼實現

4、隊列的鏈式存儲結構及實現


棧與隊列:

棧是限定僅在表尾進行插入和刪除操作的線性表。

隊列是隻允許在一端進行插入操作、而在另一端進行刪除操作的線性表。

一、棧的定義

        棧是限定僅在表尾進行插入和刪除操作的線性表。

        我們把允許插入和刪除的一端稱爲棧頂,另一端稱爲棧底,不含任何數據元素的棧稱爲空棧。棧又稱爲後進先出的線性表,簡稱LIFO結構。

        定義中說是在線性表的表尾進行插入和刪除操作,這裏表尾是指棧頂,而不是棧底。棧底是固定的,最先進棧的只能在棧底。棧的插入操作,叫作壓棧,也稱壓棧、入棧。棧的刪除操作,叫作出棧,也稱作彈棧。

二、棧的抽象數據類型

ADT 棧
Data
    同線性表。元素具有相同的類型,相鄰元素具有前驅和後繼關係。
Operation
    InitStack(*S):初始化操作,建立一個空棧S。
    DestroyStack(*S):若棧存在,則銷燬它。
    ClearStack(*S):將棧清空。
    StackEmpty(S):若棧爲空,則返回true,否則返回false。
    GetTop(S,*e):若棧存在且非空,用e返回S的棧頂元素。
    Push(*S,e):若棧S存在,插入新元素e到棧S中併成爲棧頂元素。
    Pop(*S,*e):刪除棧S中棧頂元素,並用e返回其值。
    StackLength(S):返回棧S的元素個數。
endADT

三、棧的順序存儲結構及實現

1、棧的順序存儲結構

        由於棧是線性表的特例,所以棧的順序存儲其實也是線性表順序存儲的簡化,我們簡稱爲順序棧。線性表是用數組來實現的,下標爲0的一端可以作爲順序棧的棧底,另一端則作爲順序棧的棧頂。我們可以定義一個top變量來指示棧頂元素在數組中的位置,這top就如同遊標卡尺的遊標,它可以來回移動,意味着棧頂的top可以變大變小,但無論如何遊標不能超出尺的長度。因此,top必須小於StackSize,當棧中存在一個元素時,top爲0;如果爲空棧,則top爲-1。

棧的結構定義如下:

#include <iostream>
using namespace std;

#define MAXSIZE 20
#define OK 1
#define ERROR 0
typedef int Status; //函數結果狀態碼,如OK等
typedef int ElemType;
typedef struct{
	ElemType data[MAXSIZE];
	int top; //棧頂指針
}Stack;

2、進棧操作

進入操作代碼如下:

//插入元素e爲新的棧頂元素
Status Push(Stack *s, ElemType e){
	if(s->top == MAXSIZE-1){ //棧滿
		return ERROR;
	}
	s->top++; //棧頂指針加1
	s->data[s->top]=e; //將插入元素賦值給棧頂空間
	return OK;
}

3、出棧操作

出棧操作代碼如下:

//若格非空,則刪除棧頂元素,用e返回刪除的元素
Status Pop(Stack *s, ElemType *e){
	if(s->top == -1){
		return ERROR;
	}
	*e=s->data[s->top];
	s->top--;
	return OK;
}

四、兩棧共享空間

        兩棧共享空間是利用同一個數組來存儲兩個棧,數組有兩個端點,兩個棧有兩個棧底,讓一個棧的棧底爲數組的始端,即下標爲0處;另一個棧的棧底爲數組的末端,即下標爲數組長度n-1處。這樣,兩個棧如果增加元素,就是從兩端點向中間處延伸。當棧1爲空時,則top1爲-1;當棧2爲空時,則top2爲n。棧滿的情況有三種如下:

① 當棧1是空棧,top2爲0時,就是棧2滿;

② 當棧2是空棧,top1爲n-1時,就是棧1滿;

③ 棧1、棧2都不是空棧,且兩個指針相差1時,即top1+1=top2時。

兩棧共享空間的結構實現代碼如下:

//兩棧共享空間的結構實現
typedef struct{
	ElemType data[MAXSIZE];
	int top1; //棧1的棧頂指針
	int top2; //棧2的棧頂指針
}DoubleStack;

 兩棧共享空間插入元素的實現代碼如下:

//兩棧共享空間插入元素的實現代碼
Status Push(DoubleStack *s, ElemType e, int stackNumber){ //statckNumber用於判斷是棧1還是棧2
	if(s->top1 + 1 == s->top2){ //判斷棧是否已滿
		return ERROR;
	}
	if(stackNumber == 1){ //棧1有元素進棧
		s->data[s->top1+1]=e; //賦值
	}else if(stackNumber == 2){ //棧2有元素進棧
		s->data[--s->top2]=e; //賦值
	}
	return OK;
}

 兩棧共享空間刪除元素的實現代碼如下:

//兩棧共享空間刪除元素
Status Pop(DoubleStack *s, ElemType *e, int stackNumber){
	if(stackNumber == 1){
		if(s->top1 == -1){ //棧1空棧
			return ERROR;
		}
		*e = s->data[s->top1--];
	}else if(stackNumber == 2){
		if(s->top2 == MAXSIZE){ //棧2空棧
			return ERROR;
		}
		*e = s->data[s->top2++];
	}
	return OK;
}

        事實上,使用兩棧共享的數據結構,通常是用於對兩個棧的空間需求有相反關係時,也就是一個棧在增長而另一個棧在縮短的情況。就像買賣股票一樣,有人買入,就有人賣出。這樣使用兩棧共享空間存儲方法纔有比較大的意義,否則兩個棧都在不停的增長,那很快就會因棧滿而溢出了。 

五、棧的鏈式存儲結構及實現

1、棧的鏈式存儲結構

        棧的鏈式存儲結構,簡稱鏈棧。由於單鏈表有頭指針,而棧頂指針也是必須的,所以可以讓棧頂指針當作單鏈表的頭指針。通常對於鏈棧來說,是不需要頭結點的。

鏈棧的代碼定義如下:

//定義鏈棧
typedef struct StackNode{
	ElemType data; //節點元素
	struct StackNode *next; //下一個節點指針
}StackNode,*LinkStackPtr;

typedef struct LinkStack{
	LinkStackPtr top; //棧頂指針
	int count; //棧元素數量
}LinkStack;

2、棧的鏈式存儲結構——進棧操作

進棧操作的代碼如下:

//進棧操作,添加新元素e
Status Push(LinkStack *S, ElemType e){
	LinkStackPtr s = (LinkStackPtr)malloc(sizeof(StackNode)); //創建一個LinkStackPtr類型的新結點
	s->data = e; //將元素e賦值給新結點
	s->next = S->top; //把當前的棧頂元素賦值給新結點的直接後繼
	S->top = s; //把當前的棧頂元素指針指向新結點
	S->count++; //棧元素+1
	return OK;
}

3、棧的鏈式存儲結構——出棧操作

出棧操作的代碼如下:

//出棧操作,用於e返回出棧的元素
Status Pop(LinkStack *S, ElemType *e){
	LinkStackPtr p;
	if(S->top == NULL){
		return ERROR;
	}
	*e = S->top->data; //將要刪除的元素賦值給e
	p = S->top; //將棧頂結點賦值給p
	S->top = S->top->next; //將棧頂結點賦值爲棧頂結點的後繼節點,即指針下移一位
	free(p); //釋放要刪除的結點
	S->count--; //棧中元素-1
	return OK;
}

六、棧的應用——遞歸

        我們把一個直接調用自己或通過一系列的調用語句間接地調用自己的函數,稱做遞歸函數。每個遞歸定義至少有一個條件,滿足時遞歸就不再進行,即不再引用自身而是返回值退出。

斐波那契的遞歸函數如下:

//遞歸函數
int Fbi(int n){
	if(n < 2){
		return n = 0 ? 0 : 1;
	}
	return Fbi(n-1) + Fbi(n-2);
}

        迭代和遞歸的區別:迭代使用的是循環結構,遞歸使用的是選擇結構。遞歸能使用程序的結構更清晰、更簡潔、更容易讓人理解,從而減少讀懂代碼的時間。但是大量的遞歸調用會建立函數的副本,會耗費大量的時間和內存。迭代則不需要反覆調用函數和佔用額外的內存。

        遞歸和棧有什麼關係呢?在前行階段,對於每一層遞歸,函數的局部變量、參數值以及返回地址都被壓入棧中。在退回階段,位於棧頂的局部變量、參數值和返回地址被彈出,用於返回調用層次中執行代碼的其餘部分,也就是恢復了調用的狀態。

七、棧的應用——四則運算表達式求值

1、後綴(逆波蘭)表示法定義

        一種不需要括號的後綴表達法,我們把它稱爲逆波蘭表示。比如:9+(3-1)*3+10/2 用後綴表達法表示爲:931-3*+102/+,之所以叫後綴表達式,原因在於所有的符號都是在要運算數字的後面出現。

2、後綴表達式計算結果

 後綴表達式:931-3*+102/+

計算規則:從左到右遍歷表達式中的每個數字和符號,遇到是數字就進棧,遇到是符號就將處於棧頂的兩個數字出棧進行運算,然後將運算結果進棧,最終獲取到結果。

下面是具體的進棧、出棧運算過程:

① 9進棧 3進棧 1進棧; 棧中值9 3 1

② 下面是"-",將1 3依次出棧並進行運算3-1得到2進棧;棧中值9 2

③ 3進棧;棧中值9 2 3

④ 下面是"*",將3 2依次出棧並進行運算2*3得到6進棧;棧中值9 6

⑤ 下面是"+",將6 9依次出棧並進行運算9+6得到15進棧;棧中值15

⑥ 10進棧 2進棧;棧中值15 10 2

⑦ 下面是"/",將2 10依次出棧並進行運算10/2得到5進棧,棧中值15 5

⑧ 下面是"+",將5 15依次出棧並進行運算15+5得到20

3、中綴表達式轉後綴表達式

轉化規則:從左到右遍歷中綴表達式的每個數字和符號,若是數字就輸出,即成爲後綴表達式的一部分;若是符號則判斷其與棧頂符號的優先級,是右括號或優先級低於棧頂符號,則棧頂元素依次出棧並輸出,並將當前符號進棧,一直到最終輸出後綴表達式爲止。

中綴表達式9+(3-1)*3+10/2 轉化爲後綴表達式:931-3*+102/+

下面是具體的轉化過程:

① 9輸出;後綴表達式爲9

② 下面是"+"入棧、"("入棧;棧中值 + (

③ 3輸出;後綴表達式爲93

④ 下面是"-"入棧;棧中值+ ( -

⑤ 1輸出;後綴表達式爲931

⑥ 下面是")",需要進行配對,棧中元素依次出棧並輸出,直到"("出棧爲止;後綴表達式爲931-,棧中值+

⑦ 下面是"*",優先級比棧中"+"高,所以入棧;棧中值+ *

⑧ 3輸出;後綴表達式爲931-3

⑨ 下面是"+",低於棧頂符號的優先級,所以* +依次出棧輸出,然後將當前"+"入棧;後綴表達式爲931-3*+,棧中值+

⑩ 10輸出;後綴表達式爲931-3*+10

⑪ 下面是"/" ,優先級比"+"高,入棧;棧中值+ /

⑫ 2輸出,此時已經到了最後,將棧中元素依次出棧並輸出;後綴表達式爲931-3*+102/+

八、隊列

1、隊列的定義

         隊列是隻允許在一端進行插入操作,而在另一端進行刪除操作的線性表。允許插入的一端被稱爲隊尾,允許刪除的一端被稱爲隊頭。假設隊列是q=(a1,a2,a3,...,an),那麼a1就是隊頭元素,而an就是隊尾元素。這樣我們刪除時就從a1開始,而插入時,就列在最後。

2、隊列的抽象數據類型

ADT 隊列(Queue)
Data
    同線性表。元素具有相同的類型,相鄰元素具有前驅和後繼關係。
Operation
    InitQueue(*Q):初始化操作,建立一個空隊列Q
    DestoryQueue(*Q):若隊列Q存在,則銷燬它
    ClearQueue(*Q):將隊列Q清空
    QueueEmpty(Q):若隊列Q爲空,返回true,否則返回false
    GetHead(Q,*e):若隊列Q存在且非空,用e返回隊列Q的隊頭元素
    EnQueue(*Q,e):若隊列Q存在,插入新元素e到隊列Q中併成爲隊尾元素
    DeQueue(*Q,*e):刪除隊列Q中隊頭元素,並用e返回其值
    QueueLength(Q):返回隊列Q的元素個數
endADT

3、循環隊列

1) 什麼是循環隊列?

        假設一個隊列有n個元素,則順存儲的隊列需要建立一個大於n的數組,並把隊列的所有元素存儲在數組的前n個單元中,數組下標爲0的一端即是隊頭。所謂的入隊列操作,其實就是在隊尾追加一個元素,不需要移動任何元素,因此時間複雜度爲O(1)。出列時由於是在隊頭,即下標爲0的位置,也就意味着隊列中的所有元素都得向前移動,以保證隊列的隊頭,也就是下標爲0的位置不爲空,此時時間複雜度爲O(n)。但如果不去限制隊列的元素必須存儲在數組的前n個單元這一條件,出隊的性能就會大大增加,也就是說,隊頭不需要一定設置在下標爲0的位置。

        爲了避免當只有一個元素時,隊頭和隊尾重合使處理變得麻煩,所以引入兩個指針,front指針指向隊頭元素,rear指針指向隊尾元素的下一個位置,這樣當front等於rear時,此隊列不是還剩一個元素,而是空隊列。

        這時有一個問題,如果數組末尾元素已經添加,再往後加就會產生數組越界的錯誤。而由於隊頭不一定設置在下標爲0的位置,那麼就會出現隊頭前面還有空閒的空間,這種現象被稱爲“假溢出”。

        解決假溢出的辦法就是後面滿了,從前面開始,也就是頭尾相接的循環。我們把隊列的這種頭尾相接的順序存儲結構稱爲循環隊列。在循環隊列中,當數組滿了之後,可以將rear指針指向下標爲0的位置。爲了解決front==rear時無法判斷隊列是空還是滿的問題,需要在隊列滿時,數組中還保留一個空閒單元。

        由於rear可能比front大,也可能比front小,若隊列的最大尺寸爲QueueSize,那麼隊列滿的條件就是(rear+1)%QueueSize==front ;另外,當rear>front時,隊列長度爲rear-front,但當rear<front時,隊列長度分爲兩段,一段是QueueSize-front,另一段是0+rear,加在一起,隊列長度爲rear-front+QueueSize。因此,隊列長度的通用計算公式爲(rear-front+QueueSize)%QueueSize

2) 循環隊列的代碼實現

循環隊列的定義代碼如下:

//循環隊列的順序存儲結構
typedef struct{
	ElemType data[MAXSIZE];
	int front; //隊頭指針
	int rear; //隊尾指針
}Queue;

循環隊列的初始化代碼如下:

//初始化一個空隊列
Status InitQueue(Queue *Q){
	Q->front = 0;
	Q->rear = 0;
	return OK;
}

 循環隊列的長度獲取代碼如下:

//獲取循環隊列的隊列長度
int QueueLength(Queue Q){
	return (Q.rear - Q.front + MAXSIZE) % MAXSIZE;
}

循環隊列的入隊操作代碼如下:

//循環隊列的入隊操作,如果隊列未滿,則插入元素e爲Q的隊尾元素
Status EnQueue(Queue *Q, ElemType e){
	if((Q->rear + 1) % MAXSIZE == Q->front){ //隊滿的判斷
		return ERROR;
	}
	Q->data[Q->rear] = e; //將元素e賦值給隊尾
	Q->rear = (Q->rear + 1) % MAXSIZE; //將rear指針向後移動一位
	return OK;
}

 循環隊列的出隊操作代碼如下:

//循環隊列的出隊操作,如果隊列不爲空,則刪除Q的隊頭元素,用e返回其值
Status DeQueue(Queue *Q, ElemType *e){
	if(Q->front == Q->rear){
		return ERROR;
	}
	*e = Q->data[Q->front];//將隊頭元素賦值給e
	Q->front = (Q->front - 1) % MAXSIZE; //將front指針後移一位
	return OK;
}

4、隊列的鏈式存儲結構及實現

        隊列的鏈式存儲結構,其實就是線性表的單鏈表,只不過它只能尾進頭出而已,我們把它簡稱爲鏈隊列。爲了操作方便,我們將隊頭指針指向鏈隊列的頭結點,而隊尾指向終端結點。

鏈隊列的結構定義代碼如下:

//鏈隊列的結構定義
typedef struct Node{ //隊列的結點
	ElemType data;
	struct Node *next;
}Node,*QueuePtr;

typedef struct{ //隊列的鏈表結構
	QueuePtr front,rear; //隊頭和隊尾指針
}LinkQueue;

鏈隊列的入隊操作代碼如下:

//鏈隊列的入隊操作
Status EnQueue(LinkQueue *Q, ElemType e){
	QueuePtr s = (QueuePtr)malloc(sizeof(Node));
	if(!s){ //存儲分配失敗
		exit(0);
	}
	s->data = e;
	s->next = NULL;
	Q->rear->next = s; //將當前隊尾節點的next指向s節點
	Q->rear = s; //將隊尾指針指向s節點
	return OK;
}

鏈隊列的出隊操作代碼如下:

//鏈隊列的出隊操作,若隊列不空,刪除Q的隊頭元素,用e返回其值
Status DeQueue(LinkQueue *Q, ElemType *e){
	QueuePtr p;
	if(Q->front == Q->rear){ //判斷隊列爲空
		return ERROR;
	}
	p = Q->front->next; //將要刪除的隊頭結點賦給p
	*e = p->data; //將隊頭結點的值賦給e
	Q->front->next = p->next; //改變頭結點的後繼爲原隊頭結點的後繼
	if(Q->rear == p){ //如果rear指針指向的是要刪除的節點
		Q->rear = Q->front;
	}
	free(p); //刪除節點
	return OK;
}

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章