数据结构 线性存储 -- 栈 讲解
1.棧的定義
??????????????? 我們有時會聽到這句話,? 靜態定義的內存是在棧中分配的, 動態內存是在堆里面分配的.
??????????????? 例如下面這個簡單的函數:
int f(int k){ int m = 2 * k;int * p = (int *)malloc(16);return m; }???????????????? 那么我們認為 作為參數的 k,?? 函數內靜態定義的整形變量 m,?? 指針p,? 它們本身都占用一定的內存.
????????????????? 其中m 和 k占用4字節,? p占用8字節(64位系統)
???????????????? 這些靜態變量本身所占用的內存就是靜態定義的內存,? 它們都在棧里面分配的
???????????????? 而(int *)malloc(16) 動態分配了16個字節的內存, 是在堆里面分配的.
???????????????? 那么到底什么是棧?
????????? 定義:
??????????????????????????????? 一種可以實現"先進后出"的存儲結構.????
??????????????? 那么什么是先進后出呢??? 就比如1個箱子,? 先放進去的東西在箱底,? 然后被后面放進去的東西蓋住了, 如果要取出箱底的東西, 則必然要實現把后面放進去的東西先取出來.
??????????????? 也就是說棧
?????????????? 1.只有1個 出入口(棧頂)
?????????????? 2. 另一端是封閉的(棧底)
?????????????? 3. 不允許在元素之間的插入移除操作
如下圖:
2.棧的分類
?????????????? C語言中, 棧可以分成兩類,? 靜態棧 和 動態棧
??????????? 2.1 靜態棧
????????????????????? 以數組為內核的棧, 就是靜態棧, 靜態棧里面各個元素的物理內存地址是連續的
??????????? 2.2 動態棧
????????????????????? 相應地, 以鏈表為內核的棧就是動態棧,? 棧里面的元素是用尾部指針來聯系的,?
??????????????? 本文主要講解的是動態棧.
3. 棧的基本操作
????????????? 棧只有兩個基本操作, 就是壓棧(入棧) 和 出棧
?????????? 3.1 壓棧
?????????????? 所謂入棧就是吧1個元素放到棧的頭部, 然后這個新元素就是棧頂了.
?????????? 3.2 出棧
?????????????? 從棧頂把棧頂元素移除出棧,? 然后棧頂就是原來棧頂的下1個元素.
?????????????? 可以理解出,? 對于棧來將, 增加和刪除元素都只能在棧頂進行,?? 沒有在棧的中間的任何元素操作.
?????????????? 所以動態棧本質上就是1個閹割了部分功能的鏈表.??
BOOL st_free(STPERSON * pSt){
?? ?st_clear(pSt);
?? ?free(pSt->phead);
?? ?free(pSt->pbuttom);
?? ?free(pSt);
?? ?pSt=NULL;
?? ?return TRUE;
}
4. 動態棧的基本結構
???????????? 上面提到了, 動態棧是以鏈表為核心的,?? 鏈表的一端是棧底, 另一邊就是出入棧頂了.
???????????? 究竟單鏈表的首節點是棧頂, 還是尾節點是棧頂?
????????? 4.1 假如棧頂是單鏈表的尾節點
???????????? 這種情況下, 單鏈表的首節點就是棧底了~? 如果進行入棧動作是挺方便的, 只需將棧頂的尾部指針指向新的元素. 然后新的元素指針set成NULL. 如下圖:
?????????????? 問題1, 怎么找到原來的棧頂(下圖元素4地址呢),?? 當然可以由鏈表的首節點一直遍歷, 知道某個元素的尾部指針是NULL就是棧頂, 不過遍歷是1個成本很高的動作, 所以實際上我們會定義1個棧頂指針, 專門存放棧頂的地址, 當進行入棧動作, 我們會這個棧頂指針指向新的棧頂地址.
????????????????? 但是我們進行出棧時, 就需要把棧頂元素移除. 實際上是把, 棧頂上面的元素的尾部指針set成NULL, 然后把棧頂指針指向這個元素(新棧頂), 就ok了.
????????????????? 問題來了, 因為單鏈表只能1個方向遍歷, 所以我們無法根據舊棧頂地址直接獲得新棧頂(上1個元素)的地址, 只能從鏈表的首元素(棧底)逐個遍歷,? 這就是用尾節點作為棧頂的弊端.
??????????
???????? 4.2 假如棧頂是單鏈表的尾節點
??????????? 這種情況下, 首節點是棧頂, 所以尾指點就是棧底了.???????????
??????????? 當執行入棧動作時,? 只需要吧新的元素的尾部指針指向舊棧頂元素, 然后棧頂指針指向這個新的元素就ok了, 一樣很方便.
???????????? 但是這樣的話鏈表的首節點就改變了, 也就是說整個鏈表的地址改變了
???????????? 如下圖
?????????????? 而當執行出棧動作時,? 需要獲得, 棧頂下1個元素的地址, 而這個地址就恰好存放在棧頂元素的尾部指針中,? 所以不需要遍歷就可以直接由棧頂地址獲得下面1個元素的地址了. 然后吧棧頂指針指向這個新棧頂地址就ok了.
?????????????? 如下圖:
????????? 可以看出這中模式下,? 無論出棧和入棧動作都可以方便地獲取所需元素的地址, 不需要遍歷. 所以我們1般會用1個鏈表的首節點作為棧頂.
???????? 4.3 添加不存放有效數據的頭節點和尾節點. 并把尾節點作為棧底.
???????????? 由上面的分析得出, 出棧和入棧的大部分情況下, 棧底元素基本不變的,? 而每執行1次出棧或入棧動作, 棧頂元素地址改變了, 整個鏈表的地址就改變了.
????????????? 為了方便操作, 在實際編碼中,? 我們會在鏈表里添加1個頭節點(并不是首節點), 然后頭節點的地址作為整個鏈表的地址, 頭節點的指針指向首節點的地址,??? 而棧頂指針仍然是指向首節點, 這樣的話, 改變首節點(出棧或入棧)的同時修改頭節點的指針, 這樣整個鏈表的地址就無需改變了.
?????????????? 同樣為了方便操作, 我們也會定義1個不存放實際數據的尾節點, 作為棧底, 實際意義上的棧底元素尾部指針指向這個棧底元素,? 那么執行1個棧是空的, 那么它仍然具有1個不存放實際數據的棧底
?????????????? 如下圖:
5. 一個動態棧的簡單c語言代碼實現
???????????? 當然了, 這個棧的內核是1個鏈表, 而且只會實現最基本的功能.
5.1 首先編寫1個頭文件
???????????? 在這個頭文件里, 我們要定義2個結構體,
???????????? 1個結構體對應棧的節點. 它應該包括若干數據成員和1個尾部指針成員pnext, 用于指向下1個節點.
???????????? 而另1個結構體對應棧本身, 它包括4個成員, 分別是:
???????????? phead???? 他是1個不存放有效數據的頭節點.? phead的地址就是棧的鏈表內核的地址.? phead->pnext 就是棧頂
???????????? pbuttom?? 他是1個不存放有效數據的棧底節點, 但phead->pnext 指向pbuttom時, 則這個是i個空棧.
???????????? len??????????? 它用于存放鏈表的節點個數, 方便程序員得到這個信息.
???????????? is_inited?? 用于判斷這個棧是否已經初始化,? 否則新定義1個棧, 里面成員肯定是野指針
???????????? 另外, 這個頭文件還應該聲明要定義的算法函數,? 這樣別的文件引用這個頭文件, 就可以使用對應的函數了.
代碼如下:
stuck1.h
#include "bool_me.h" #ifndef __STUCK1_H_ #define __STUCK1_H_struct person_st{ //nodeint id;char name[16];struct person_st * pnext;};typedef struct person_st PERSON_ST;struct stuck_person{ //structPERSON_ST * phead; //address of the headnode of the linklistPERSON_ST * pbuttom; // buttom of the stuckint len;BOOL is_inited;};typedef struct stuck_person STPERSON;//init a new node with dynamic memoryPERSON_ST * person_st_new(int id, char * pname);//printf the infomation of a nodevoid person_st_printf(PERSON_ST * pnode);//create a stuck with dynamic linklistSTPERSON * st_create(void);//judge whether the stuck is empty (phead->pnext == pbutton)BOOL st_is_empty(STPERSON * pSt);//push a new element into the stuckBOOL st_push(STPERSON * pSt, PERSON_ST * pnode);//pop a top element out from the stuckBOOL st_pop(STPERSON * pSt, PERSON_ST ** pOutput);//traverse the stuck to print all the elementsvoid st_print(STPERSON * pSt);//put out and free all the elements from the elements;BOOL st_clear(STPERSON *pSt);//traverse the stuck to free all the elements, and free the stuck itselfBOOL st_free(STPERSON * pSt);#endif上面定義了1個節點類型結構體 PERSON_ST
和1個棧結構體 STPERSON
可以見到我定義了若干個算法函數, 下面會逐個講解這些函數.
5.2 錯誤處理函數st_error(char * pstr)
這個函數專門用于輸出出錯信息, 并退出整個函數, 而且我不會讓外面的文件直接調用這個函數, 所以加上static 前序
stuck1.c?? //下面的函數定義都寫在這個文件中, 這個文件也要引用上面的頭文件
static void st_error(const char * pErr){printf("%s\n",pErr);exit(-1); }
5.3 動態新建1個節點函數 PERSON_ST * person_st_new(int id, char * pname)
?????? 這個函數功能是動態創建1個節點,? 所以動態定義就是指分配給它的內存是動態分配的, 這樣這個節點可以方便地讓其他函數使用, 必要時也可以手動釋放.
?????? 而且我們會接受兩個參數. 新建這個節點時,會同時給它的兩個成員賦值. 相當于初始化了.
?????? 代碼如下:
PERSON_ST * person_st_new(int id, char * pname){PERSON_ST * pnode = (PERSON_ST *)malloc(sizeof(PERSON_ST));if (NULL == pnode){st_error("fail to assign memory to new node");}pnode->id=id;strncpy(pnode->name, pname+0,15);return pnode; }
??????? 注意, 如上面代碼, 我還會判斷參數字符串 pname 的長度, 如果超過了結構體的成員定義, 則截取對應長度后再賦值
5.4 打印1個節點的函數 person_st_print(PERSON_ST * pnode)
這個函數太簡單, 不解析了
代碼如下:
void person_st_print(PERSON_ST * pnode){printf("id is %d, name is %s\n",pnode->id, pnode->name); }
5.5 創建并初始化1個 棧?? STPERSON * st_create(void)
相當于面向對象語言里的new函數啦.? 這里會動態分配內存給他的每個指針成員
步驟:
1. 動態分配內存給1個? 棧 指針pSt
2. 分別動態分配內存給棧成員 phead 和 pbuttom
3. 將phead的尾部指針指向puttom 這樣的話這個就是1個空棧
4. puttom的尾部指針指向NULL
5. 棧成員len 設為0
6. 棧成員is_init 設為TRUE
7. 返回這個棧指針
代碼如下:
STPERSON * st_create(void){STPERSON * pSt = (STPERSON *)malloc(sizeof(STPERSON));pSt->phead = (PERSON_ST *)malloc(sizeof(PERSON_ST));if (NULL == pSt->phead){st_error("fail to assign memory to headnode");}pSt->pbuttom = (PERSON_ST *)malloc(sizeof(PERSON_ST));if (NULL == pSt->pbuttom){st_error("fail to assign memory to buttom");}pSt->pbuttom = (PERSON_ST *)malloc(sizeof(PERSON_ST));pSt->phead->id=0;pSt->pbuttom->id=-1;pSt->phead->pnext = pSt->pbuttom;pSt->pbuttom->pnext=NULL;pSt->len=0;pSt->is_inited = TRUE;return pSt; }5.6 判斷某個棧是否空棧 BOOL st_is_empty(STPERSON * pSt)
這個函數也很簡單, 只需要判斷phead 的尾部指針是否指向 pbuttom(棧底)就可以了, 上面說過了, 這兩個節點都不存放有效數據的.? 也就是說這個兩個節點之間沒有任何存放有效數據的節點.
代碼如下:
BOOL st_is_empty(STPERSON * pSt){if (TRUE != pSt->is_inited){st_error("the stuck is not initialed yet");}if (pSt->phead->pnext == pSt->pbuttom){return TRUE;}return FALSE; }
5.7 入棧函數 BOOL st_push(STPERSON * pSt, PERSON_ST * pnode)
這個函數的作用就是將參數中的 pnode節點壓入 棧pSt中,? 至于這個pnode節點如何得來? 可以用上面的person_st_new新建1個(必須, 否則不能手動釋放).
步驟:
1. 這個要壓棧的節點尾部指針指向 phead->pnext(舊 棧頂),
2. phead->pnex 指向這個要壓棧的節點( 新棧頂)
3. 棧的成員len+1
代碼如下:
//push a new element into the stuck BOOL st_push(STPERSON * pSt, PERSON_ST * pnode){if (TRUE != pSt->is_inited){printf("the stuck is not initialed yet\n");return FALSE;}pnode->pnext = pSt->phead->pnext;pSt->phead->pnext = pnode;pSt->len++;return TRUE; }
5.8 棧打印所有元素函數 void st_print(STPERSON * pSt)
這里的元素指的是存放有效數據的節點.? 頭節點和棧底節點不打印輸出
邏輯也很簡單, 首先判斷是否空棧,? 然后從棧頂元素(頭節點的下1個元素)開始逐個輸出,? 知道遇到pbuttom
代碼如下:
//traverse the stuck to print all the elements void st_print(STPERSON * pSt){if (TRUE != pSt->is_inited){printf("the stuck is not initialed yet, fail to print it\n");return;}if (TRUE == st_is_empty(pSt)){printf("the stuck is empty!\n");return;}PERSON_ST * pnode = pSt->phead;while (pSt->pbuttom != pnode->pnext){pnode = pnode->pnext;person_st_print(pnode);} }
5.9 出棧函數 BOOL st_pop (STPERSON * pSt, PERSON_ST ** pOutput)
這個就是出棧函數, 他會把棧頂元素移除出這個棧.? 而且把這個棧頂元素的地址傳出到 pOutput.
這個pOutput 就是在外面定義的1個 節點類型指針, 然后把這個指針本身的地址傳入來作為參數.? 然后函數里會改變這個pOutput指針的值, 讓他指向棧頂元素, 只有動態分配的內存才能這樣操作啊.
原理可以參閱我的另一篇文章:
c語言 跨函數使用內存
步驟如下:
1. 判斷是否空棧, 否則返回false
2. 讓*pOutput 指向 棧頂
3. phead 指向棧頂的下1個元素(新棧頂)
4. 棧成員len-1;
5 返回true;
代碼如下:
BOOL st_pop(STPERSON * pSt, PERSON_ST ** pOutput){if (TRUE != pSt->is_inited){printf("the stuck is not initialed yet\n");return FALSE;}if (TRUE == st_is_empty(pSt)){printf("the stuck is empty!\n");return FALSE;}*pOutput = pSt->phead->pnext; //ptoppSt->phead->pnext = (*pOutput)->pnext;pSt->len--;return TRUE; }
5.10 清空棧函數 BOOL st_clear (STPERSON * pSt)
注意這里是清空棧,? 并不是銷毀棧, 所以并不會釋放棧本身的內存和棧成員的內存, 只是釋放棧里所有有效節點的內存.
有人問, 直接讓phead的尾部指針指向pbuttom 就ok了?
的確, 這個棧就成為1個空棧, 但是那些存放有效數據的節點很容易就找回來, 造成內存泄露
所以我在這個函數里會釋放掉這些存放有效數據節點的內存.
BOOL st_clear(STPERSON * pSt){if (TRUE != pSt->is_inited){printf("the stuck is not initialed yet\n");return FALSE;}PERSON_ST * pnode = pSt->phead;PERSON_ST * pAfter = pnode->pnext;//free(pnode); do not free phead;while (pSt->pbuttom != pAfter){ //do not free the pbuttompnode=pAfter;printf("free pnode which id is %d\n",pnode->id);pAfter=pnode->pnext;free(pnode);}//free(pSt); do not free the stuckpSt->phead->pnext = pSt->pbuttom;pSt->len=0;return TRUE; }
5.11 清空棧函數 BOOL st_free (STPERSON * pSt)
這個函數跟上面的很類似, 只不過這個函數還會釋放棧本身的內存.
釋放棧本身內存之前, 要釋放棧里面指針成員的內存 ,? 我相信會更加安全:
代碼如下:
這樣我在頭文件里聲明的函數都寫完了.? 下面會測試一下
5.12 寫1個測試的小程序.
當然這個測試程序會引用上面的頭文件, 會新建1個棧, 還會嘗試出棧, 入棧動作.
代碼如下:
int stuck_1(){PERSON_ST * pnode = person_st_new(1,"Jasonabc1234567890111111110");person_st_print(pnode);free(pnode);STPERSON * pst1 = st_create();st_push(pst1,person_st_new(1, "Jason"));st_push(pst1,person_st_new(2, "Cindy"));st_push(pst1,person_st_new(3, "Gateman"));st_push(pst1,person_st_new(4, "Fiana"));st_print(pst1);printf("top twice\n\n");st_pop(pst1, &pnode);person_st_print(pnode);free(pnode);st_pop(pst1, &pnode);person_st_print(pnode);free(pnode);printf("top done\n\n");st_print(pst1);st_clear(pst1);st_free(pst1);printf("stuck_1 done\n");return 0; }輸出:
6. 棧的一些實際應用
6.1 函數調用
??????? 假如代碼中定義了1個函數 f(),?? f()調用了函數g(),? g()里面又調用了函數k(),
??????? 那么調用時首先會放f()壓入棧執行,? 同時會將f()里面的變量及對應地址壓入棧.???
??????? 當f()里面調用g()時. 再將g()壓入棧執行,
??????? 當g() 里面再調用k() 時, 會再將k()壓入棧執行.
??????? 當k() 執行完時, 里面的變零和相關地址會釋放, 就會把k()的相關信息移除出棧, 相當于出棧.
??????? 出棧同時返回地址,? 那么g()就知道 k()執行完成, 那么g()繼續執行
??????? g()執行完時, 也會出棧.... f()繼續執行
??????? 大概就是這個道理.
6.2 表達式求值
6.3 內存分配
6.4 緩沖處理
6.5 走迷宮
????????
總結
以上是生活随笔為你收集整理的数据结构 线性存储 -- 栈 讲解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 线性结构--离散存储 链表讲解
- 下一篇: 数据结构 - 队列简介 及 1个简单的