俗話說得好,線性表(尤其是鏈表)是一切數據結構和算法的基礎,很多復雜甚至是高級的數據結構和算法,細節處,除去數學和計算機程序基礎的知識,大量的都在應用線性表。
一、棧
其實本質還是線性表:限定僅在表尾進行插入或刪除操作。 俗稱:后進先出 (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
棧中元素遍歷:b
棧中元素遍歷:k
棧中元素遍歷:y
2
棧中元素遍歷:b
棧中元素遍歷:k
棧已經清空!
棧成功銷毀!
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電子書,資料,幫忙內推,歡迎拍磚!