棧的存儲結構和常見操作(c 語言實現)


俗話說得好,線性表(尤其是鏈表)是一切數據結構和算法的基礎,很多復雜甚至是高級的數據結構和算法,細節處,除去數學和計算機程序基礎的知識,大量的都在應用線性表。

一、棧

其實本質還是線性表:限定僅在表尾進行插入或刪除操作。 俗稱:后進先出 (LIFO=last in first out結構),也可說是先進后出(FILO)。

同樣的,棧也分為順序和鏈式兩大類。其實和線性表大同小異,只不過限制在表尾進行操作的線性表的特殊表現形式。

1、順序棧:利用一組地址連續的存儲單元依次存放自棧底到棧頂的數據元素,同時附設指針 top 指示棧頂元素在順序棧中的位置,附設指針 base 指示棧底的位置。 同樣,應該采用可以動態增長存儲容量的結構。且注意,如果棧已經空了,再繼續出棧操作,則發生元素下溢,如果棧滿了,再繼續入棧操作,則發生元素上溢。棧底指針 base 初始為空,說明棧不存在,棧頂指針 top 初始指向 base,則說明棧空,元素入棧,則 top++,元素出棧,則 top--,故,棧頂指針指示的位置其實是棧頂元素的下一位(不是棧頂元素的位置)。

  1 #ifndef _____ADT__
  2 #define _____ADT__
  3 #include <stdbool.h>
  4 #include <stdio.h>
  5 #include <stdlib.h>
  6 #define STACK_SIZE 50
  7 #define STACK_INCREMENT 10
  8 
  9 typedef struct{
 10     int stackSize;//棧容量
 11     char *base;//棧底指針
 12     char *top;//棧頂指針
 13 } SqStack;
 14 
 15 //初始化
 16 //本質還是使用動態數組
 17 void initStack(SqStack *s)
 18 {
 19     s->base = (char *)malloc(STACK_SIZE * sizeof(char));
 20     //分配成功
 21     if (s->base != NULL) {
 22         //空棧
 23         s->top = s->base;
 24         s->stackSize = STACK_SIZE;
 25     }
 26     else
 27     {
 28         puts("分配失敗!");
 29     }
 30 }
 31 
 32 //判空
 33 bool isEmpty(SqStack s)
 34 {
 35     return s.top == s.base ? true : false;
 36 }
 37 
 38 //判滿
 39 bool isFull(SqStack s)
 40 {
 41     return (s.top - s.base) >= STACK_SIZE ? true : false;
 42 }
 43 
 44 //求當前長度
 45 int getLength(SqStack s)
 46 {
 47     int i = 0;
 48     char *q = s.top;
 49     
 50     while (q != s.base) {
 51         q--;
 52         i++;
 53     }
 54     
 55     return i;
 56 }
 57 
 58 //求棧頂元素
 59 char getTop(SqStack s, char topElement)
 60 {
 61     if (isEmpty(s)) {
 62         puts("棧空!");
 63     }
 64     
 65     topElement = *(s.top - 1);
 66     return topElement;
 67 }
 68 
 69 //入棧
 70 void push(SqStack *s, char topElement)
 71 {
 72     char *q = NULL;
 73     
 74     if (isFull(*s)) {
 75         q = (char *)realloc(s->base, STACK_INCREMENT * sizeof(char));
 76         
 77         if (NULL == q) {
 78             exit(0);
 79         }
 80         
 81         s->base = q;
 82         s->stackSize = s->stackSize + STACK_INCREMENT;
 83     }
 84     //進棧
 85     *s->top++ = topElement;
 86 }
 87 
 88 //出棧
 89 void pop(SqStack *s, char *topElement)
 90 {
 91     if (isEmpty(*s)) {
 92         exit(0);
 93     }
 94     
 95     s->top--;
 96     *topElement = *s->top;
 97 }
 98 
 99 //遍歷
100 void traversal(SqStack s)
101 {
102     for (int i = 0; i < getLength(s); i++) {
103         printf("棧中元素遍歷:%c \n", s.base[i]);
104     }
105 }
106 
107 //清空
108 void cleanStack(SqStack *s)
109 {
110     if (!isEmpty(*s)) {
111         s->top = s->base;
112         puts("棧已經清空!");
113     }
114 }
115 
116 //銷毀
117 void destroyStack(SqStack *s)
118 {
119     if (s->base != NULL) {
120         free(s->base);
121         s->base = NULL;
122         s->top = NULL;
123         s->stackSize = 0;
124         puts("棧成功銷毀!");
125     }
126 }
127 
128 #endif /* defined(_____ADT__) */

 函數: void exit(int status);    所在頭文件:stdlib.h

 功 能: 關閉所有文件,終止正在執行的進程。

 exit(1)表示異常退出.這個1是返回給操作系統的。

 exit(x)(x不為0)都表示異常退出

 exit(0)表示正常退出

 exit()的參數會被傳遞給一些操作系統,包括UNIX,Linux,和MS DOS,以供其他程序使用。

 

 exit()和return的區別:

 按照ANSI C,在最初調用的main()中使用return和exit()的效果相同。 但要注意這里所說的是“最初調用”。如果main()在一個遞歸程序中,exit()仍然會終止程序;但return將控制權移交給遞歸的前一級,直到最初的那一級,此時return才會終止程序。

return和exit()的另一個區別在於,即使在除main()之外的函數中調用exit(),它也將終止程序。

 

 _exit()與exit的區別:

 頭文件不同:

 exit:#include<stdlib.h>

 _exit:#include<unistd.h>

 _exit()函數:直接使進程停止運行,清除其使用的內存空間,並銷毀其在內核中的各種數據結構;

 exit()函數則在這些基礎上作了一些包裝,在執行退出之前加了若干道工序。比如系統調用之前exit()要檢查文件的打開情況,把文件緩沖區中的內容寫回文件。

 

 1 #include "ADT.h"
 2 
 3 int main(void) {
 4     char temp = '0';
 5     SqStack stack;
 6     initStack(&stack);
 7     
 8     printf("%d\n", getLength(stack));
 9     
10     push(&stack, 'b');
11     push(&stack, 'k');
12     push(&stack, 'y');
13     
14     printf("%d\n", getLength(stack));
15     // 函數使用temp之前必須初始化
16     temp = getTop(stack, temp);
17     printf("%c\n", temp);
18     
19     traversal(stack);
20     
21     pop(&stack, &temp);
22     printf("%d\n", getLength(stack));
23     
24     traversal(stack);
25     
26     cleanStack(&stack);
27     
28     destroyStack(&stack);
29     
30     return 0;
31 }

測試結果:

0

3

y

棧中元素遍歷:

棧中元素遍歷:

棧中元素遍歷:

2

棧中元素遍歷:

棧中元素遍歷:

棧已經清空!

棧成功銷毀!

Program ended with exit code: 0 


 

順序棧的小結:

1)、盡量使用指向結構的指針做函數參數,這樣的操作比結構體變量作函數參數效率高,因為無需傳遞各個成員的值,只需傳遞一個結構的地址,且函數中的結構體成員並不占據新的內存單元,而與主調函數中的成員共享存儲單元。這種方式還可通過修改形參所指成員影響實參所對應的成員值。

2)、棧清空,一定是棧頂指向棧底,不可顛倒,否則析構出錯!

3)、再次注意二級指針和一級指針做函數參數的不同

4)、使用一個指針變量之前,必須初始化,不為指針分配內存,即指針沒有指向一塊合法的內存,那么指針無法使用,強行運行出錯,這就是野指針的危害。類似其他類型變量都是如此,(這也是為什么建議聲明變量的同時就立即初始化,哪怕是一個不相干的數值),就怕程序寫的復雜的話,一時忘記變量是否初始化,導致出錯。

5)、為什么順序棧(包括順序表)在初始化函數的傳入參數里不用二級指針傳參?

    個人理解:

  首先清楚函數傳參的類型,值傳遞,引用(c++)傳遞,和指針傳遞,且函數修改的是實參的一份拷貝,並不是直接去修改實參。 

    問題是,在之前的鏈表里,定義結點結構( Node)和指向結點的指針 p,有 struct Node *p;為了給結點分配內存,把這個指針(p本身占據一份內存空間,在棧區操作系統分配,但是 p 的指向是沒有初始化的)p 傳入初始化函數內,等於是傳入的指向結點Node的一個指針 p 的拷貝 _p,而這個拷貝 _p 本身(假設指針變量p自己在內存的地址是0x1111)和拷貝 _p 存儲的(指針 p指向的 內存區域)內容是不一樣的,此時給 拷貝 _p 使用malloc函數分配內存,看似是修改了 _p ,實際上是把 _p 指向的內存區域改變了, p 本身在內存的地址(0x1111)沒有被改變,故函數執行完畢,棧分配的內存被操作系統回收,然后參數返回給主調函數,那份拷貝 _p 還是以前傳入的那份拷貝 _p, 高矮胖瘦那是紋絲未動,故不會對實參起到修改的作用,完全類似值傳遞,在值傳遞,就是把實參的本身的值傳入函數,如果函數內部對其拷貝進行修改,其實傳入的參數本身並沒有被改變,雖然函數體內,他的值被修改了,但是一旦函數執行完,棧的內存被系統回收,修改就變得徒勞。

 

    順序棧里,一般是定義的整個表List結構,struct List p;變量p 就是一個實例化的棧結構,注意 p 已經分配了內存(主調函數 main ,操作系統在棧區給p分配),這和主調函數里鏈表的指針 p 不一樣,鏈表的指針 p 在 main 函數,只是給指針本身分配了內存空間,但是對 其指向的表結點沒有分配,需要在初始化函數初始化!故到了順序棧里,當給函數傳入&p(因為開始main 已經給棧結構分配了內存空間,而&p 是棧結構 p 在內存的地址0x1111,也就是棧本身的首地址,也是 base指向的棧的基址),同樣是傳入一份拷貝 _&p ,且不是給 _&p  malloc 內存,而是給 _&p 指向的內容分配空間—— (&p)->base(表的基地址)分配內存,也就是說,這里堆 p 修改也是沒用的,但是對 p 的指向修改,也就是 base,是有用的。而 base 本身就是一個指針,p 其實和 base 值相等,只不過變量的類型不一樣,故不需要再傳入二級指針。

 

6)、初始化那里,其實寫的不好,主調函數里 s 分配了內存,如果沒有對這個結構體初始化,就不代表 s 的成員 base 或者 top 等就是有指向的,更別說 NULL 了。很大程度是指向是垃圾值,不確定的內存區域,故這里的判斷

 if (s->base != NULL)

在這個前提下,是沒有用處的語句。故還是聲明變量的同時,最好是初始化,哪怕是0或者 NULL。 

 

2、鏈棧

其實就是鏈表的特殊情形,一個鏈表,帶頭結點,棧頂在表頭,插入和刪除(出棧和入棧)都在表頭進行,也就是頭插法建表和頭刪除元素的算法。顯然,鏈棧還是插入刪除的效率較高,且能共享存儲空間。

是棧頂在表頭!棧頂指針指向表頭結點。棧底是鏈表的尾部,base 就是尾指針。還有,理論上,鏈式結構沒有滿這一說,但是理論上是這樣的,也要結合具體的內存,操作系統等環境因素。

  1 #ifndef _____ADT__
  2 #define _____ADT__
  3 #include <stdbool.h>
  4 #include <stdio.h>
  5 #include <stdlib.h>
  6 
  7 typedef struct Node{
  8     char data;
  9     struct Node *next; //next 指針
 10 } Node, *ListStack;
 11 
 12 //初始化頭結點,top 指針指向頭結點
 13 void initStack(ListStack *top)
 14 {
 15     //top就是頭指針!也就是棧頂指針
 16     *top = (ListStack)malloc(sizeof(Node));
 17     //就一個結點,也就是空棧,其實和鏈表一樣,沒啥意思
 18     (*top)->next = NULL;
 19     //內容初始化
 20     (*top)->data = '0';
 21 }
 22 
 23 //判空
 24 bool isEmpty(ListStack top)
 25 {
 26     //棧頂指針的next==NULL 就是空棧,沒有滿的判斷,但是也要悠着點。小心內存受不了。
 27     return top->next == NULL ? true : false;
 28 }
 29 
 30 //入棧
 31 void push(ListStack top, char topElement)
 32 {
 33     ListStack q = NULL;
 34     q = (ListStack)malloc(sizeof(Node));
 35     
 36     if (NULL == q) {
 37         exit(0);
 38     }
 39     //類似頭插法建表,進棧
 40     q->next = top->next;
 41     top->next = q;
 42     //賦值
 43     top->data = topElement;
 44     //棧底永遠是表尾指針
 45 }
 46 
 47 //出棧
 48 void pop(ListStack top, char *topElement)
 49 {
 50     ListStack p = NULL;
 51     
 52     if (isEmpty(top)) {
 53         exit(0);
 54     }
 55     //棧頂元素出棧,記住,棧頂指針永遠是指向棧頂元素的下一位,p 指向棧頂元素
 56     p = top->next;
 57     *topElement = p->data;
 58     //刪除這個元素
 59     top->next = p->next;
 60     free(p);
 61 }
 62 
 63 //求當前長度
 64 int getLength(ListStack top)
 65 {
 66     int i = 0;
 67     ListStack q = top->next;
 68     
 69     while (q != NULL) {
 70         i++;
 71         q = q->next;
 72     }
 73     
 74     return i;
 75 }
 76 
 77 //求棧頂元素
 78 char getTop(ListStack top, char topElement)
 79 {
 80     if (isEmpty(top)) {
 81         puts("棧空!");
 82     }
 83     
 84     topElement = top->next->data;
 85     return topElement;
 86 }
 87 
 88 //遍歷
 89 void traversal(ListStack top)
 90 {
 91     ListStack p = top->next;
 92     
 93     for (int i = 0; i < getLength(top); i++) {
 94         printf("棧中元素遍歷:%c \n", p->data);
 95         p = p->next;
 96     }
 97 }
 98 
 99 //銷毀
100 void destroyLinkStack(ListStack *top)
101 {
102     ListStack p = *top;
103     ListStack pn = (*top)->next;
104     
105     while (pn != NULL)
106     {
107         free(p);
108         p = pn;
109         pn = pn->next;
110     }
111     //銷毀最后一個
112     free(p);
113     p = NULL;
114     puts("棧成功銷毀!");
115 }
116 
117 #endif /* defined(_____ADT__) */

main 函數

 1 #include "ADT.h"
 2 
 3 int main(void) {
 4     ListStack stack = NULL;
 5     initStack(&stack);
 6     
 7     printf("棧長度 = %d\n", getLength(stack));
 8     
 9     push(stack, 'a');
10     push(stack, 'b');
11     push(stack, 'c');
12     push(stack, 'd');
13     
14     printf("棧長度 = %d\n", getLength(stack));
15     
16     traversal(stack);
17     
18     char temp = '0';
19     printf("棧頂元素 = %c\n", getTop(stack, temp));
20     pop(stack, &temp);
21     
22     printf("棧長度 = %d\n",getLength(stack));
23     
24     traversal(stack);
25     
26     destroyLinkStack(&stack);
27     
28     return 0;
29 }

其實鏈棧和鏈表是一樣的,沒什么新鮮的東西。可見,線性表,尤其是鏈表,是數據結構和算法里的重中之重,后續很多復雜高級的數據結構和算法,都會無數次的用到鏈表的相關知識和概念。

 

歡迎關注

 

dashuai的博客是終身學習踐行者,大廠程序員,且專注於工作經驗、學習筆記的分享和日常吐槽,包括但不限於互聯網行業,附帶分享一些PDF電子書,資料,幫忙內推,歡迎拍磚!

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM