文章目錄
第四章 棧和隊列
棧的定義:
限定僅在表尾進行插入和刪除操作的線性表,我們把允許插入和刪除的一端稱爲棧頂(top),另一端稱爲棧底(bottom),不含任何數據元素的叫空棧。棧又稱後進先出的線性表(Last In First Out),簡稱LIFO結構。
棧就像子彈彈夾,先放進去的後出來,最後放進去的第一個出來,後退、撤銷等操作都是用的棧實現的。
棧的插入操作:叫進棧,也稱壓棧、入棧,類似子彈入彈夾。
棧的刪除操作:叫出棧,也叫彈棧,如同彈夾的子彈彈出。
注意:棧的變化很多,不一定第一個進就要最後一個出,可能是進出進出。
棧的抽象數據類型:
普通情況、空棧、棧滿:
結構定義和出入棧代碼
棧的結構定義代碼:
typedef int SElemType;
typedef struct
{
SElemType data[MAXSIZE];
int top; // 棧頂指針
}SqStack;
入棧代碼:
Status Push(SqStack *S, SElemType e)
{
if (S->top == MAXSIZE - 1) // 棧滿
{
return ERROR;
}
S->top++; // 棧頂指針後移
S->data[S->top] = e; // 將新插入元素賦值給棧頂空間
return OK;
}
出棧操作:
Status Pop(SqStack *S, SElemType *e)
{
if (S->top == -1)
{
return ERROR;
}
*e = S->data[S->top]; // 將要刪除的棧頂元素賦值給e
S->top--; // 棧頂指針-1
return OK;
}
JAVA實現棧
棧類:
public class stack {
public Object[] data;
public int top;
public stack() {
}
}
class Stack_{
private int MAXSIZE;
private stack Sta;
// 初始化
public Stack_(int MAXSIZE) {
this.MAXSIZE = MAXSIZE;
Sta = new stack();
Sta.data = new Object[MAXSIZE];
Sta.top = -1; // 從-1開始,比較方便,能和數組index對應上
}
// 入棧
public void push(Object e){
// 判斷是否滿棧
if (Sta.top == MAXSIZE-1)
throw new IndexOutOfBoundsException("滿棧了");
Sta.top++;
Sta.data[Sta.top] = e;
}
public Object pop(){
if (Sta.top == -1)
throw new IndexOutOfBoundsException("已經是空棧了,無法再刪除");
Object data = Sta.data[Sta.top];
Sta.data[Sta.top] = null;
Sta.top--;
return data;
}
public void show(){
for (int i = 0; i < Sta.top + 1; i++) {
System.out.println(Sta.data[i]);
}
}
}
測試代碼:
public class main {
public static void main(String[] args) {
Stack_ S = new Stack_(3);
S.push(12);
S.push(13);
S.push(14);
S.show();
System.out.println("刪除:" + S.pop());
}
}
兩棧共享空間
一圖勝千言:
top1是棧1的棧頂指針,top2爲棧2的棧頂指針,除去存在空棧的情況下,top1 + 1 = top2 的時候,爲棧滿。
空間結構
typedef struct
{
SElemType data[MAXSIZE];
int top1;
int top2;
}SqDoubleStack;
push方法:
除了插入,還要判斷棧參數,參數決定要往哪個棧寫
Status Push(SqDoubleStack *S, SElemType e, int stackNumber)
{
if (S->top1 + 1 == S->top2) // 滿棧情況
return ERROR;
if (stackNumber == 1) // 往棧1走
S->data[++S->top1] = e; // 賦值+變地址
else if (stackNumber == 2)
S->data[--S->top2] = e;
return OK;
}
pop方法:
Status Pop(SqDoubleStack *S, SElemType *e, int stackNumber)
{
if (stackNumber == 1)
{
if (S->top1 == -1)
return ERROE;
*e = S->data[S->top1--];
}
else if (stackNumber == 2)
{
if (S->top2 == MAXSIZE)
return ERROR;
*e = S->data[S->top2++];
}
return OK;s
}
一般是此消彼長的兩組數據放一起會使用這張的。
JAVA實現雙棧共享空間
雙棧類:
public class Stack_ {
public Object[] data;
public int top1;
public int top2;
public Stack_() {
}
public Stack_(int len) {
this.data = new Object[len];
this.top1 = -1;
this.top2 = len;
}
}
class ShareStack {
private int MAXSIZE;
private Stack_ Sta;
public ShareStack(int MAXSIZE) {
this.MAXSIZE = MAXSIZE;
Sta = new Stack_(MAXSIZE);
}
public void push(Object e, int StackIdx){
if (StackIdx!=1 && StackIdx!=2)
throw new IndexOutOfBoundsException("鏈表的編號錯誤");
if (Sta.top1 >= Sta.top2-1)
throw new IndexOutOfBoundsException("滿棧了");
if (StackIdx == 1){
Sta.top1++;
Sta.data[Sta.top1] = e;
} else {
Sta.top2--;
Sta.data[Sta.top2] = e;
}
}
public Object pop(int StackIdx) {
if (StackIdx!=1 && StackIdx!=2)
throw new IndexOutOfBoundsException("棧的編號錯誤");
if (StackIdx == 1){
if (Sta.top1 == -1){
throw new IndexOutOfBoundsException("棧1無內容可刪除");
}
Object data = Sta.data[Sta.top1];
Sta.data[Sta.top1] = null;
Sta.top1--;
return data;
} else {
if (Sta.top1 == MAXSIZE){
throw new IndexOutOfBoundsException("棧2無內容可刪除");
}
Object data = Sta.data[Sta.top2];
Sta.data[Sta.top2] = null;
Sta.top2++;
return data;
}
}
public void show(int StackIdx){
if (StackIdx!=1 && StackIdx!=2)
throw new IndexOutOfBoundsException("棧的編號錯誤");
if (StackIdx==1) {
System.out.println("棧1爲:");
for (int i = 0; i < Sta.top1+1; i++) {
System.out.println(Sta.data[i]);
}
} else {
System.out.println("棧2爲:");
for (int i = MAXSIZE; i > Sta.top2-1; i--) {
System.out.println(Sta.data[i-1]);
}
}
}
}
測試代碼:
public class main {
public static void main(String[] args) {
ShareStack SS = new ShareStack(5);
SS.push(1,2);
SS.push(2,2);
SS.push(3,2);
SS.push("a",1);
SS.push("b",1);
SS.show(1);
SS.show(2);
System.out.println("--------------------");
System.out.println("刪除1中元素:" + SS.pop(1));
System.out.println("刪除2中元素:" + SS.pop(2));
System.out.println("--------------------");
SS.show(1);
SS.show(2);
}
}
棧的鏈式存儲結構
棧頂放在鏈表的頭部,鏈棧基本不存在棧滿的情況,對於空棧來說,鏈表原定義的是頭指針指向空,那麼鏈棧的空其實就是top=NULL的時候。
鏈棧的結構代碼:
typedef struct StackNode
{
SElemType data;
struct StackNode *next;
}StackNode, *LinkStackPtr;
typedef struct LinkStack
{
LinkStackPtr top;
int count;
}LinkStack;
push操作:
這邊跟之前的鏈表相反,每個結點的next都是指向下面,也就是前面的元素,因爲鏈表是先進後出。
設元素值爲e的新節點是s:
Status Push(LinkStack *S, SElemType e)
{
LinkStackPrt s = (LinkStackPtr) malloc(sizeof(StackNode));
s->data = e;
s->next = S->top; // 將當前的棧元素賦值給新節點的直接後繼
S->top = s; // 新結點稱爲最頂端
S->count++;
return OK:
}
pop操作:
// 刪除S的棧頂元素並返回值,若爲空棧就返回ERROR
Status Pop(LinkStack *S, SElemType *e)
{
LinkStackPtr p;
if (StackEmpty(*S))
return ERROR;
*e = S->top->data;
p = S->top; //獲取節點地址,是爲了釋放
S->top = S->top->netx; // 指針下移
free(p);
S->count--;
return OK;
}
如果棧找那個的元素變化不可預料,最好使用鏈棧;如果變化在可控範圍內,建議使用順序棧。
JAVA實現鏈棧
鏈棧類:
class StackNode{
// 包含指向前一個的指針和內容
public Object data;
public StackNode next;
public StackNode() {
data = null;
next = null;
}
}
public class LinkStack {
private int count;
private StackNode top;
public LinkStack() {
count = 0;
top = new StackNode();
}
public void push(Object e){
// 先設置新東西
StackNode SN = new StackNode();
SN.data = e;
SN.next = top; // 指向前面
// 升級top
top = SN;
count++;
}
public Object pop(){
if (count==0)
throw new IndexOutOfBoundsException("無內容可刪除");
Object data = top.data;
top = top.next;
count--;
return data;
}
public void show(){
StackNode sn = top;
for (int i = 0; i < count; i++) {
System.out.println(sn.data);
sn = sn.next;
}
}
}
測試代碼:
public class main {
public static void main(String[] args) {
LinkStack LS = new LinkStack();
LS.push(1);
LS.push(2);
LS.push(3);
System.out.println("show:");
LS.show();
System.out.println("刪除:" + LS.pop());
System.out.println("show:");
LS.show();
}
}
棧的應用
棧的作用:
簡化了程序設計的問題,劃分了不同關注層次,使得思考範圍縮小,更加聚焦於我們要解決的問題核心。數組的話,還要分散精力考慮數組的下標等細節問題。
遞歸
遞歸定義:把直接調用自己或通過一系列的調用語句間接地調用自己的函數,稱爲遞歸函數。
和棧的關係:想象一下遞歸,執行到了最後之後,又會按照倒序來執行回去,所以編譯器是使用棧來實現遞歸的,前行調用函數時,函數的局部變量、參數值以及返回地址都被壓入棧中;退回時,這些又被彈出。
斐波那契數列實現
問題定義:兔子兩月大後能生小兔子,一對兔子每個月生一對兔子,問一年後有多少?
遞歸代碼:
int Fbi(int i)
{
if (i<2)
return i==0 ? 0 : 1;
return Fbi(i-1) + Fbi(i-2);
}
int main()
{
int i;
for (int i=0; i<40; i++)
print("%d", Fbi(i));
return 0;
}
四則運算表達式求值
先跳過
隊列
定義:隊列是隻允許在一端進行插入操作,而在另一端進行刪除操作的線性表。
隊列是一種先進先出(First In First Out)的線性表,簡稱FIFO。允許插入的一段稱爲隊尾,允許刪除的一端稱爲隊頭。
隊列的抽象數據類型:
循環隊列
問題來源:
如果知識普通的隊列,使用起來是有問題的,下面是每次出入都移動的方法,但是每次都移動會很麻煩:
如果使用不移動的方法,那麼要有兩個指針,分別指向隊頭和隊尾,每次進出都移動指針,然後如果插入的總數量超過限定的數量,還是會出問題:
出再入:
這種明明還有空位(0和1)卻還發生溢出的情況,稱爲假溢出,所以使用把隊列首尾相連的方法來解決假溢出。
新的問題:rear和front重合怎麼辦呢,兩種方法,一是設置一個flag;二是保留一個空間,不讓滿。方法二如下圖:
方法二相關:
- 隊列滿的條件是:(rear + 1) % QueueSize == front
- 隊列長度計算公式:(rear - front + QueueSize) % QueueSize
循環隊列結構代碼:
typedef int QElemType;
typedef struct
{
QElemType data[MAXSIZE];
int front;
int rear;
}SqQueue;
循環隊列初始化代碼:
Status InitQueue(SqQueue *Q)
{
Q->front = 0;
Q->rear = 0;
return OK;
}
循環隊列入隊操作:
Status EnQueue(SqQueue *Q, QElemType e)
{
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 + 1) % MAXSIZE;
return OK;
}
鏈式隊列
鏈隊列:隊列的鏈式存儲結構,其實就是線性表的單鏈表,只不過他只能尾進頭出而已,我們把它簡稱爲鏈隊列。
一般將隊頭指針指向鏈隊列的頭結點,而隊尾指針指向終端結點,空隊列時都指向頭結點:
鏈隊列的結構:
typedef int QElemType;
// 結點結構
typedef struct QNode
{
QElemType data;
struct QNode *next;
}QNode, *QueuePtr;
// 隊列的鏈式結構
typedef struct
{
QueuePtr front, rear; // 頭尾指針
}LinkQueue;
兩種約束,一是元素間有next指引,二是有頭尾指針約束範圍,不過這次是無限增長了,也不用判斷循環什麼的。
鏈隊列的入隊操作:
就是在鏈表尾部插入結點
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;
Q->rear = s;
return OK;
}
鏈隊列的出隊操作:
就是頭結點的後繼節點出隊,頭結點的後繼指向改爲後面的結點,若除頭結點外只剩一個元素,則需將rear指向頭結點。
Status DeQueue(LinkQueue *Q, QElemType *e)
{
QueuePrt p;
if (Q->front == Q-rear)
return ERROR;
p = Q->front->next;
*e = p->data; // 獲取被刪除的元素
Q->front->next = p->next; // 指向後面的結點
if (Q->rear == p) // 如果只有一個並且刪掉了的話,rear指向頭結點
Q->rear = Q->front;
free(p);
return OK;
}
循環隊列會事先申請好空間,使用期間不釋放,而對於鏈隊列,經常需要申請和釋放,所以在可以確定隊列的最大長度的時候,建議使用循環隊列。