第4章 棧與隊列
4.2 棧的定義
棧(stack) 是限定僅在表層進行插入和刪除操作的線性表
允許插入和刪除的一端稱為棧頂(top)
另一端稱為棧底(bottom)
后進先出(Last In First Out) 線性表, 簡稱 LIFO
棧的插入操作: 進棧, 壓棧, 入棧
棧的刪除操作: 出棧, 彈棧
棧本身是一個線性表, 之前討論的有關線性表的順序存儲和鏈式存儲對于棧同樣適用
4.4 棧的順序存儲結構及實現
4.5 兩棧共享空間
4.6 棧的鏈式存儲結構及實現
4.6.2 棧的鏈式結構-----進棧操作
棧是由棧元素構成的線性鏈表, 其中有一個棧元素為棧頂元素.
不存在棧溢出情況. 除非整個內存都用完了
空棧: 當 top = NULL 時候
棧元素:
typedef struct StackNode
{
sElemType data;// 元素內容
struct StackNode *next; //指向下一個元素地址
} StackNode, *LinkStackPtr;
棧頂元素
typedef struct LinkStack
{
LinkStackPtr top;// 指向棧頂元素位置
int count;// 棧元素的數量
}LinkStack;
進棧操作 (棧為鏈式存儲結構時, 不用考慮棧溢出)
過程:
創建新的棧元素 s , s 的數據data 裝我們的新數據, s 的 next 指針 指向棧頂top元素
把棧頂元素top指向這個新的棧元素 s, 讓s 稱為新的棧頂, 棧頂元素的cout屬性加一
代碼如下:
// 先配置 新的棧元素
// 再配置 棧頂元素
Status push(LinkStack *S, sElemType e)
{
// 大寫 S 是整個棧, e 是單純的數據
// 小寫 s 是棧元素
LinkStackPtr s = (LinkStackPtr)malloc(sizeof(StackNode));
// 棧元素的數據 為 e
s->data = e;
// 棧元素的 next 指向之前的棧頂元素.
s->>next = S->top;
// 棧頂元素現在是 s
S->top = s;
// 棧元素數量加1
S->count++;
return OK;
}
4.6.3 棧的鏈式存儲結構-----出棧結構
當棧頂 top = NULL 時 為空棧, 不能執行出棧操作(Pop)
過程:
拿到棧頂數據
取出棧頂元素
棧頂元素更新為下一個棧元素
釋放取出的棧頂元素
代碼如下:
Status Pop(LinkStack *S, sElemType *e)
{
// 傳入參數 S: 整個棧
// 傳入參數 *e : 用來存棧頂數據的存儲位置
//p : 棧頂元素
LinkStackPtr p;
// 先判斷是否是空棧
if(StackEmpty( *s )){
return ERROR;
}
// *e 獲取棧頂的數據
*e = S->top-data;
// 給 p 賦值為 棧頂元素
p = S->top;
// 棧頂元素指向下一個元素(出棧)
S->top = S->top->next;
// 釋放被出棧的棧頂元素p
free(p);
// 跟新棧元素數量
S->count--;
// 出棧操作結束
return OK;
}
總結: 如果棧的使用過程中元素變化不可預料, 有時很小, 有時非常大, 那么最好用鏈棧,
反之, 如果它的變化在可控范圍內, 建議使用順序棧會更好一些
4.8 棧的應用----遞歸
4.8.1 斐波那契數列實現
F(n) = 0, 當n=0
F(n) = 1, 當n=1
F(n) = F(n-1) + F(n-2), 當n>1
打印前40位的斐波那契數列
簡單的實現:
int main()
{
// 從斐波那契數列這個名字上, 我們就可以看到, 可以用數組來實現.
// 根據公式的規律把數列和數組的每個元素對應存起來.
// 直接給 a[0] 設值 為 0
// 直接給 a[1] 設值 為 1
// a[i] = a[i-1] + a[i-2] 和公式 F(n) = F(n-1) + F(n-2) 對應起來.
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;
}
遞歸實現方法:
/* 上一個實現方法的思路是 從頭到尾 的推斷方式
先考慮 i=0 再考慮 i=1 一直到 i = 40
*/
/* 這里的遞歸采用的是 從尾到頭 的推斷方式
先是 i = 40, i = 39, i = 38, 一直到 i = 1, i = 0
*/
// 斐波那契的遞歸函數
int Fbi( int i )
{
if (i<2)
{
// i=0 時 Fbi(0) = 0
// i=1 時 Fbi(1) = 1
return i==0 ? 0 : 1;
}
// i >= 2時: Fib(i) = Fib(i-1) + Fib(i-2)
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;
}
4.8.2 遞歸定義
我們把一個直接調用自己或通過一系列的調用語句間接地調用自己的函數, 稱作遞歸函數
每個遞歸定義, 必須至少有一個條件, 滿足時遞歸不再進行, 即不再引用自身而是返回值退出
-
遞歸和迭代:
- 迭代: 使用循環結構, 不需要反復調用函數和占用額外的內存,
- 遞歸: 使用選擇結構, 使程序的結構更清晰, 更簡潔, 更容易讓人理解, 從而減少讀懂代碼的時間. 但是大量的遞歸調用會建立函數的副本, 會耗費大量的時間和內存.
遞歸是用棧這種數據結構實現的. 簡單的說, 在執行過程中, 對于每一層遞歸, 函數的局部變量, 參數以及返回地址都被壓入棧中, 在退回階段, 位于棧頂的局部變量, 參數值和返回地址被彈出, 用于返回調用層次中執行代碼的其余部分, 也就是恢復了調用的狀態.
4.9 棧的應用 -- 四則運算表達式求值
4.9.1 后綴 (逆波蘭)表示法定義
4.9.2 后綴表達式計算結構
4.9.3 中綴表達式轉后綴表達式
4.10 隊列的定義
定義: 隊列(queue) 是只允許在一端進行插入操作. 而在另一端進行刪除操作的線性表.
隊列是一種先進先出(First In First Out)線性表, 簡稱FIFO. 允許插入的一端稱為隊尾, 允許刪除的一端稱為隊頭. 假設隊列是 q = (a1, a2, ......, an) , 那么 a1 就是隊頭元素, 而 an 是隊尾元素, 這樣我們就可以刪除時, 總是從 a1 開始, 插入時, 列在最后.
隊頭 隊尾
出隊列 <--- a1, a2, a3, ...... an <---入隊列
4.12 循環隊列
順序隊列的不足: 造成假溢出
循環隊列: 后面滿了, 就再從頭開始, 也就是頭尾相接的循環. 我們把隊列的這種頭尾相接的順序存儲結構稱為循環隊列
c 語言中 區域運算符%
比如 5%4 = 1
4%5 = 4
循環隊列的順序存儲結構代碼
//隊列結構體
typedef int QElemType; // QElemType 為 int 類型(類型按照實際情況來定, 這里假設為 int)
typedef struct
{
QElemType data[MAXSIZE];// int 類型數組
int front; // 頭指針
int rear; // 尾指針
}sqQueue
循環隊列的初始化代碼
Status InitQueue(sqQueue *Q)
{
Q->front=0;
Q->rear=0;
return OK;
}
循環隊列求隊列長度代碼如下:
當 rear > front 時, 隊列長度為: QueueLength = rear - front
當 rear < front 時, 隊列長度為: QueueLength = (QueueSize - front) + (0 + rear)
把上面兩個條件結合在一起得到公式: QueueLength = (rear - front + QueueSize) % QueueSize
int QueueLength(sqQueue Q)
{
return (Q.rear - Q.front + MAXSIZE) % MAXSIZE;
}
循環隊列的入隊列操作代碼如下:
Status EnQueue(SqQueue *Q, QElemType d)
{
// 判斷是否隊列已經滿了
if((Q->rear + 1) % MAXSIZE == Q->front)
{
return ERROR;
}
// 隊列中插入值
Q->data[Q->rear] = e;
// 后移隊尾指針
Q->rear = (Q->rear+1)%MAXSIZE;
return OK;
}
循環隊列的出隊列操作代碼如下:
Status DeQueue(sqQueue *Q, QElemType *e)
{
// 判斷隊列是否為空
if(Q->front == Q->rear)
{
return ERROR;
}
// 取出隊首數據
*e = Q->data[Q->front];
// 隊首 指針后移
Q->front = (Q->front+!)%MAXSIZE;
return OK;
}
4.13 隊列的鏈式存儲結構及實現
隊列的鏈式存儲結構, 其實就是線性表的單鏈表, 只不過它只能尾進頭出而已, 我們把它簡稱為鏈隊列
空隊列時, front 和 rear 都指向頭結點
鏈隊列的結構:
- 頭指針指向鏈隊列的頭結點.
- 隊尾指針指向終點結點
typedef int QElemType; // QElemType 是int 類型, (可以根據實際情況自己設定類型)
typedef struct QNode // 結點結構
{
QElemType data; // 結點數據
struct QNode *next;//存放下一個結點的指針
}
typedef struct
{
QueuePtr front, rear; // 隊頭, 隊尾 指針
} LinkQueue;
隊列的鏈式存儲結構-----入隊操作
Status EnQueue(LinkQueue *Q, QElemType e)
{
// 給新的隊列元素分配內存
QueuePtr s = (QueuePtr)malloc(sizeof(QNode));
if(!s)
{
exit(OVERFLOW); // 退出程序, 并返回 OVERFLOW 的值給主程序.(當運算溢出時)
}
s->data = e; // 新的隊列元素賦值
s->next = Null; // 新的隊列元素的下一個元素為 Null
Q->rear->next = s; // 隊尾指針的下一個元素 為 s
Q->rear = s; // 隊尾指針指向 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 = p->data;
// 隊頭指針后移
Q->front->next = p->next
// 如果隊列被清空
if(Q->rear == p)
{
Q->rear = Q -> front;
}
// 釋放p
free(p);
return OK
}
- 循環隊列和鏈隊列比較:
- 時間上: 沒有什么大的區別
- 空間上: 循環隊列必須有一個固定的長度, 就有了存儲元素個數和空間浪費的問題.空間上,鏈隊列會更加靈活.
可以確定隊列長度最大值時,使用循環隊列
不能預估隊列長度時, 使用鏈隊列