數據結構淺析(四):棧與隊列

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; 
棧普通3種狀態

現在有一個棧,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 的執行過程,如下圖:

Fbi(5)執行過程
Fbi(5)執行過程

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)。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 棧 棧是限定僅在表尾進行插入和刪除操作的線性表。 棧又稱為后進先出(Last In First Out )的線性表...
    jtsky閱讀 666評論 0 0
  • 棧是限定僅在表尾進行插入和刪除操作的線性表。隊列是只允許在一端進行插入操作,而在另一端進行刪除操作的線性表。 棧的...
    Yix1a閱讀 543評論 0 0
  • 棧 棧的英文單詞是Stack,它代表一種特殊的線性表,這種線性表只能在固定一端(通常認為是線性表的尾端)進行插入,...
    Jack921閱讀 1,527評論 0 5
  • 妖妖其人,何以吟之? 艷艷其詞,以詩見之。 渺渺其話,以知遠事。 卿卿其言,以寥心思。
    木土有阿杜閱讀 363評論 1 2
  • 喜歡穩定的生活,喜歡安排就緒,有條不紊的狀態,遇到突發情況就焦慮不安,本能地厭惡逃避。沒錯,這就是我今天的情緒覺知...
    Tina_Sun閱讀 212評論 0 0