第 4 章 棧與隊列

棧是限定僅在表尾進行插入插入和刪除操作的線性表;
隊列是只允許在一端進行插入操作,而在另一端進行刪除操作的線性表。

4.1 開場白

4.2 棧的定義

4.2.1 棧的定義

棧是限定僅在表尾進行插入插入和刪除操作的線性表
  允許插入和刪除的一端稱爲棧頂(top),另一端稱爲棧底(button)。不含任何數據元素的棧稱爲空棧,棧又稱爲後進先出(Last In First Out)的線性表,簡稱LIFO結構。
  棧是一個特殊的線性表,特殊之處在於限制了插入和刪除的位置,它始終只在棧頂進行,這也就使得:棧底是固定的,最先進棧的只能在棧底。
  棧的插入操作:叫做進棧,也稱壓棧、入棧;刪除操作:叫做出棧,也叫做彈棧。
  

4.2.2 進棧出棧變化形式

最先進棧的元素,不一定是最後出棧。在不是所有元素都進棧的情況下,事先進去的元素也可以出棧,只要保證是棧頂元素出棧就可以了。

4.3 棧的抽象數據類型

ADT 棧 (stack)
Data
    同線性表。元素具有相同的類型,相鄰元素具有前驅和後繼的關係。
Operation
    InitStack(*S):初始化操作,建立一個空棧;
    DestroyStack(*S):若棧存在,則銷燬它;
    ClearStack(*S):將棧清空;
    StackEmpty(S):若棧爲空,返回TRUE,否則返回false;
    GetTop(S, *e):若棧存在且非空,用e 返回S的棧頂元素;
    Push(*S,e):若棧存在,插入新元素e到S中併成爲棧頂元素;
    Pop(*S,e):刪除棧S中棧頂元素,並用e返回其值;
    StackLength(S):返回棧S中的元素個數。

4.4 棧的順序存儲結構及實現

4.4.1 棧的順序存儲結構

簡稱爲順序棧。數組實現,下標爲0的一端作爲棧底。
棧的結構定義

#define MAXSIZE 20;
typedef int SElemType;
typedef struct{
    SElemType   data[MAXSIZE];
    int         top; //用於棧頂指針;
}SqStack;

4.4.2 棧的順序存儲結構——進棧操作

//插入元素e爲新的棧頂元素
Status Push(SqStack *S, SElemType e)
{
    if (S->top == MAXSIZE - 1)
        return ERROR;
    S->top++;
    S->data[S->top] = e;
    return OK;
}

4.4.3 棧的順序存儲結構——出棧操作

//若棧不空,則刪除S的棧頂元素,用e返回其值,並返回OK;否則返回ERROR
Status Pop(SqStack *S, SElemType *e)
{
    if (S->top == -1)
        return ERROR;
    *e = S->data[S->top];
    S->top--;
    return OK;
}

兩者的時間複雜度都是O(1),由於不存在線性表的插入和刪除需要移動元素問題, 棧的順序存儲結構還是很方便的。但是有一個很大的缺陷:必須事先確定數組存儲空間的大小。

4.5 兩棧共享空間

兩個相同類型的棧,可以最大限度第利用其事先開闢的存儲空間來進行操作。關鍵思路:它們是在數組的兩端,向中間靠攏。
空間結構代碼:

//兩棧共享空間結構
typedef struct
{
    SElemType data[MAXSIZE];
    int top1;   //棧1的棧頂指針
    int top2;   //棧2的棧頂指針
}SqDoubleStack;
//插入元素e爲新的棧頂元素
Status Push(SqDoubleStack *S, SElemType e, int stackNumber)
{
    if (S->top1 + 1 == S->top2)
        return ERROR;
    if (stackNumber == 1)
        S->data[++S->top1] = e;
    else if (stackNumber == 2)
        S->data[++S->top2] = e;
    return OK;
}

//若棧不空,則刪除S的棧頂元素,用e返回其值,並返回OK
Status Pop(SqDoubleStack *S, SElemType *e, int stackNumber)
{
    if (stackNumber == 1)
    {
        if (S->top == -1)
            return ERROR;
        *e = S->data[S->top1--];
    }
    else if (stackNumber == 2)
    {
        if (S->top2 == MAXSIZE)
            return ERROR;
        *e = S->data[S->top2++];
    }
    return OK;
}

兩棧共享空間,適用於兩個棧的空間需求具有相反關係時,即一個棧增長時另一個棧在縮短的情況。

4.6 棧的鏈式存儲結構及實現

4.6.1 棧的鏈式存儲結構

簡稱爲鏈棧。棧頂放在單鏈表的頭部,對於鏈棧來說,不需要頭指針的,用棧頂代替。
鏈棧的結構

//鏈棧的結構代碼如下:
typedef struct StackNode
{
    SElemType   data;
    struct StackNode * next;
}StackNode, *LinkStackPtr;

typedef struct LinkStack
{
    LinkStackPtr top;
    int count;
}LinkStack;

4.6.2 棧的鏈式存儲結構——進棧操作

//插入元素e爲新的棧頂元素
Status Push(LinkStack *S, SElemType e)
{
    LinkStackPtr s = (LinkStackPtr)malloc(sizeof(StackNode));
    s->data = e;
    s->next = S->top;
    S->top = s;
    S-count++;
    return OK;
}

4.6.3 棧的鏈式存儲結構——出棧操作

//若棧不空,則刪除S的棧頂元素,用e返回其值,並返回OK;
Status Pop(LinkStack *S, SElemType *e)
{
    LinkStackPtr p;
    if (StackEmpty(*S))
        return ERROR;
    *e = S->top->data;
    p = S->top;
    S->top = S->top->next;
    free(p);
    S->count--;
    return OK;
}

時間複雜度都爲O(1)。
對比順序棧和鏈棧:

  • 時間複雜度都爲O(1);
  • 順序棧需要事先確定一個固定的長度,存在空間浪費,但優勢是存取定位很方便。鏈棧則要求每個元素都有指針,增加了內存開銷,但對於棧的長度無限制
  • 如果棧的使用過程中元素變化不可預料,最後有鏈棧;反之,它的變化在可控範圍內,建議使用順序棧。

4.7 棧的使用

棧的引入簡化了程序設計的問題,劃分了不同的關注層次,使得思考範圍縮小,更加聚焦與我們要解決的問題核心。

4.8 棧的應用——遞歸

4.8.1 斐波那契數列實現

f(n)=0,1,F(n1)+F(n2), n = 0 n = 1 n>1

打印出前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 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;
}

4.8.2 遞歸定義

把一個直接調用自己或通過一系列的調用語句間接調用自己的函數,稱做遞歸函數。
每個遞歸定義必須至少有一個條件,滿足時遞歸不再進行,即不在引用自身而是返回值退出。
迭代使用循環結構,遞歸使用的是選擇結構。遞歸能使程序的結構更清晰、簡單,但是大量的遞歸調用會建立函數的副本,會耗費大量的時間和內存。迭代不需要反覆調用函數和佔用額外的內存。應該視情況不同選擇不同的代碼。
編譯器使用棧實現遞歸。

4.9 棧的應用——四則運算表達式求值

4.9.1 後綴(逆波蘭)表示法定義

不需要括號的後綴表示法,稱爲逆波蘭表示。是表達式的一種新的顯示方式,非常巧妙地解決了程序實現四則運算的難題。
對於“9+(3-1)*3+10 / 2”,用後綴表示法式“ 9 3 1- 3* + 10 2/ +”

4.9.2 後綴表達式計算結果

用後綴表示法式“ 9 3 1- 3* + 10 2/ +”
規則:從左到右遍歷表達式的每個數字和符號,遇到是數字就進棧,遇到是符號,就將處於棧頂兩個數字出棧,進行運算,運算結果進棧,一直到最終獲得結果。
下面推導如何讓9+(3-1)*3+10 / 2”,用後綴表示法式“ 9 3 1- 3* + 10 2/ +”

4.9.3中綴表達式轉後綴表達式

平時用的標準四則運算表達式稱中綴表達式。
中綴表達式9+(3-1)*3+10 / 2”,轉化爲後綴表達式“ 9 3 1- 3* + 10 2/ +”
規則:從左到右遍歷中綴表達式的每個數字和符號,若是數字就輸出,即成爲後綴表達式的一部分;若是符號,則判斷其與棧頂符號的優先級,是右括號或優先級不高於棧頂符號則棧頂元素依次出棧並輸出,並將當前符號進棧,一直到最終輸出後綴表達式爲止。
要想讓計算機具有處理標準表達式的能力:

  1. 將中綴表達式轉化爲後綴表達式;
  2. 將後綴表達式進行運算得出結果。

4.10 隊列的定義

隊列是只允許在一端進行插入操作,而在另一端進行刪除操作的線性表。隊列是一種先進先出(First In First Out)的線性表,簡稱FIFO。允許插入的一端是隊尾,允許刪除的一端稱爲隊頭

4.11 隊列的抽象數據類型

ADT 棧 (Queue)
Data
    同線性表。元素具有相同的類型,相鄰元素具有前驅和後繼的關係。
Operation
    InitQueue(*Q):初始化操作,建立一個空隊列Q;
    DestroyQueue(*Q):若隊列存在,則銷燬它;
    ClearQueue(*Q):將隊列清空;
    QueueEmpty(Q):若爲空,返回TRUE,否則返回false;
    GetHead(Q, *e):若隊裏存在且非空,用e 返回隊列的隊頭元素;
    EnQueue(*Q,e):若隊列存在,插入新元素e到Q中併成爲隊尾元素;
    DeQueue(*Q*e):刪除隊列中隊頭元素,並用e返回其值;
    QueueLength(Q):返回隊列中的元素個數。

4.12 循環隊列

4.12.1 隊列順序存儲的不足

插入隊尾時間爲O(1),刪除隊頭時間爲O(n)。

4.12.2 循環隊列定義

把隊列的這種頭尾相接的順序存儲結構稱爲循環隊列。
引入兩個指針front指針指向隊頭元素,rear指向隊尾元素的下一個位置。若隊列的最大尺寸爲QueueSize,則隊列慢的條件爲(rear+1)%QueueSize == front
通用的計算隊列長度公式爲:
(rear-front + QueueSize) % QueueSize

typedef int QElemType;
typedef struct
{
    QElemType   data[MAXSIZE];
    int         front;  //頭指針
    int         rear;   //尾指針,指向隊尾元素的下一個位置
}SqQueue;

//初始化一個空隊列Q
Status InitQueue(SqQueue *Q)
{
    Q->front = 0;
    Q->rear = 0;
}

//返回Q的元素個數,即隊列的當前長度
int QueueLength(SqQueue Q)
{
    return (Q.rear - Q.front + MAXSIZE) % MAXSIZE;
}

//插入元素e爲新的隊尾元素
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;
}

//若隊列不空,則刪除Q中隊頭元素,用e返回其值
Status DeQueue(SqQueue *Q, QElemType *e)
{
    if (Q->front == Q->rear)
        return ERRROR;
    *e = Q->data[Q->front];
    Q->front = (Q->front + 1) % MAXSIZE;

    return OK;
}

4.13 隊列的鏈式存儲結構及實現

就是線性表的單鏈表,只不過它只能尾進頭出而已, 簡稱爲鏈隊列。
鏈隊列的結構爲:

typedef int QElemType;
typedef struct QNode{
    QElemType       data;
    struct QNode    *next;
} QNode, *QueuePtr;

typedef struct
{
    QueuePtr front, rear;
}LinkQueue;

4.13.1 隊列的鏈式存儲結構——入隊操作

入隊操作時,其實就是在鏈表尾部插入節點:

//插入元素e爲Q的新的隊尾元素
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;
}

4.13.2 隊列的鏈式存儲結構——出隊操作

//若隊列不空,刪除Q的隊頭元素,用e返回其值,並返回OK,否則返回ERROR
Status DeQueue(LinkQueue *Q, QElemType *e)
{
    QueuePtr p;
    if (Q->front == Q->rear)
        return ERROR;
    p = Q->front->next;
    *e = p->data;
    Q->front->next = p->next;
    if (Q->rear == p)
        Q->rear = Q->front;
    free(p);
    return OK;
}

對於循環隊列與鏈隊列的比較,從時間上,它們的時間都爲O(1)。空間上來說,循環隊列必須有一個固定的長度,所以就有了存儲元素個數和空間浪費的問題,而鏈隊不存在這個問題,所以,鏈隊列更加靈活。

4.14 總結回顧

4.15 結尾語

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