棧的定義
定義:棧是限定僅在表尾進行插入和刪除操作的線性表
。我們把允許插入和刪除的一端稱爲棧頂,另一端稱爲棧底,不含任何數據元素的棧稱爲空棧。棧又稱後進先出(Last In First Out)的線性表,簡稱LIFO結構。
棧的插入操作,叫作進棧(Push),也稱壓棧、入棧。棧的刪除操作,叫作出棧(Pop),也有的叫作彈棧。示意圖如下:
棧的抽象數據類型
棧本身就是一個線性表,所以關於線性表的操作,對棧基本上也是符合的,只是在某些方面會有少許變化,以下是棧的抽象數據類型:
ADT 棧(stack)
Data
同線性表。元素具有相同的類型,相鄰元素具有前驅和後繼關係。
Operation
InitStack(*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
棧的順序存儲結構及實現
棧的順序存儲結構其實是線性表順序存儲結構的簡化,我們簡稱爲順序棧。在沒有指針的高級程序語言中,我們使用數組來實現線性表,棧也是同樣的道理,不過因爲棧後進先出的特點,我們定義下標爲0的一端作爲棧底,因爲首元素在棧底,變化最小。
棧的結構定義如下:
//SElemType類型根據實際情況而定,這裏假設爲int
typedef int SElemType;
typedef struct{
SElemType data[MAXSIZE];
//用於棧頂指針
int top;
}SqStack;
壓棧和彈棧
-
壓棧(入棧)
壓棧的示意圖如上,下面我們來看下push的代碼算法實現:
//時間複雜度爲O(1) //插入元素e爲新的棧頂元素 Status Push(SqStack *S,SElemType e){ //棧滿 if(S->top == MAXSIZE - 1){ return ERROR; } //棧頂指針+1 S->top++; //將新插入元素賦值給棧頂空間 S-data[S->top] = e; return OK; }
-
彈棧(出棧)
彈棧的示意圖如上,下面我們來看下pop的代碼算法實現:
//時間複雜度爲O(1)
//若棧不空,則刪除S的棧頂元素,用e返回其值,並返回OK;否則返回ERROR
Status Push(SqStack *S,SElemType e){
//空棧
if(S->top == -1){
return ERROR;
}
//將要刪除的棧頂元素賦值給e
*e = S-data[S->top];
//棧頂指針-1
S->top--;
return OK;
}
兩棧共享空間
棧的順序存儲結構有一個很大的缺陷,就是必須事先確定數組存儲空間大小,萬一不夠用了,就需要編程手段來擴展數組的容量,非常麻煩。對於一個棧,我們也只能儘量考慮周全,設計出合適大小的數組來處理,但對於兩個相同類型的棧(前提)
,我們卻可以做到最大限度地利用其實現開闢的存儲空間來進行操作,這就是兩棧共享空間。
做法如下,數組有兩個端點,兩個棧有兩個棧底,讓一個棧的棧底爲數組的始端,即下標爲0處,另一個棧爲數組的末端,即下標爲數組長度n-1處。這樣,兩個棧如果增加元素,就是兩端點向中間延伸。
兩棧共享空間的結構的代碼如下:
//兩棧共享空間結構
typedef struct{
SElemType data[MAXSIZE];
//棧1棧頂指針
int top1;
//棧2棧頂指針
int top2;
}SqDoubleStack;
兩棧共享空間的push方法:
//插入元素e爲新的棧頂元素。除了插入元素值外,還需要有一個判斷是棧1還是棧2的棧號stackNumber
Status push(SqDoubleStack *S,SElemType e,int stackNumber){
//棧已滿,不能再push新元素了
if(S->top1+1 == S->top2){
return ERROR;
}
//棧1有元素進棧
if(stackNumber == 1){
S->data[++S->top1] = e;
}
//棧2有元素進棧
else if(stackNumber == 2){
S->data[--S->top2] =e;
}
return OK;
}
兩棧共享空間的pop方法:
//若棧不空,則刪除S的棧頂元素,用e返回其值,並返回OK;否則返回ERROR
Status pop(SqDoubleStack *S,SElemType *e,int stackNumber){
if(stackNumber == 1){
if(S->top1 == -1){
//說明棧1已經是空棧,溢出
return ERROR;
}
//將棧1的棧頂元素出棧
*e = S->data[S->top1--];
}else if(stackNumber == 2){
if(S->top2 == MAXSIZE){
//說明棧2已經是空棧,溢出
return ERROR;
}
//將棧2的棧頂元素出棧
*e = S->data[S->top2++];
}
return OK;
}
事實上,使用這樣的數據結構,通常都是當兩個棧的空間需求有相反關係時,也就是一個棧增長時另一個棧在縮短的情況。
棧的鏈式存儲結構及實現
棧的鏈式存儲結構,簡稱鏈棧。由於單鏈表有頭指針,而棧頂指針也是必須的,所以比較好的辦法是把棧頂放在單鏈表的頭部(如下圖)。因爲有了棧頂在頭部,單鏈表中比較常用的頭結點也就失去意義,通常對於鏈棧來說,是不需要頭結點的。
鏈棧基本上不存在棧滿的情況,除非內存已經沒有可以使用的空間。但對於空棧來說,鏈表原定義是頭指針指向空,那麼鏈棧的空其實就是top=NULL。
鏈棧的結構代碼如下:
typedef struct StackNode{
SElemType data;
struct StackNode *next;
}StackNode,*LinkStackPtr;
typedef struct LinkStack{
LinkStackPtr top;
int count;
}LinkStack;
進棧和出棧
1.進棧(上圖左側)
進棧即push操作,假設元素值爲e的新結點爲s,top爲棧頂指針,代碼實現如下:
//插入元素e爲新的棧頂元素
Status Push(LinkStack *S,SElemType e){
LinkStackPtr s = (LinkStackPtr)malloc(sizeof(StackNode));
s->data = e;
//把當前的棧頂元素賦值給新結點的直接後繼
s->next = S->top;
//將新結點s賦值給棧頂指針
S->top = s;
S->count++;
return OK;
}
2.出棧(上圖右側)
出棧即pop操作,假設變量p用來存儲要刪除的棧頂結點,將棧頂指針下移一位,最後釋放p,代碼實現如下:
//若棧不空,則刪除S的棧頂元素,用e返回其值,並返回OK;否則返回ERROR
Status Pop(LinkStack *S,SElemType *e){
LinkStackPtr p;
if(StackEmpty(*S)){
return ERROR;
}
*e = S->top->data;
//將棧頂結點賦值給p
p = S->top;
//使得棧頂指針下移一位,指向後一結點
S->top = S->top->next;
free(p);
S->count--;
return OK;
}
如果棧的使用過程中元素變化不可預料,有時很大,有時很小,那麼最好使用鏈棧;反之,如果它的變化在可控範圍之內,最好使用順序棧。
##隊列的定義
定義:隊列是隻允許一端進行插入操作,而在另一端進行刪除操作的線性表
。允許插入的一端稱爲隊尾,允許刪除的一端稱爲隊頭。
隊列是**先進先出(First In First Out)**的線性表
假設隊列是q={a1,a2,…,an},那麼a1就是隊頭元素,而an是隊尾元素。刪除時總是從a1開始,而插入時,列在最後。
##隊列的抽象數據類型
隊列的操作基本和線性表差不多,不同的就是插入數據只能在隊尾,刪除數據只能在隊頭。
ADT 隊列(Queue)
Data
同線性表。元素具有相同的類型,相鄰元素具有前驅和後繼關係。
Operation
InitQueue(*Q): 初始化操作,建立一個空隊列。
DestroyQueue(*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
##隊列的順序存儲結構
假設一個隊列有n個元素,則順序存儲的隊列需建立一個大於n的數組,並把隊列的所有元素存儲在數組的前n個單元,數組下標爲0的一端即是隊頭。現在我們來研究一下關於入隊和出隊的流程:
從上面我們可以看出根據正常的隊列定義,我們入隊的時間複雜度爲O(1),但是出隊的時間複雜度確實O(n),原因是隊頭是下標爲0的位置,每一次在隊頭出隊一個元素,後面的元素就要全部向前移一格。這樣來看性能是很不好的,有什麼方式來優化呢?很簡單,就是我們不再限制把隊列中的所有元素存在數組的前
n個單元,具體如何實現呢,我們來看一下一個新的流程圖:
在這裏我們引入了兩個指針,分別是front指向隊頭元素;rear指向隊尾的下一個位置。在出隊一個元素之後,隊頭元素後移一位,此時出隊的時間複雜度由O(n)優化爲了O(1),但是當前這個模式有一個問題,那就是假溢出
。爲了解決假溢出問題,我們的循環隊列出來了。循環隊列
的定義很簡單:我們把隊列的這種頭尾相接的順序存儲結構稱爲循環隊列。
什麼是假溢出呢?我們以上圖爲例,當前隊列的總容量爲5,我們先移除下標0、1中的元素,然後填充下標2、3、 4中的元素,此時隊頭指針指向下標2,那隊尾呢?好像已經越界了,這就是假溢出,因爲實際上當前數組中下標0、1還是可以填充隊列元素的。
根據循環隊列的定義,我們把上面出現假溢出的案例的流程圖補上:
這樣我們就圓滿的解決了假溢出的bug。但是不要高興太早,新bug來啦,那就是我們該如何判斷隊列什麼時候是空,什麼時候是滿?
爲了解決這個問題,我們把隊列空和隊列滿的條件做了一點修改:
(1)隊列空:front=rear
(2)隊列滿:若數組中海油一個空閒單元,我們就認爲隊列滿(見下圖)。
在隊列滿這裏,有一點需要注意,那就是rear既可能比front大,也可能比front小,所以儘管它們只相差一個位置時就是滿的,但也可能相差整整一圈,假設隊列的最大長度爲QueueSize,那麼隊列滿的判斷條件是:
(rear+1) % QueueSize == front
同時,在計算隊列的實際長度時也存在rear > front和rear < front,通用的計算隊列長度公式爲:
(rear - front + QueueSize) % QueueSize
下面我們開始介紹循環隊列的結構和一些常見操作。
##循環隊列的順序存儲結構
//QElemType類型根據實際情況而定,這裏假設爲int
typedef int QElemType;
//循環隊列的順序存儲結構
typedef struct{
QElemType data[MAXSIZE];
//頭指針
int front;
//尾指針,若隊列不爲空,指向隊尾元素的下一個位置
int rear;
}
###循環隊列的常見操作
1.初始化
//初始化空隊列
Status InitQueue(SqQueue *Q){
Q->front = 0;
Q->rear = 0;
return OK;
}
2.隊列長度
//返回隊列Q的元素個數
int QueueLength(SqQueue Q){
return (Q.rear-Q.font+MAXSIZE)%MAXSIZE;
}
3.入隊
//若隊列Q未滿,則插入元素e爲Q新的隊尾元素
Status EnQueue(SqQueue *Q,QElemType e){
//隊列已滿
if((Q->rear+1)%MAXSIZE == Q->front){
return ERROR;
}
//將元素e賦值給隊尾
Q->data[Q->rear] = e;
//rear指針後移一位,若到最後則轉到數組頭部
Q->rear=(Q->rear+1)%MAXSIZE;
return OK;
}
4.出隊
//若隊列Q不空,則刪除Q中隊頭元素,用e返回其值
Status EnQueue(SqQueue *Q,QElemType *e){
//隊列爲空
if(Q->rear == Q->front){
return ERROR;
}
//將隊頭元素賦值給e
*e = Q->data[Q->front];
//front指針後移一位,若到最後則轉到數組頭部
Q->front = (Q->front+1)%MAXSIZE;
return OK;
}
從上面的介紹我們可以發現,單是順序存儲,若不是循環隊列,算法的時間性能不高,但循環隊列又面臨着數組可能會溢出的問題,所以我們還需要研究一下隊列的鏈式存儲結構。
##隊列的鏈式存儲結構及實現
隊列的鏈式存儲結構,其實就是單鏈表,只不過它只能尾進頭出而已,我們把它稱爲鏈隊列。爲了操作上的方便,我們將隊頭指針front指向鏈隊列的頭結點,將隊尾指針rear指向終端結點:
鏈隊列的結構:
//QElemType類型根據實際情況而定,這裏假設爲int
typedef int QElemType;
//結點結構
typedef struct QNode{
QElemType data;
struct QNode *next;
}QNode,*QueuePtr;
//隊列的鏈表結構
typedef struct{
QueuePtr front,near;
}LinkQueue;
###鏈隊列的入隊、出隊
1.入隊
//插入元素e爲Q的新的隊尾元素
Status EnQueue(LinkQueue *Q,QElemType e){
//s即爲新插入的元素e
QueuePtr s = (QueuePtr)malloc(sizeof(QNode));
if(!s){
//存儲分配失敗
exit(OVERFLOW);
}
s->data = e;
s->next = NULL;
//把擁有元素e新結點s賦值給原隊尾結點的後繼
Q->rear->next = s;
//把當前的s設置爲隊尾結點,rear指向s
Q->rear = s;
return OK;
}
2.出隊
//若隊列不空,刪除Q的隊頭元素,用e返回其值,並返回OK,否則返回ERROR
Status DeQueue(LinkQueue *Q,QElemType *e){
QueuePtr p;
//此時是空隊列
if(Q->front == Q->rear){
return ERROR;
}
//將欲刪除的隊頭結點暫存給p
p = Q->front->next;
//將欲刪除的隊頭結點的值賦值給e
*e = p->data;
//將原隊頭結點後繼p->next賦值給頭結點後繼
Q->front->next = p->next;
if(Q->rear == p){
Q->rear = Q->front;
}
free(p);
return OK;
}
對於循環隊列和鏈隊列的比較,可以從兩方面考慮,時間上,它們的基本操作都是常數時間,即O(1),不過循環隊列是事先申請好空間,使用期間不釋放,而對於鏈隊列,每次申請和釋放結點存在一定的時間開銷;空間上,循環隊列必須有一個固定的長度,所以就有了空間浪費的問題,而鏈隊列不存在這個問題,儘管它需要一個指針域,會產生一些空間上的開銷,但也可以接受。