棧是限定僅在尾表進行插入和刪除操作的線性表。
隊列是隻允許在一端進行插入操作、而在另一端進行刪除操作的線性表。
棧
棧的定義
定義:棧是限定僅在尾表進行插入和刪除操作的線性表。允許插入和刪除的一端稱爲棧頂(top),另一端稱爲棧底(bottom),不含任何數據元素的棧稱爲空棧。棧又稱爲後進先出(Last In First Out)的線性表,簡稱LIFO結構。
棧是一種特殊的線性表,只能在線性表的表尾進行插入和刪除操作,表尾指的就是棧頂。
棧的插入操作,叫做進棧,壓棧,入棧。棧的刪除操作,叫做出棧,彈棧。
進棧出棧的變化形式
最先進棧的元素不一定是隻能最後出棧的元素,棧對線性表中元素的進出時間沒有限制。三個元素有五種出棧次序。
棧的抽象數據類型
ADT 棧(stack)
Data
同線性表。元素具有相同的類型,相鄰元素具有前驅和後繼關係
Operation.
InitStack(*S):初始化,建立一個空棧
DestroyStack(*S):棧空,銷燬
ClearStack(*S):將棧清空
StackEmpty(S):如果棧空,返回不同布爾值
GetTop(S,*e):若棧存在且非空,用e返回S的棧頂元素
Push(*S,e):若棧存在,插入新元素e到棧S中併成爲棧頂元素
Pop(*S,*e):刪除棧S中棧頂元素,並用e返回其值
StackLength(S):返回棧S的元素個數
endADT
棧的順序存儲及實現
結構定義:
/*棧的結構定義*/
typedef int SElemType;
typedef struct
{
SElemType data[MAXSIZE];
int top; //用於棧頂指針,空棧是爲-1
}SqStack;
進棧操作:
/*插入元素e爲新的棧頂元素*/
Status Push(SqStack *S,SElemType e)
{
if (S->top == MAXSIZE-1) //棧滿
{
return ERROR;
}
S->top++; //棧頂指針自增1
S->data[S->top]=e; //新插元素賦值給棧頂空間
return OK;
}
出棧操作:
/*插入元素e爲新的棧頂元素*/
Status Push(SqStack *S,SElemType e)
{
if (S->top == MAXSIZE-1) //棧滿
{
return ERROR;
}
S->top++; //棧頂指針自增1
S->data[S->top]=e; //新插元素賦值給棧頂空間
return OK;
}
兩棧共享空間
棧頂指針在數組兩端,向中間靠攏。當棧1空時,也就是top1= -1,當棧2空時,即top2=n。當top1+1=top2時棧滿。
兩站共享空間結構:
/*兩棧共享空間結構*/
typedef struct
{
SElemType data[MAXSIZE];
int top1; //棧1棧頂指針
int top2; //棧2棧頂指針
}SqDoubleStack;
/*插入元素e爲新的元素*/
Status Push(SqDoubleStack *S, SElemType e, int stackNumber)
{
if (S->top1 == S->top2) //棧滿,不能再有新元素入棧
{
return ERROR;
}
if (stackNumber == 1) //棧1有元素進棧
{
S->data[++S->top1]=e; //先top1+1後給數組元素賦值
}
else if (stackNumber == 2) //棧2有元素進棧
{
S->data[--S->top2]=e; //先top2-1後給數組元素賦值
}
return OK;
}
/*若棧不空,則刪除S棧頂元素,用e返回其值,返回OK,否則返回ERROR*/
Status Pop(SqDoubleStack *S,SElemType *e,int stackNumber)
{
if (stackNumber == 1)
{
if (S->top1 == -1)
{
return ERROR; //說明棧1已經是空棧,溢出
}
*e = S->data[S->top1--]; //將棧一的棧頂元素出棧,並使top1-1
}
else if (stackNumber == 2)
{
if (S->top2==MAXSIZE)
{
return ERROR; //說明棧2已經是空棧,溢出
}
*e = S->data[S->top2++]; //將棧二的棧頂元素出棧,並使top2+1
}
return OK;
}
棧的鏈式存儲及其實現
單鏈表有頭指針,棧頂指針也是必須的,所以可以讓它們倆合二爲一,所以好的方法是把棧頂放在單鏈表的頭部。因此也不需要頭結點了。
鏈棧的結構代碼:
typedef struct StackNode
{
SElemType data;
struct StackNode *next;
}StackNode,*LinkStackPtr;
typedef struct LinkStack
{
LinkStackPtr top;
int count;
}LinkStack;
進棧操作:
Status Push(LinkStack *S, SElemType e)
{
LinkStackPtr s=(LinkStackPtr)malloc(sizeof(StackNode));
s->data=e;
s->next=S->top; //把當前的棧頂元素賦值,給新結點的的直接後繼
S->top=s; //將新結點賦值給棧頂指針
S->count++;
return OK;
}
出棧操作:
Status Pop(LinkStack *S,SElemType *e)
{
LinkStackPtr p;
if (StackEmpty(*S))
{
return ERROR;
}
*e = S->top->data;
p=S->top; //將棧頂結點賦值給P
S->top=S->top->next; //使得棧頂指針下移一位,指向後一結點
free(p);
S->count--;
return OK;
}
如果棧的使用過程中元素的變化不可預料,最好使用鏈棧,反而如果它的變化在可控範圍內,建議使用順序棧。時間複雜度都是O(1)。
棧的作用
棧的引入簡化了程序設計的問題,劃分了不同關注層次,使得思考範圍縮小,更加聚焦於我們要解決問題的核心。
遞歸
把一個直接調用自己或通過一系列的調用語句間接調用自己的函數,稱爲遞歸函數。
/*斐波那契的遞歸函數*/
int Fbi(int i)
{
if (i<2)
{
return i == 0?0:1;
}
return Fbi(i-1)+Fbi(i-2); //這裏Fbi就是函數自己,它在調自己
}
int main()
{
int i;
for (int i = 0; i < 40; i++)
{
printf("%d", Fbi(i));
}
return 0;
}
四則運算表達式求值
逆波蘭表示法,不需要括號的後綴表達式法,所有的符號都是在要運算數字的後面出現,利用棧可以對後綴表達式進行處理。
規則:從左到右遍歷表達式的每個數字和符號,遇到數字就進棧,遇到符號,就將處於棧頂的兩個數字出棧,進行運算,運算結果進棧,一直到最終獲得結果。
中綴表達式:平時所用的標準四則運算表達式。
中綴表達式轉後綴表達式:從左到右遍歷中綴表達式的每個數字和符號,遇到數字就輸出,即成爲後綴表達式的一部分;若是符號,則判斷與棧頂符號的優先級,是右括號或優先級不高於棧頂符號則棧頂元素依次出棧並輸出,並將當前符號進棧,一直到最終輸出後綴表達式爲止。
計算機處理四則運算表達式:
- 將中綴表達式轉化爲後綴表達式(棧用來進出運算的符號)。
- 將後綴表達式進行運算得出結果(棧用來進出運算的數字)。
隊列
隊列是隻允許在一端進行插入操作,而在另一端進行刪除操作的線性表。隊列是一種先進先出(First In First Out)的線性表,簡稱FIFO。允許插入的一端稱爲隊尾,允許刪除的一端稱爲隊頭。
隊列的抽象數據類型
ADT 隊列(queue)
Data
同線性表。元素具有相同的類型,相鄰元素具有前驅和後繼關係
Operation.
InitQueue(*Q):初始化操作,建立一個空隊列Q
DestroyQueue(*Q):隊列存在則銷燬它
ClearQueue(*Q):將隊列Q清空
QueueEmpty(Q):若隊列爲空,返回TRUE
GetHead(Q,*e):若隊列Q存在且非空,用e返回隊列Q中的對頭元素
EnQueue(*Q,e):若隊列存在,插入新元素e到隊列Q中併成爲隊尾元素
DeQueue(*Q,*e):刪除隊列Q中對頭元素,並用e返回其值
QueueLength(Q):返回隊列Q中元素個數
endADT
循環隊列
爲了避免當只有一個元素時,對頭和隊尾重合時處理變得麻煩,所以引入兩個指針,front指向隊頭元素,rear指向隊尾元素的下一個位置。把隊列的這種頭尾相接的順序存儲結構稱爲循環隊列。
通用的計算隊列長度公式爲:
(rear - front +QueueSize) %QueueSize
typedef int QElemType; //根據實際清空確定時int還是其他類型
/*循環隊列的順序存儲結構*/
typedef struct
{
QElemType data[MAXSIZE];
int front; //頭指針
int rear; //尾指針,若隊列不爲空,指向隊尾元素的下一個位置
}SqQueue;
/*初始化一個空隊列Q*/
Status InitQueue(SqQueue *Q)
{
Q->front=0;
Q->rear=0;
return OK;
}
/*返回Q的元素個數,也就是隊列的當前長度*/
int QElemType(SqQueue)
{
return (Q.rear-Q.front+MAXSIZE)%MAXSIZE;
}
/*若隊列未滿,則插入元素e爲Q的新的隊尾元素*/
Status EnQueue(SqQueue *Q,QElemType 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(SqQueue *Q,QElemType *e)
{
if (Q->front == Q->rear) //隊列空的判斷
{
return ERROR;
}
*e = Q->data[Q->front]; //對頭元素賦值給e
Q->front = (Q->front+1)%MAXSIZE; //front指針向後移一位置
//若到最後則轉到數組的頭部
return OK;
}
隊列的鏈式存儲結構
隊列的鏈式存儲結構,其實就是線性表的單鏈表,只不過它只能尾進頭出,簡稱爲鏈隊列。隊頭指針(front)指向鏈隊列的頭結點,隊尾指針(rear)指向終端結點。空對列時front和rear都指向頭結點。
鏈隊列的結構:
typedef int QElemType;
typedef struct QNode //結點結構
{
QElemType data;
struct QNode *next
}QNode,*QueuePtr;
typedef struct //隊列的鏈表結構
{
QueuePtr front,rear; //對頭、隊尾指針
}LinkQueue;
入隊操作:
/*插入元素e爲Q的新的隊尾元素*/
Status EnQueue(LinkQueue *Q,QElemType e)
{
QueuePtr s=(QueuePtr)malloc(sizeof(QNode));
if (!s) //存儲分配失敗
{
exit(OVERFLOW);
}
s->data=e;
s->next=NULL;
Q->rear->next=s; //把擁有元素e的新結點s賦值給原隊尾元素的後繼
Q->rear=s; //把當前的s設置爲隊尾結點,rear指向s
return OK;
}
出隊操作:
/*若隊列不空,刪除Q的對頭元素,用e返回其值,並返回OK,否則返回ERROR*/
Status DeQueue(LinkQueue *Q,QElemType *e)
{
QueuePtr p;
if (Q->front == Q->rear)
{
return ERROR;
}
p=Q->front->next; //將欲刪除的對頭結點暫存給p
*e=p->data; //將欲刪除的對頭結點的值賦值給e
Q->front->next=p->next; //將原對頭結點的後繼p->next賦值給頭結點的後繼
if (Q->rear==p) //若對頭是隊尾,則刪除後將rear指向頭結點。
{
Q->rear = Q->front;
}
free(p);
return OK;
}
總結
主要講解了棧和隊列,它們均可以用線性表的順序存儲結構來實現,但都存在着順序存儲的一些弊端,因此它們各自使用技巧來解決這個問題。
對於棧來說,如果是兩個相同數據類型的棧,則可以用數組的兩端作棧底的方法來讓兩個棧共享數據,這就可以最大化地利用數組的空間。
對於隊列來說,爲了避免數組插入和刪除時需要移動數據,於是就引入循環隊列,使得對頭和隊尾可以在數組中循環變化。使得本來插入和刪除是O(n)的時間複雜度變成了O(1)。