數據結構(三)--棧
通常程序開發中內存管理是非常重要的,而內存主要分為占內存和堆內存。那么棧和堆內存有什么區別呢?希望在這篇文章里能帶你找到答案!
1. 棧和堆的引入
在一個簡單的程序中我們定義和聲明幾個基本類型的變量、結構體和數組,先來直觀看一下棧和堆的不同:
- 靜態變量 和 局部變量是以壓棧出棧的方式分配內存的,系統會在一個代碼段中分配和回收局部變量,實際上每個代碼段、函數都是一個或多個嵌套的棧,我們不需要手動管理棧區內存。
- 動態內存是一種堆排序的方式分配內存的,內存分配好后便不會自動回收,需要程序員手動回收。否則就會造成內存泄漏,內存越用越少。
簡單了解了一下程序中內存棧與堆的區別,下面就正式開始講數據結構中的棧。
(注意:數據結構棧、內存棧、函數調用棧三者在含義上略有不同,但是其核心思想和理念是相同的)
2. 棧的定義
棧是一種“先進后出”的一種數據結構,有壓棧出棧兩種操作方式。如下圖:
3. 棧的分類
棧主要分為兩類:
- 靜態棧
- 動態棧
【靜態棧】
靜態棧的核心是數組,類似於一個連續內存的數組,我們只能操作其棧頂元素。
【動態棧】
靜態棧的核心是數組,類似於一個連續內存的數組,我們只能操作其棧頂節點。
4. 棧的算法
棧的算法主要是壓棧和出棧兩種操作的算法,下面我就用代碼來實現一個簡單的棧。
首先要明白以下思路:
- 棧操作的是一個一個節點
- 棧本身也是一種存儲的數據結構
- 棧有
初始化
、壓棧
、出棧
、判空
、遍歷
、清空
等主要方法
4.1 棧的頭文件定義
頭文件定義如下:
typedef struct Node{ // 節點
int data;
struct Node *pNext;
}*PNODE,NODE;
typedef struct Stack{ // 棧
PNODE pTop;
PNODE pBottom;
}STACK,*PSTACK;
/**棧的初始化*/
void init(PSTACK);
/**壓棧*/
void push(PSTACK,int);
/**出棧*/
int pop(PSTACK , int *);
/**遍歷打印棧*/
void traverse(PSTACK);
/**是否為空棧*/
int isEmpty(PSTACK);
/**清空棧*/
void clearStack(PSTACK);
有了頭文件定義,基本就確定了棧的使用結構和使用方式。下面是在主函數中對這個棧的創建和使用。
int main(void){
STACK stack; // 聲明一個棧
init(&stack); // 初始化
// 壓棧
push(&stack, 10);
push(&stack, 20);
push(&stack, 30);
push(&stack, 40);
push(&stack, 50);
traverse(&stack); // 遍歷打印棧
int val;
int isPopSuccess = pop(&stack,&val);
if (isPopSuccess) {
printf("pop 的值為 %d\n",val);
}
traverse(&stack);
clearStack(&stack); // 清空棧
traverse(&stack);
return 0;
}
4.2 棧的初始化
思路:
拿到棧聲明的指針,開辟一塊內存空間給棧頂棧底,此時是一個空棧,棧頂棧底指向同一塊內存,且棧底棧頂以外不再指向其他節點。
/**棧的初始化*/
void init(PSTACK pS){
pS->pTop = (PNODE)malloc(sizeof(NODE));
if (pS->pTop == NULL) {
printf("內存分配失敗退出");
return;
}else
{
pS->pBottom = pS->pTop;
pS->pTop->pNext = NULL;
}
}
4.3 壓棧 和 出棧
思路:
壓棧是把新的節點放入棧頂,且每次壓棧操作只能將新的節點放到棧的頂部。
出棧需判斷是否原本為空棧,存在出棧失敗的情況,把棧頂指向棧頂元素的下一個元素,並釋放原來棧頂元素空間。
/**
壓棧
@param pS 執行壓棧的棧指針
@param val 被壓棧的值
*/
void push(PSTACK pS,int val){
// 創建新節點,放到棧頂
PNODE pNew = (PNODE)malloc(sizeof(NODE));
pNew->data = val;
pNew->pNext = pS->pTop;
pS->pTop = pNew; // 棧頂指針指向新元素
}
/**
出棧
@param pS 執行出棧的棧地址
@param val 出棧值的地址
@return 是否出棧成功
*/
int pop(PSTACK pS , int *val){
if (isEmpty(pS)) {
printf(" 空棧 ,出棧失敗");
return 0;
}else
{
PNODE p = pS->pTop;
pS->pTop = p->pNext;
if (val != NULL) {
*val = p->data;
}
free(p); // 釋放原來top內存
p = NULL;
return 1;
}
}
/**是否為空棧*/
int isEmpty(PSTACK pS)
{
if (pS->pTop == pS->pBottom) {
return 1;
}else
{
return 0;
}
}
4.4 棧的清空 和 遍歷
當一個代碼段執行完成之后,實際上就是這個棧所有分配的空間都被回收,棧隨之被清空!
思路:
棧清空,實際就是需要循環執行出棧操作。
棧遍歷,實際就是棧元素從棧頂一個個遍歷到棧底,可以打印棧中元素的值
/**清空棧*/
void clearStack(PSTACK pS){
if (isEmpty(pS)) {
return;
}else{
PNODE p = pS->pTop;
PNODE q = NULL;
while (p!=pS->pBottom) {
q = p->pNext;
free(p); // 釋放原棧頂元素空間
p = q;
}
pS->pTop = pS->pBottom;
}
// 偷懶的做法
// while (!isEmpty(pS)) {
// pop(pS, NULL);
// }
}
/**遍歷打印棧*/
void traverse(PSTACK pS){
// 只要不是空棧,就一直輸出
PNODE p = pS->pTop;
while (p != pS->pBottom) {
printf("%d ",p->data);
p = p->pNext; // 把top的下一個節點付給top,繼續遍歷
}
printf("\n");
}
5. 棧的應用
棧結構固有的先進后出的特性,使它成為在程序設計中非常有用的工具,這里列舉幾個典型的例子。
5.1 數制轉換
十進制數 N 和其他 d 進制數的轉換是計算機實現計算的基本問題,其解決方法有很多種,其中一個簡單的方法基於如下原理:
N = (N div d) * d + N mod d
(其中div是整除運算,mod 為求余運算)
例如:1348(10進制) == 2504(8進制)運算過程如下:
N | N div 8 | N mod 8 |
---|---|---|
1348 | 168 | 4 |
168 | 21 | 0 |
21 | 2 | 5 |
2 | 0 | 2 |
需求:輸入一個任意非負十進制整數,打印輸出其對應的八進制整數
思路:由於上述計算過程是從低到高位順序產生八進制數的各個數位,而打印輸出,一般來說應從高位到低位進行,恰好和計算過程相反。因此可利用棧先進后出特性,將計算過程中得到的八進制數各位順序進棧,再按出棧序列打印輸出既為與輸入對應的八進制數。
void conversion(void){
// 創建棧
STACK S;
init(&S);
// 用戶輸入十進制數
scanf("%d",&N);
// 放入棧中
while (N) {
push(&S, N % 8);
N = N / 8;
}
// 打印出來
printf("對應八進制數字為:");
int a;
while (!isEmpty(&S)) {
pop(&S, &a);
printf("%d",a);
}
printf("\n");
}
思考 用數組實現貌似更簡單,為什么不用數組?
從算法上分析不難看出,棧的引入簡化了程序設計的問題,划分了不同的關注層次,使思考范圍縮小了。而使用數組不僅掩蓋了問題的本質,還要分散精力去思路數組下標增減等細節問題。
這也是早期面向對象編程的一種思想,要把對應的功能划分關注層次,在邏輯的實現上面更加專注問題的本質。
5.2 括號匹配的檢驗
編程語言中基本都允許使用 (),[],{}
這幾種括號,假設現在讓使用兩種,一段完整代碼中其須成對匹配,檢驗括號是否匹配的方法可用"期待的緊迫程度"這個概念來描述。
當計算機接受了第一個括號后,它期待着與其匹配的第八個括號出現,然而等來的確實第二個括號,此時第一個括號[
只能暫時靠邊,而迫切等待與第二個括號匹配的第七個括號)
出現,類似地,等來的是第三個括號[
,其期待的匹配程度比第二個更加急迫,則第二個括號也只能靠邊,讓位於第三個括號,顯然第二個括號的期待急迫性高於第一個括號,在接受了第四個括號之后,第三個括號的期待得到滿足,消解之后,第二個括號的期待匹配變成最緊迫的任務了·····,以此類推。
可見此處理過程與棧的特點相吻合,由此,在算法中設置一個棧,每讀入一個括號,若是右括號則使至於棧頂的最緊迫的期待得以消解,若是不合法的情況(左括號),則作為一個新的更緊迫的期待壓入棧中,自然使原來所有未消解的期待的緊迫性都降了一級。另外在算法開始和結束的時候,棧都應該是空的。
算法實現:
/**
檢測括號(本實例用數字代替括號)
[ ] --> 1 , 2
( ) --> 3 , 4
*/
void checkBracelet(void)
{
// 創建棧
STACK S;
init(&S);
// 用戶輸入括號
int N;
printf("請輸入對應的括號(end結束):\n");
scanf("%d",&N);
if (isEmpty(&S)) {
push(&S, N);
printf("第一個括號輸入\n");
traverse(&S); // 打印此時棧內容
}
while (!isEmpty(&S)) {
// 用戶輸入括號
int N;
printf("請輸入對應的括號(0結束):\n");
scanf("%d",&N);
if (N == 0) {
break; // 用戶輸入0直接退出
}
// 判斷當前棧頂是否符合標准,
if (S.pTop->data == N) {
printf("消除一對\n");
pop(&S, NULL);
traverse(&S); // 打印此時棧內容
}else
{
printf("未消除\n");
push(&S, N);
traverse(&S); // 打印此時棧內容
}
}
}
這里的實例我列舉了兩個,實際上還有很多。比如 行編輯程序、迷宮求解、表達式求值等。這里我就先不做列舉了。
6. 小結
通過這里復習數據結構中棧的內容,感覺重新理解了很多計算機實現的底層知識,雖然不知道的更多,但是面對計算機心中又多了一層認知!