數據結構(三)--棧


 

數據結構(三)--棧

通常程序開發中內存管理是非常重要的,而內存主要分為占內存和堆內存。那么棧和堆內存有什么區別呢?希望在這篇文章里能帶你找到答案!

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. 小結

通過這里復習數據結構中棧的內容,感覺重新理解了很多計算機實現的底層知識,雖然不知道的更多,但是面對計算機心中又多了一層認知!

文中代碼地址:https://github.com/xiaoyouPrince/DataStructure


免責聲明!

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



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