1.棧
1.1.棧的定義
棧(stack)是限定僅在表尾(棧頂 top)進行插入和刪除操作的后進先出的線性表。
push、pop 操作在棧頂進行。
ADT 棧(stack)
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.2. 棧的順序存儲結構及實現
棧的結構定義
/* SElemType類型根據實際情況而定,這里假設為int */
typedef int SElemType;
typedef struct
{
SElemType data[MAXSIZE];
/* 用于棧頂指針 */
int top;
}SqStack;
現在有一個棧,StackSize是5,則棧普通情況、空棧和棧滿的情況、空棧和棧滿的情況示意圖如上
進棧(push):
/* 插入元素e為新的棧頂元素 */
Status Push(SqStack *S, SElemType e)
{
/* 棧滿 */
if (S->top == MAXSIZE - 1)
{
return ERROR;
}
/* 棧頂指針增加一 */
S->top++;
/* 將新插入元素賦值給棧頂空間 */
S->data[S->top] = e;
return OK;
}
出棧(pop):
/* 若棧不空,則刪除S的棧頂元素,用e返回其值,
并返回OK;否則返回ERROR */
Status Pop(SqStack *S, SElemType *e)
{
if (S->top == -1)
return ERROR;
/* 將要刪除的棧頂元素賦值給e */
*e = S->data[S->top];
/* 棧頂指針減一 */
S->top--;
return OK;
}
1.3.兩棧共享空間
數組有兩個端點,兩個棧有兩個棧底,讓一個棧的棧底為數組的始端,即下標為0處,另一個棧為數組的末端,即下標為數組長度n-1處。這樣,兩個棧如果增加元素,就是兩端點向中間延伸。(只針對兩個具有相同數據類型的棧)
棧1為空時,即top1等于-1時;棧2為空時,即top2等于n時;棧滿時,即top1+1==top2時。
兩棧共享空間的結構的代碼:
/* 兩棧共享空間結構 */
typedef struct
{
SElemType data[MAXSIZE];
int top1; /* 棧1棧頂指針 */
int top2; /* 棧2棧頂指針 */
} SqDoubleStack;
對于兩棧共享空間的push方法,除了要插入元素值參數外,還需要有一個判斷是棧1還是棧2的棧號參數stackNumber。插入元素的代碼如下:
/* 插入元素e為新的棧頂元素 */
Status Push(SqDoubleStack *S, SElemType e,
int stackNumber)
{
/* 棧已滿,不能再push新元素了 */
if (S->top1 + 1 == S->top2)
return ERROR;
/* 棧1有元素進棧 */
if (stackNumber == 1)
/* 若棧1則先top1+1后給數組元素賦值 */
S->data[++S->top1] = e;
/* 棧2有元素進棧 */
else if (stackNumber == 2)
/* 若棧2則先top2-1后給數組元素賦值 */
S->data[--S->top2] = e;
return OK;
}
對于兩棧共享空間的pop方法,參數就只是判斷棧1棧2的參數stackNumber,代碼如下:
/* 若棧不空,則刪除S的棧頂元素,用e返回其值,
并返回OK;否則返回ERROR */
Status Pop(SqDoubleStack *S, SElemType *e, int stackNumber)
{
if (stackNumber == 1)
{
/* 說明棧1已經是空棧,溢出 */
if (S->top1 == -1)
return ERROR;
/* 將棧1的棧頂元素出棧 */
*e = S->data[S->top1--];
}
else if (stackNumber == 2)
{
/* 說明棧2已經是空棧,溢出 */
if (S->top2 == MAXSIZE)
return ERROR;
/* 將棧2的棧頂元素出棧 */
*e = S->data[S->top2++];
}
return OK;
}
1.4.棧的鏈式存儲結構(鏈棧)及實現
鏈棧的結構代碼如下:
typedef struct StackNode
{
SElemType data;
struct StackNode *next;
} StackNode, *LinkStackPtr;
typedef struct LinkStack
{
LinkStackPtr top;
int count;
} LinkStack;
進棧:
/* 插入元素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;
}
出棧:
/* 若棧不空,則刪除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;
/* 釋放結點p */
free(p);
S->count--;
return OK;
}
2.棧的應用--遞歸
2.1.斐波那契數列(Fibonacci)實現
問:如果兔子在出生兩個月后,就有繁殖能力,一對兔子每個月能生出一對小兔子來。假設所有兔都不死,那么一年以后可以繁殖多少對兔子呢?
分析:拿新出生的一對小兔子分析一下:第一個月小兔子沒有繁殖能力,所以還是一對;兩個月后,生下一對小兔子數共有兩對;三個月以后,老兔子又生下一對,因為小兔子還沒有繁殖能力,所以一共是三對……
依次類推可以列出下表:
表中數字1,1,2,3,5,8,13……構成了一個序列。這個數列有個十分明顯的特點,前面相鄰兩項之和,構成了后一項
如下圖:
對應的數學函數:
打印出前40位的斐波那契數列數:
int main()
{
int i;
int a[40];
a[0] = 0;
a[1] = 1;
printf("%d ", a[0]);
printf("%d ", a[1]);
for (i = 2; i < 40; i++)
{
a[i] = a[i - 1] + a[i - 2];
printf("%d ", a[i]);
}
return 0;
}
其實我們的代碼,如果用遞歸來實現,還可以更簡單:
/* 斐波那契的遞歸函數 */
int Fbi(int i)
{
if (i < 2)
return i == 0 ? 0 : 1;
/* 這里Fbi就是函數自己,它在調用自己 */
return Fbi(i - 1) + Fbi(i - 2);
}
int main()
{
int i;
for (i = 0; i < 40; i++)
printf("%d ", Fbi(i));
return 0;
}
相比較迭代的代碼,是不是干凈很多。
那么這段遞歸是怎么執行的呢?我們來模擬代碼種的 Fbi(i) 函數當 i=5 的執行過程,如下圖:
%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B.png)
2.2.遞歸定義
迭代和遞歸的區別是:
迭代使用的是循環結構,遞歸使用的是選擇結構。遞歸能使程序的結構更清晰、更簡潔、更容易讓人理解,從而減少讀懂代碼的時間。但是大量的遞歸調用會建立函數的副本,會耗費大量的時間和內存。迭代則不需要反復調用函數和占用額外的內存。
遞歸與棧有什么關系?
這得從計算機系統的內部說起,前面我們已經看到遞歸是如何執行它的前行(Fbi(i))和退回(return)階段的。遞歸過程退回的順序是它前行順序的逆序。在退回過程中,可能要執行某些動作,包括恢復在前行過程中存儲起來的某些數據。
這種存儲某些數據,并在后面又以存儲的逆序恢復這些數據,以提供之后使用的需求,顯然很符合棧這樣的數據結構,因此,編譯器使用棧實現遞歸就沒什么好驚訝的了。
簡單的說,就是在前行階段,對于每一層遞歸,函數的局部變量、參數值以及返回地址都被壓入棧中。在退回階段,位于棧頂的局部變量、參數值和返回地址被彈出,用于返回調用層次中執行代碼的其余部分,也就是恢復了調用的狀態。
3.棧的應用--四則運算表達式求值
3.1.后綴(逆波蘭)表示法應用
對于“9+(3-1)×3+10÷2”,如果要用后綴表示法應該是什么樣子:“9 3 1-3*+102/+”,這樣的表達式稱為后綴表達式,叫后綴的原因在于所有的符號都是在要運算數字的后面出現。
舉例:
后綴表達式:9 3 1-3*+10 2/+
規則:從左到右遍歷表達式的每個數字和符號,遇到是數字就進棧,遇到是符號,就將處于棧頂兩個數字出棧,進行運算,運算結果進棧,一直到最終獲得結果。
1.初始化一個空棧。此棧用來對要運算的數字進出使用;
2.后綴表達式中前三個都是數字,所以9、3、1進棧;
3.接下來是“-”,所以將棧中的1出棧作為減數,3出棧作為被減數,并運算3-1得到2,再將2進棧;
4.接著是數字3進棧;
5.后面是“*”,也就意味著棧中3和2出棧,2與3相乘,得到6,并將6進棧;
6.下面是“+”,所以棧中6和9出棧,9與6相加,得到15,將15進棧;
7.接著是10與2兩數字進棧;
8.接下來是符號“/”,因此,棧頂的2與10出棧,10與2相除,得到5,將5進棧;
9.最后一個是符號“+”,所以15與5出棧并相加,得到20,將20進棧,如圖4-9-5的左圖所示。10.結果是20出棧,棧變為空
很順利的解決了計算的問題,那么如何讓“9+(3-1)×3+10÷2”轉化為“9 3 1-3+10 2/+”呢?
3.2.中綴表達式轉后綴表達式
“9+(3-1)×3+10÷2”這樣平時所用的標準四則運算表達式,因為所有的運算符號都在兩數字之間,所以叫做中綴表達式。
中綴表達式“9+(3-1)×3+10÷2”轉化為后綴表達式“9 3 1-3*+10 2/+”
規則:從左到右遍歷中綴表達式的每個數字和符號,若是數字就輸出,即成為后綴表達式的一部分;若是符號,則判斷其與棧頂符號的優先級,是右括號或優先級不高于棧頂符號(乘除優先加減)則棧頂元素依次出棧并輸出,并將當前符號進棧,一直到最終輸出后綴表達式為止。
1.初始化一空棧,用來對符號進出棧使用;
2.第一個字符是數字9,輸出9,后面是符號“+”,進棧;
3.第三個字符是“(”,依然是符號,因其只是左括號,還未配對,故進棧;
4.第四個字符是數字3,輸出,總表達式為93,接著是“-”,進棧;
5.接下來是數字1,輸出,總表達式為 9 31,后面是符號“)”,此時,我們需要去匹配此前的“(”,所以棧頂依次出棧,并輸出,直到“(”出棧為止。此時左括號上方只有“-”,因此輸出“-”??偟妮敵霰磉_式為 9 3 1-;
6.緊接著是符號“×”,因為此時的棧頂符號為“+”號,優先級低于“×”,因此不輸出,“”進棧。接著是數字3,輸出,總的表達式為 9 3 1-3;
7.之后是符號“+”,此時當前棧頂元素“”比這個“+”的優先級高,因此棧中元素出棧并輸出(沒有比“+”號更低的優先級,所以全部出棧),總輸出表達式為9 3 1-3+。然后將當前這個符號“+”進棧。也就是說,前6張圖的棧底的“+”是指中綴表達式中開頭的9后面那個“+”,而圖4-9-9左圖中的棧底(也是棧頂)的“+”是指“9+(3-1)×3+”中的最后一個“+”;
8.緊接著數字10,輸出,總表達式變為9 31-3+10。后是符號“÷”,所以“/”進棧;
9.最后一個數字2,輸出,總的表達式為9 31-3+10 2。如圖4-9-10的左圖所示。10.因已經到最后,所以將棧中符號全部出棧并輸出。最終輸出的后綴表達式結果為93 1-3+10 2/+”;
4.隊列
4.1.隊列定義
隊列(queue)是一種先進先出(First In First Out)的線性表,簡稱FIFO。只允許在一端進行插入操作,而在另一端進行刪除操作的線性表。允許插入的一端稱為隊尾,允許刪除的一端稱為隊頭。
ADT 隊列(Queue)
Data
同線性表。元素具有相同的類型,相鄰元素具有前驅和后繼關系。
Operation
InitQueue(*Q): 初始化操作,建立一個空隊列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
4.2.循環對列
4.2.1.循環對列
首先了解下,什么是假溢出:
假設這個隊列的總個數不超過5個,但目前如果接著入隊的話,因數組末尾元素已經占用,再向后加,就會產生數組越界的錯誤,可實際上,我們的隊列在下標為0和1的地方還是空閑的。我們把這種現象叫做“假溢出”。
解決假溢出的辦法就是后面滿了,就再從頭開始,也就是頭尾相接的循環。這種頭尾相接的順序存儲結構就是循環隊列。
此時問題出來了,空隊列時,front等于rear,現在當隊列滿時,也是front等于rear,那么如何判斷此時的隊列究竟是空還是滿呢?
- 辦法一是設置一個標志變量flag,當front==rear,且flag=0時為隊列空,當front==rear,且flag=1時為隊列滿。
-
辦法二是當隊列空時,條件就是front=rear,當隊列滿時,我們修改其條件,保留一個元素空間。也就是說,隊列滿時,數組中還有一個空閑單元。例如圖4-12-8所示,我們就認為此隊列已經滿了,也就是說,我們不允許圖4-12-7的右圖情況出現。
問題又來了,第二種方法,由于rear可能比front大,也可能比front小,所以盡管它們只相差一個位置時就是滿的情況,但也可能是相差整整一圈。
若隊列的最大尺寸為QueueSize,那么隊列滿的條件是(rear+1)%QueueSize==front(取模“%”的目的就是為了整合rear與front大小為一個問題)。
另外,當rear>front時,此時隊列的長度為rear-front。但當rear<front時,隊列長度分為兩段,一段是QueueSize-front,另一段是0+rear,加在一起,隊列長度為rear-front+QueueSize。
因此通用的計算隊列長度公式為:
(rear-front+QueueSize)%QueueSize
有了這些講解,現在實現循環隊列就不難了。
循環隊列的順序存儲結構代碼如下:
/* QElemType類型根據實際情況而定,這里假設為int */
typedef int QElemType;
/* 循環隊列的順序存儲結構 */
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 QueueLength(SqQueue Q)
{
return (Q.rear - Q.front + MAXSIZE) % MAXSIZE;
}
循環隊列的入隊列操作代碼如下:
/* 若隊列未滿,則插入元素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;
}
循環隊列的出隊列操作代碼如下:
/* 若隊列不空,則刪除Q中隊頭元素,用e返回其值 */
Status DeQueue(SqQueue *Q, QElemType *e)
{
/* 隊列空的判斷 */
if (Q->front == Q->rear)
return ERROR;
/* 將隊頭元素賦值給e */
*e = Q->data[Q->front];
/* front指針向后移一位置, */
Q->front = (Q->front + 1) % MAXSIZE;
/* 若到最后則轉到數組頭部 */
return OK;
}
到這里可以看出單是順序存儲,若不是循環隊列,算法的時間性能是不高的,但循環隊列又面臨著數組可能會溢出的問題,所以我們還需要研究一下不需要擔心隊列長度的鏈式存儲結構。
4.2.2.對列的鏈式存儲結構及實現
鏈對列的結構:
/* QElemType類型根據實際情況而定,這里假設為int */
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;
/* 把擁有元素e新結點s賦值給原隊尾結點的后繼, */
Q->rear->next = s;
/* 見上圖中① */
/* 把當前的s設置為隊尾結點,rear指向s,見上圖中② */
Q->rear = s;
return OK;
}
出對列:
/* 若隊列不空,刪除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;
/* 見上圖中② */
/* 若隊頭是隊尾,則刪除后將rear指向頭結點,見上圖中③ */
if (Q->rear == p)
Q->rear = Q->front;
free(p);
return OK;
}
對于循環隊列與鏈隊列的比較,可以從兩方面來考慮,從時間上,其實它們的基本操作都是常數時間,即都為O(1)的,不過循環隊列是事先申請好空間,使用期間不釋放,而對于鏈隊列,每次申請和釋放結點也會存在一些時間開銷,如果入隊出隊頻繁,則兩者還是有細微差異。對于空間上來說,循環隊列必須有一個固定的長度,所以就有了存儲元素個數和空間浪費的問題。而鏈隊列不存在這個問題,盡管它需要一個指針域,會產生一些空間上的開銷,但也可以接受。所以在空間上,鏈隊列更加靈活。
總的來說,在可以確定隊列長度最大值的情況下,可以用循環隊列,如果無法預估隊列的長度時,則用鏈隊列。
5.總結
棧(stack)是限定僅在表尾進行插入和刪除操作的線性表。
隊列(queue)是只允許在一端進行插入操作,而在另一端進行刪除操作的線性表。
它們均可以用線性表的順序存儲結構來實現,但都存在著順序存儲的一些弊端。因此它們各自有各自的技巧來解決這個問題。
對于棧來說,如果是兩個相同數據類型的棧,則可以用數組的兩端作棧底的方法來讓兩個棧共享數據,這就可以最大化地利用數組的空間。
對于隊列來說,為了避免數組插入和刪除時需要移動數據,于是就引入了循環隊列,使得隊頭和隊尾可以在數組中循環變化。解決了移動數據的時間損耗,使得本來插入和刪除是O(n)的時間復雜度變成了O(1)。