1. 程式人生 > >java資料結構與演算法之棧(Stack)設計與實現

java資料結構與演算法之棧(Stack)設計與實現

關聯文章:

  本篇是java資料結構與演算法的第4篇,從本篇開始我們將來了解棧的設計與實現,以下是本篇的相關知識點:

棧的抽象資料型別

  棧是一種用於儲存資料的簡單資料結構,有點類似連結串列或者順序表(統稱線性表),棧與線性表的最大區別是資料的存取的操作,我們可以這樣認為棧(Stack)是一種特殊的線性表,其插入和刪除操作只允許線上性表的一端進行,一般而言,把允許操作的一端稱為棧頂(Top),不可操作的一端稱為棧底(Bottom),同時把插入元素的操作稱為入棧(Push),刪除元素的操作稱為出棧(Pop)。若棧中沒有任何元素,則稱為空棧,棧的結構如下圖:

  由圖我們可看成棧只能從棧頂存取元素,同時先進入的元素反而是後出,而棧頂永遠指向棧內最頂部的元素。到此可以給出棧的正式定義:棧(Stack)是一種有序特殊的線性表,只能在表的一端(稱為棧頂,top,總是指向棧頂元素)執行插入和刪除操作,最後插入的元素將第一個被刪除,因此棧也稱為後進先出(Last In First Out,LIFO)或先進後出(First In Last Out FILO)的線性表。棧的基本操作建立棧,判空,入棧,出棧,獲取棧頂元素等,注意棧不支援對指定位置進行刪除,插入,其介面Stack宣告如下:

package com.zejian.structures.Stack;

/**
* Created by zejian on 2016/11/27.
* Blog : http://blog.csdn.net/javazejian/article/details/53362993 [原文地址,請尊重原創]
* 棧介面抽象資料型別
*/
public interface Stack<T> {

   /**
    * 棧是否為空
    * @return
    */
   boolean isEmpty();

   /**
    * data元素入棧
    * @param data
    */
void push(T data); /** * 返回棧頂元素,未出棧 * @return */ T peek(); /** * 出棧,返回棧頂元素,同時從棧中移除該元素 * @return */ T pop(); }

順序棧的設計與實現

  順序棧,顧名思義就是採用順序表實現的的棧,順序棧的內部以順序表為基礎,實現對元素的存取操作,當然我們還可以採用內部陣列實現順序棧,在這裡我們使用內部資料組來實現棧,至於以順序表作為基礎的棧實現,將以原始碼提供。這裡先宣告一個順序棧其程式碼如下,實現Stack和Serializable介面:

/**
 * Created by zejian on 2016/11/27.
 * Blog : http://blog.csdn.net/javazejian/article/details/53362993 [原文地址,請尊重原創]
 * 順序棧的實現
 */
public class SeqStack<T> implements Stack<T>,Serializable {

    private static final long serialVersionUID = -5413303117698554397L;

    /**
     * 棧頂指標,-1代表空棧
     */
    private int top=-1;

    /**
     * 容量大小預設為10
     */
    private int capacity=10;

    /**
     * 存放元素的陣列
     */
    private T[] array;

    private int size;

    public SeqStack(int capacity){
        array = (T[]) new Object[capacity];
    }

    public SeqStack(){
        array= (T[]) new Object[this.capacity];
    }
    //.......省略其他程式碼
}

其獲取棧頂元素值的peek操作過程如下圖(未刪除只獲取值):

程式碼如下:

/**
  * 獲取棧頂元素的值,不刪除
  * @return
  */
 @Override
 public T peek() {
     if(isEmpty())
         new EmptyStackException();
     return array[top];
 }

從棧新增元素的過程如下(更新棧頂top指向):

程式碼如下:

/**
 * 新增元素,從棧頂(陣列尾部)插入
 * 容量不足時,需要擴容
 * @param data
 */
@Override
public void push(T data) {
    //判斷容量是否充足
    if(array.length==size)
        ensureCapacity(size*2+1);//擴容

    //從棧頂新增元素
    array[++top]=data;
    }

棧彈出棧頂元素的過程如下(刪除並獲取值):

程式碼如下:

/**
  * 從棧頂(順序表尾部)刪除
  * @return
  */
 @Override
 public T pop() {
     if(isEmpty())
         new EmptyStackException();
     size--;
     return array[top--];
 }

到此,順序棧的主要操作已實現完,是不是發現很簡單,確實如此,棧的主要操作就這樣,當然我們也可以通過前一篇介紹的MyArrayList作為基礎來實現順序棧,這個也比較簡單,後面也會提供帶程式碼,這裡就不過多囉嗦了。下面給出順序棧的整體實現程式碼:

package com.zejian.structures.Stack;

import java.io.Serializable;
import java.util.EmptyStackException;

/**
 * Created by zejian on 2016/11/27.
 * Blog : http://blog.csdn.net/javazejian/article/details/53362993 [原文地址,請尊重原創]
 * 順序棧的實現
 */
public class SeqStack<T> implements Stack<T>,Serializable {

    private static final long serialVersionUID = -5413303117698554397L;

    /**
     * 棧頂指標,-1代表空棧
     */
    private int top=-1;

    /**
     * 容量大小預設為10
     */
    private int capacity=10;

    /**
     * 存放元素的陣列
     */
    private T[] array;

    private int size;

    public SeqStack(int capacity){
        array = (T[]) new Object[capacity];
    }

    public SeqStack(){
        array= (T[]) new Object[this.capacity];
    }

    public  int size(){
        return size;
    }


    @Override
    public boolean isEmpty() {
        return this.top==-1;
    }

    /**
     * 新增元素,從棧頂(陣列尾部)插入
     * @param data
     */
    @Override
    public void push(T data) {
        //判斷容量是否充足
        if(array.length==size)
            ensureCapacity(size*2+1);//擴容

        //從棧頂新增元素
        array[++top]=data;

        size++;
    }

    /**
     * 獲取棧頂元素的值,不刪除
     * @return
     */
    @Override
    public T peek() {
        if(isEmpty())
            new EmptyStackException();
        return array[top];
    }

    /**
     * 從棧頂(順序表尾部)刪除
     * @return
     */
    @Override
    public T pop() {
        if(isEmpty())
            new EmptyStackException();
        size--;
        return array[top--];
    }

    /**
     * 擴容的方法
     * @param capacity
     */
    public void ensureCapacity(int capacity) {
        //如果需要拓展的容量比現在陣列的容量還小,則無需擴容
        if (capacity<size)
            return;

        T[] old = array;
        array = (T[]) new Object[capacity];
        //複製元素
        for (int i=0; i<size ; i++)
            array[i]=old[i];
    }

    public static void main(String[] args){
        SeqStack<String> s=new SeqStack<>();
        s.push("A");
        s.push("B");
        s.push("C");
        System.out.println("size->"+s.size());
        int l=s.size();//size 在減少,必須先記錄
        for (int i=0;i<l;i++){
            System.out.println("s.pop->"+s.pop());
        }

        System.out.println("s.peek->"+s.peek());
    }
}

鏈式棧的設計與實現

  瞭解完順序棧,我們接著來看看鏈式棧,所謂的鏈式棧(Linked Stack),就是採用鏈式儲存結構的棧,由於我們操作的是棧頂一端,因此這裡採用單鏈表(不帶頭結點)作為基礎,直接實現棧的新增,獲取,刪除等主要操作即可。其操作過程如下圖:

從圖可以看出,無論是插入還是刪除直接操作的是連結串列頭部也就是棧頂元素,因此我們只需要使用不帶頭結點的單鏈表即可。程式碼實現如下,比較簡單,不過多分析了:

package com.zejian.structures.Stack;

import com.zejian.structures.LinkedList.singleLinked.Node;

import java.io.Serializable;

/**
 * Created by zejian on 2016/11/27.
 * Blog : http://blog.csdn.net/javazejian/article/details/53362993 [原文地址,請尊重原創]
 * 棧的鏈式實現
 */
public class LinkedStack<T> implements Stack<T> ,Serializable{

    private static final long serialVersionUID = 1911829302658328353L;

    private Node<T> top;

    private int size;

    public LinkedStack(){
        this.top=new Node<>();
    }

    public int size(){
        return size;
    }


    @Override
    public boolean isEmpty() {
        return top==null || top.data==null;
    }

    @Override
    public void push(T data) {
        if (data==null){
            throw new StackException("data can\'t be null");
        }
        if(this.top==null){//呼叫pop()後top可能為null
            this.top=new Node<>(data);
        }else if(this.top.data==null){
            this.top.data=data;
        }else {
           Node<T> p=new Node<>(data,this.top);
            top=p;//更新棧頂
        }
        size++;
    }

    @Override
    public T peek()  {
        if(isEmpty()){
            throw new EmptyStackException("Stack empty");
        }

        return top.data;
    }

    @Override
    public T pop() {
        if(isEmpty()){
            throw new EmptyStackException("Stack empty");
        }

        T data=top.data;
        top=top.next;
        size--;
        return data;
    }
    //測試
    public static void main(String[] args){
        LinkedStack<String> sl=new LinkedStack<>();
        sl.push("A");
        sl.push("B");
        sl.push("C");
        int length=sl.size();
        for (int i = 0; i < length; i++) {
            System.out.println("sl.pop->"+sl.pop());
        }
    }
}

最後我們來看看順序棧與鏈式棧中各個操作的演算法複雜度(時間和空間)對比,順序棧複雜度如下:

操作 時間複雜度
SeqStack空間複雜度(用於N次push) O(n)
push()時間複雜度 O(1)
pop()時間複雜度 O(1)
peek()時間複雜度 O(1)
isEmpty()時間複雜度 O(1)

鏈式棧複雜度如下:

操作 時間複雜度
SeqStack空間複雜度建立(用於N次push) O(n)
push()時間複雜度 O(1)
pop()時間複雜度 O(1)
peek()時間複雜度 O(1)
isEmpty()時間複雜度 O(1)

由此可知棧的主要操作都可以在常數時間內完成,這主要是因為棧只對一端進行操作,而且操作的只是棧頂元素。

棧的應用

棧是一種很重要的資料結構,在計算機中有著很廣泛的應用,如下一些操作都應用到了棧。

  • 符號匹配
  • 中綴表示式轉換為字尾表示式
  • 計算字尾表示式
  • 實現函式的巢狀呼叫
  • HTML和XML檔案中的標籤匹配
  • 網頁瀏覽器中已訪問頁面的歷史記錄

接下來我們分別對符合匹配,中綴表示式轉換為字尾表示式進行簡單的分析,以加深我們對棧的理解。

  • 符號匹配
    在編寫程式的過程中,我們經常會遇到諸如圓括號“()”與花括號“{}”,這些符號都必須是左右匹配的,這就是我們所說的符合匹配型別,當然符合不僅需要個數相等,而且需要先左後右的依次出現,否則就不符合匹配規則,如“)(”,明顯是錯誤的匹配,而“()”才是正確的匹配。有時候符合如括號還會嵌套出現,如“9-(5+(5+1))”,而巢狀的匹配原則是一個右括號與其前面最近的一個括號匹配,事實上編譯器幫我檢查語法錯誤是也是執行一樣的匹配原理,而這一系列操作都需要藉助棧來完成,接下來我們使用棧來實現括號”()”是否匹配的檢測。
    判斷原則如下(str=”((5-3)*8-2)”):

    • a.設定str是一個表示式字串,從左到右依次對字串str中的每個字元char進行語法檢測,如果char是,左括號則入棧,如果char是右括號則出棧(有一對匹配就可以去匹配一個左括號,因此可以出棧),若此時出棧的字元char為左括號,則說明這一對括號匹配正常,如果此時棧為空或者出棧字元不為左括號,則表示缺少與char匹配的左括號,即目前不完整。
    • b.重複執行a操作,直到str檢測結束,如果此時棧為空,則全部括號匹配,如果棧中還有左括號,是說明缺少右括號。

    整個檢測演算法的執行流程如下圖:

    接著我們用棧作為儲存容器通過程式碼來實現這個過程,程式碼比較簡單,如下:

    package com.zejian.structures.Stack;
    
    /**
    * Created by zejian on 2016/11/27.
    * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創]
    * 表示式檢測
    */
    public class CheckExpression {
    
      public static String isValid(String expstr)
      {
          //建立棧
          LinkedStack<String> stack = new LinkedStack<>();
    
          int i=0;
          while(i<expstr.length())
          {
              char ch=expstr.charAt(i);
              i++;
              switch(ch)
              {
                  case '(': stack.push(ch+"");//左括號直接入棧
                      break;
                  case ')': if (stack.isEmpty() || !stack.pop().equals("(")) //遇見右括號左括號直接出棧
                      return "(";
              }
          }
          //最後檢測是否為空,為空則檢測通過
          if(stack.isEmpty())
              return "check pass!";
          else
              return "check exception!";
      }
    
      public static void main(String args[])
      {
          String expstr="((5-3)*8-2)";
          System.out.println(expstr+"  "+isValid(expstr));
      }
    }
  • 中綴表示式轉換為字尾表示式
    我們先來了解一下什麼是中綴表示式,平常所見到的計算表示式都算是中綴表示式,如以下的表示式:

    //1+3*(9-2)+9 --->中綴表示式(跟日常見到的表示式沒啥區別)

    瞭解中綴表示式後來看看其定義:將運算子寫在兩個運算元中間的表示式稱為中綴表示式。在中綴表示式中,運算子擁有不同的優先順序,同時也可以使用圓括號改變運算次序,由於這兩點的存在,使用的中綴表示式的運算規則比較複雜,求值的過程不能從左往右依次計算,當然這也是相對計算機而言罷了,畢竟我們日常生活的計算使用的還是中綴表示式。既然計算機感覺複雜,那麼我們就需要把中綴表示式轉化成計算機容易計算而且不復雜的表示式,這就是字尾表示式了,在後綴表示式中,運算子是沒有優先順序的,整個計算都是遵守從左往右的次序依次計算的,如下我們將中綴表示式轉為字尾表示式:

    //1+3*(9-2)+9        轉化前的中綴表示式
    //1 3 9 2 - * + 9 +  轉化後的字尾表示式

    中綴轉字尾的轉換過程需要用到棧,這裡我們假設棧A用於協助轉換,並使用陣列B用於存放轉化後的字尾表示式具體過程如下:
    1)如果遇到運算元,我們就直接將其放入陣列B中。
    2)如果遇到運算子,則我們將其放入到棧A中,遇到左括號時我們也將其放入棧A中。
    3)如果遇到一個右括號,則將棧元素彈出,將彈出的運算子輸出並存入陣列B中直到遇到左括號為止。注意,左括號只彈出並不存入陣列。
    4)如果遇到任何其他的操作符,如(“+”, “*”,“(”)等,從棧中彈出元素存入陣列B直到遇到發現更低優先順序的元素(或者棧為空)為止。彈出完這些元素後,才將遇到的操作符壓入到棧中。有一點需要注意,只有在遇到” ) “的情況下我們才彈出” ( “,其他情況我們都不會彈出” ( “。
    5)如果我們讀到了輸入的末尾,則將棧中所有元素依次彈出存入到陣列B中。
    6)到此中綴表示式轉化為字尾表示式完成,陣列儲存的元素順序就代表轉化後的字尾表示式。
    執行圖示過程如下:

    簡單分析一下流程,當遇到運算元時(規則1),直接存入陣列B中,當i=1(規則2)時,此時運算子為+,直接入棧,當i=3(規則2)再遇到運算子*,由於棧內的運算子+優先順序比*低,因此直接入棧,當i=4時,遇到運算子’(‘,直接入棧,當i=6時,遇運算子-,直接入棧,當i=8時(規則3),遇’)’,-和’(‘直接出棧,其中運算子-存入字尾陣列B中,當i=9時(規則5),由於*優先順序比+高,而+與+平級,因此和+出棧,存入陣列B,而後面的+再入棧,當i=10(規則5),結束,+直接出棧存入陣列B,此時陣列B的元素順序即為1 3 9 2 - * + 9 +,這就是中綴轉字尾的過程。
    接著轉成字尾後,我們來看看計算機如何利用字尾表示式進行結果運算,通過前面的分析可知,字尾表示式是沒有括號的,而且計算過程是按照從左到右依次進行的,因此在後綴表達的求值過程中,當遇到運算子時,只需要取前兩個運算元直接進行計算即可,而當遇到運算元時不能立即進行求值計算,此時必須先把運算元儲存等待獲取到運算子時再進行計算,如果存在多個運算元,其運算次序是後出現的運算元先進行運算,也就是後進先運算,因此後綴表示式的計算過程我們也需要藉助棧來完成,該棧用於存放運算元,字尾表示式的計算過程及其圖解如下:

    藉助棧的程式計算過程:

    簡單分析說明一下:
    1)如果ch是數字,先將其轉換為整數再入棧
    2)如果是運算子,將兩個操作數出棧,計算結果再入棧
    3)重複1)和2)直到字尾表示式結束,最終棧內的元素即為計算的結果。
    整體整體呈現實現如下:

    package com.zejian.structures.Stack;
    
    /**
    * Created by zejian on 2016/11/28.
    * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創]
    * 中綴轉字尾,然後計算字尾表示式的值
    */
    public class CalculateExpression {
    
      /**
       * 中綴轉字尾
       * @param expstr 中綴表示式字串
       * @return
       */
      public static String toPostfix(String expstr)
      {
          //建立棧,用於儲存運算子
          SeqStack<String> stack = new SeqStack<>(expstr.length());
    
          String postfix="";//儲存字尾表示式的字串
          int i=0;
          while (i<expstr.length())
          {
              char ch=expstr.charAt(i);
              switch (ch)
              {
                  case '+':
                  case '-':
                      //當棧不為空或者棧頂元素不是左括號時,直接出棧,因此此時只有可能是*/+-四種運算子(根據規則4),否則入棧
                      while (!stack.isEmpty() && !stack.peek().equals("(")) {
                          postfix += stack.pop();
                      }
                      //入棧
                      stack.push(ch+"");
                      i++;
                      break;
                  case '*':
                  case '/':
                      //遇到運算子*/
                      while (!stack.isEmpty() && (stack.peek().equals("*") || stack.peek().equals("/"))) {
                          postfix += stack.pop();
                      }
                      stack.push(ch+"");
                      i++;
                      break;
                  case '(':
                      //左括號直接入棧
                      stack.push(ch+"");
                      i++;
                      break;
                  case ')':
                      //遇到右括號(規則3)
                      String out = stack.pop();
                      while (out!=null && !out.equals("("))
                      {
                          postfix += out;
                          out = stack.pop();
                      }
                      i++;
                      break;
                  default:
                      //運算元直接入棧
                      while (ch>='0' && ch<='9')
                      {
                          postfix += ch;
                          i++;
                          if (i<expstr.length())
                              ch=expstr.charAt(i);
                          else
                              ch='=';
                      }
                      //分隔符
                      postfix += " ";
                      break;
              }
          }
          //最後把所有運算子出棧(規則5)
          while (!stack.isEmpty())
              postfix += stack.pop();
          return postfix;
      }
    
      /**
       * 計算字尾表示式的值
       * @param postfix 傳入字尾表示式
       * @return
       */
      public static int calculatePostfixValue(String postfix)
      {
          //棧用於儲存運算元,協助運算
          LinkedStack<Integer> stack = new LinkedStack<>();
          int i=0, result=0;
          while (i<postfix.length())
          {
              char ch=postfix.charAt(i);
              if (ch>='0' && ch<='9')
              {
                  result=0;
                  while (ch!=' ')
                  {
                      //將整數字符轉為整數值ch=90
                      result = result*10 + Integer.parseInt(ch+"");
                      i++;
                      ch = postfix.charAt(i);
                  }
                  i++;
                  stack.push(result);//運算元入棧
              }
              else
              {  //ch 是運算子,出棧棧頂的前兩個元素
                  int y= stack.pop();
                  int x= stack.pop();
                  switch (ch)
                  {   //根據情況進行計算
                      case '+': result=x+y; break;
                      case '-': result=x-y; break;
                      case '*': result=x*y; break;
                      case '/': result=x/y; break;   //注意這裡並沒去判斷除數是否為0的情況
                  }
                  //將運算結果入棧
                  stack.push(result);
                  i++;
              }
          }
          //將最後的結果出棧並返回
          return stack.pop();
      }
      //測試
      public static void main(String args[])
      {
          String expstr="1+3*(9-2)+90";
          String postfix = toPostfix(expstr);
          System.out.println("中綴表示式->expstr=  "+expstr);
          System.out.println("字尾表示式->postfix= "+postfix);
          System.out.println("計算結果->value= "+calculatePostfixValue(postfix));
      }
    
    }

    以上便是利用轉實現中綴與字尾的轉換過程並且通過後綴計算機能及其簡單計算出字尾表示式的結果。ok~,到此我們對棧的分析就結束了,本來還想聊聊函式呼叫的問題,但感覺這個問題放在遞迴演算法更恰當,嗯,原始碼地址如下:
    github原始碼下載,歡迎star(含文章列表,持續更新)