資料結構與演算法分析學習筆記(四) 棧

北冥有隻魚發表於2021-09-25
軟體世界是現實世界的投影。

引言

軟體世界的一些概念大多都不是憑空創造,這些概念大多都是從現實世界抽象而來。就像我們本文所討論的"棧"一樣,日常生活中也有這樣的模型,比如疊成一摞的碗,最先放置的反而放在最下面,像下面這樣:

這與排隊相反。排隊對應的一種資料結構,我們稱之為佇列。事實上棧這種資料結構在日常使用中還是非常常用的,就比如說撤銷操作,這是軟體內普遍內建的操作,就像我們在日常編碼中做了一些修改,想回到上一步,我們一般就是使用ctrl + z。軟體按時間記錄下了你的操作或者是輸入,像下面這樣:

時間最靠前的反而在最下面,這樣我們就能吃後悔藥了。在比如我們寫程式碼中的括號匹配,如果少寫了一個,編譯器是如何知道的呢? 我們來分析一下這個問題:

編號為括號的進入順序, 每個右括號將與最近遇到的那個未匹配的左括號相匹配,即"後進的先出, 或者先進的後出"。

體會一下上面的話,每個右括號將與最近遇到的那個未匹配的左括號相匹配。 我們用p記錄左括號的最新位置,用q指向當前新進入的括號,將p與q相比對,如果相匹配,則刪除左括號,p後退一位。如果匹配到最後還是有剩餘的左括號那麼這個括號就是不匹配的。

再比如我們常用的函式互相呼叫,Java總是從一個main函式開始,如果我們的程式碼是main方法呼叫a方法,a方法裡面又呼叫c方法,c方法裡面又呼叫d方法,那麼顯而易見的是d方法執行完畢之後再執行c方法,c方法執行完才執行a方法。這種執行模型也很像棧:

仔細體會上面的用例,這些問題的共同的特點就是處理過程中都具有"後進先出"的特性,這是一類典型的問題,我們將其抽象出來也就是資料結構中的"棧"模型。下面開始講解一些棧的概念和一些基本應用。

概述

下面是棧的一種示意圖:

你可以將示意圖中的長方體理解為一個竹筒,最上面開口,最下面封閉,裡面放的是寫有編號的小球,放入順序依據小球順序,從小到大。如果我們想從這個竹筒中取球,可以發現一種規律: 先放進去的只能後拿出來; 反之,後放進去的小球能夠先拿出來,也就是先進後出,這是棧的典型特點。

當我們把上面結構中的小球抽象為資料結構中的結點時,因為結點間的關係是線性的,因此它也屬於線性表,又由於它的操作只能在同一端進行,因此它是一個運算受限的線性表,我們將這種型別線性表稱之為棧。

現在我們給出棧的定義: 棧是一種特殊的線性表,它所有的插入和刪除都限制在表的同一端進行。棧中允許允許進行插入、刪除操作的一段叫棧頂(top),另一端則叫棧底(bottom)。當棧中沒有元素時,稱之為空棧

棧中的插入運算我們通常稱之為壓棧、進棧或入棧(PUSH),棧的刪除運算通常被稱為彈棧(push)出棧(pop).
第一個進棧的元素在棧底,最後一個進棧的元素在棧頂,第一個出棧的元素為棧頂元素,最後一個出棧的元素為棧底元素。

可能會有人覺得為什麼要引入棧,或者直接用陣列和連結串列實現棧不就行了嗎? 為什麼要專門劃出一種資料結構來? 原則上我們後面講的佇列、樹、圖也都是用陣列和連結串列來實現,每一種資料結構都對應了一類專門的資料處理問題,像棧專門處理"先進後出",同時也簡化了程式設計思路,縮小了我們的思考範圍。就算是用陣列來實現棧,我們也得讓外部呼叫者無需擔心下標越界以及增減的問題,那麼這就是一種新的陣列,只不過我們習慣上用棧來稱呼而已。

同線性表基本類似,棧的基本運算如下:

  • 棧初始化操作: 建立一個空棧表
  • 棧判空: 判斷棧是否為空
  • 取棧頂元素: 取棧頂元素的值
  • 壓棧: 在棧S中插入元素e, 使其成為新的棧頂元素
  • 彈棧: 刪除棧S的元素。

現在的許多高階語言,比如說Java、C#都內建了對棧資料結構的封裝,我們可以不用關注棧的實現細節,而直接使用對棧的push和Pop的操作。同時也可以借鑑設計思路。

用陣列實現的棧我們稱之為順序棧,用連結串列實現的棧我們一般就稱之為鏈棧。關於順序棧其實也可以再進行細分,一種是一開始就給定棧的大小,入棧超出大小的時候我們可以選擇將棧底的上一個元素稱為棧底元素。第二種我們稱之為動態棧,棧的大小是動態的,也就是動態擴容,超出棧的上限的時候,自動擴容,棧的大小基本取決於記憶體的大小。

順序棧

我們嘗試用陣列來實現以下順序棧,其實順序棧相關的設計也可以參考Java中的相關的設計思路,我們來看下Java中的Stack:

操作也與我們上面討論的一致。那peek和pop有什麼區別:

  • peek 取棧頂的元素不刪除
  • pop 取棧頂的元素刪除並返回棧頂元素
    Java中Stack的設計思路:
  • 棧判空 看陣列的實際容量
  • push操作 將元素新增到陣列的末尾
  • peek 操作 返回陣列最後一個元素
  • pop 操作 返回陣列的最後一個元素, 同時棧頂元素向後移動一個位置
    我們也可以讓我們建的順序棧繼承我們在資料結構與演算法分析(三) 線性表SequenceList,這樣擴容操作我們就不用在重寫一遍,只用實現棧的方法就可以了。 關於方法的設計我們可以完全借鑑Java中的Stack:

    public class SequenceStack extends SequenceList {
      public SequenceStack() {
      }
      public int push(int data){
          add(data);
          return data;
      }
      /**
       * 返回陣列的最後一個元素, 同時棧頂元素向後移動一個位置
       * @return
       */
      public int pop(){
          if (size() == 0){
              // 丟擲異常
          }
          int data = peek();
          remove(size() - 1);
          return data;
      }
    
      public int peek(){
          if (size() == 0){
              // 丟擲異常
          }
          return  get(size()  - 1);
      }
    
      public boolean empty(){
          return size() == 0;
      }
    }

    鏈式棧

    採用鏈式儲存方式的棧我們稱之為鏈棧或鏈式棧。鏈棧有一個個結點構成,每個結點包含資料資料域和指標域兩部分。在鏈式棧中,利用每一個結點的儲存域儲存棧中的每一個元素,利用指標域來表示元素之間的關係。像下面這樣:

    public class LinkedStack {
      private Node top; //指向棧頂元素
      private class Node{
          private int data;
          private Node next;
    
          public Node(int data , Node next){
              this.data = data;
              this.next = next;
          }
          public int getData() {
              return data;
          }
    
          public Node getNext() {
              return next;
          }
      }
    
      public LinkedStack() {
      }
      /**
       * @param data
       * @return
       */
      public int push(int data){
          Node node = new Node(data,top);
          top = node;
          return data;
      }
    
      public int pop(){
          return peek();
      }
    
      public int peek(){
          int data = top.data;
          top = top.next;
          return data;
      }
      public boolean empty() {
          return top == null;
      }
    }

    棧的應用舉例

數制轉換

十進位制數到R進位制數的轉換,我們一般採用的方法是十進位制數和進製取餘,最高位往往是最後才出現,剛好符合後入先出的棧。例如1024 轉8進位制:

最高位最後出現, 但棧剛好是後進先出。進位制轉換示例:

 /**
     * 進位制轉換
     * @param input
     * @param decimal
     */
    private static void binaryConversion(int input, int decimal) {
        if(decimal == 0){
            return;
        }
        Stack<Integer> stack = new Stack<>();
        while (input != 0){
            stack.push(input % decimal);
            input = input /  decimal;
        }
        String result =  "";
        while (!stack.empty()){
            result = result + stack.pop();
        }
        System.out.println(result);
    }

參考資料

  • 《 資料結構與演算法分析新視角》 周幸妮 任智源 馬彥卓 編著 中國工信出版集團

相關文章