《大話數據結構》第四章 棧和隊列


第四章 棧和隊列

棧的定義
限定僅在表尾進行插入和刪除操作的線性表,我們把允許插入和刪除的一端稱爲棧頂(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。允許插入的一段稱爲隊尾,允許刪除的一端稱爲隊頭

隊列的抽象數據類型


循環隊列

問題來源
如果知識普通的隊列,使用起來是有問題的,下面是每次出入都移動的方法,但是每次都移動會很麻煩:

如果使用不移動的方法,那麼要有兩個指針,分別指向隊頭和隊尾,每次進出都移動指針,然後如果插入的總數量超過限定的數量,還是會出問題:

a1,a2a_1,a_2再入a5a_5

這種明明還有空位(0和1)卻還發生溢出的情況,稱爲假溢出,所以使用把隊列首尾相連的方法來解決假溢出。

新的問題:rear和front重合怎麼辦呢,兩種方法,一是設置一個flag;二是保留一個空間,不讓滿。方法二如下圖:

方法二相關

  1. 隊列滿的條件是:(rear + 1) % QueueSize == front
  2. 隊列長度計算公式:(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;
}

循環隊列會事先申請好空間,使用期間不釋放,而對於鏈隊列,經常需要申請和釋放,所以在可以確定隊列的最大長度的時候,建議使用循環隊列。


思維導圖

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章