資料結構之棧

weixin_33850890發表於2018-07-29

一:概述

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

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

public interface Stack<T> {
    // 棧是否為空
    boolean isEmpty();

    // 入棧
    void push(T data);

    // 取出棧頂元素,不出棧
    T peek();

    // 出棧
    T pop();
}

二:順序棧的設計與實現
    順序棧,顧名思義就是採用順序表實現的的棧,順序棧的內部以順序表為基礎,實現對元素的存取操作,當然我們還可以採用內部陣列實現順序棧,在這裡我們使用內部資料組來實現棧:

public class SeqStack<T> implements Stack<T>{
    //棧頂索引,-1代表空棧
    private int top = -1;

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

    //存放資料的陣列
    private T[] arr;

    //棧的實際使用量
    private int size;

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

    //是否是空棧的判斷依據是top是否為-1;如果大於等於0,說明棧中有元素
    @Override
    public boolean isEmpty() {
        return top == -1;
    }

    //獲取棧頂元素,本例基於陣列來實現棧,所以棧頂元素,就是陣列的最後一個元素
    @Override
    public T peek() {
        //如果是空棧就丟擲異常
        if(isEmpty()) {
            new EmptyStackException();
        }
        //返回陣列的最後一個元素
        return arr[top];
    }

    //獲取棧頂元素並出棧
    @Override
    public T pop() {
        //國際慣例,空棧丟擲異常
        if(isEmpty()) {
            new EmptyStackException();
        }
        size --;
        //需要注意的是,這裡不會將陣列的最後一個元素置空,因為top的值已經修改
        //下次再push的時候只是把原來的陣列元素的值進行了修改,因為一旦pop後,
        //top的值減小了,原來的陣列的元素永遠訪問不到,完全沒有必要置空.
        return arr[top--];
    }

    //入棧
    @Override
    public void push(T data) {
        //如果棧已滿,那麼擴容
        if(size == arr.length) {
            ensureCapacity(size*2+1);
        }
        //先將棧頂指標+1,然後將元素放入top + 1那個位置
        arr[++top] = data;
        size++;
    }

    //擴容的思路很簡單,建立一個新的陣列,然後將原來陣列的資料複製到新陣列
    private void ensureCapacity(int capacity) {
        //以防萬一
        if(capacity > size) {
            return;
        }
        
        T[] old = arr;
        arr = (T[]) new Object[capacity];
        for(int i = 0 ; i < old.length ; i++) {
            arr[i] = old[i];
        }
    }
}

    測試程式碼:

public static void main(String[] args) {
    SeqStack<String> stack = new SeqStack<>();
    stack.push("A");
    stack.push("B");
    stack.push("C");
    stack.push("D");

    int l = stack.size;
    System.out.println("size : " + l + " , top index : " + stack.top + 
        " , top : " + stack.peek());
        
    for(int i = 0 ; i < l; i ++) {
        stack.pop();
    }
    //此處丟擲異常
    System.out.println("size : " + l + " , top : " + stack.peek());
}

    測試結果:

push A , size : 1
push B , size : 2
push C , size : 3
push D , size : 4
size : 4 , top index : 3 , top : D
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1
    at teststack.SeqStack.peek(SeqStack.java:58)
    at teststack.SeqStack.main(SeqStack.java:42)

    從上面可以看出,順序棧的邏輯很簡單,就是操作底層的陣列而已,所有的入棧和出棧操作,都是往陣列新增和刪除元素,然後改變top指標的位置。
    以上就是順序棧的實現,簡單。

三:鏈式棧的設計與實現

    瞭解完順序棧,我們接著來看看鏈式棧,所謂的鏈式棧(Linked Stack),就是採用鏈式儲存結構的棧,由於我們操作的是棧頂一端,因此這裡採用單連結串列作為基礎,直接實現棧的新增,獲取,刪除等主要操作即可。其操作過程如下圖:
4864291-9c609661d22823ef.jpeg
5.005.jpeg
    從上圖可以看出,鏈式棧的設計就是頭節點就是棧頂元素,所有的入棧和出棧,都是操作頭結點,下面用程式碼來實現:
public class LinkStack<T> implements Stack<T>{
    //棧頂元素
    private Node<T> top;

    private int size;

    public LinkStack() {
        top = new Node<T>();
    }

    //如果棧頂元素為空,或者棧頂元素的資料為空,一律視為空棧
    @Override
    public boolean isEmpty() {
        return top == null || top.data == null;
    }

    @Override
    public void push(T data) {
        if (data == null) {
            try {
                throw new Exception("data can\'t be null");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        //如果棧頂節點是空的話,那麼建立一個新的節點當做棧頂節點
        if (top == null) {
            top = new Node<>(data);
        //如果棧頂節點的資料為空的話,那麼將資料賦值給該節點
        } else if (top.data == null) {
            top.data = data;
        //如果棧頂節點和資料都不為空,說明不是空棧,這時候需要建立一個
        //新的節點,將它賦值給棧頂節點,同時將它的next指向當前的棧頂節點
        } else {
            Node<T> p = new Node<T>(data, this.top);
            top = p;
        }
        size++;
    }

    // 獲取棧頂元素,我們一直持有棧頂節點的引用,直接返回好了
    @Override
    public T peek() {
        if (isEmpty()) {
            new EmptyStackException();
        }
        return top.data;
    }

    //出棧,就是將棧頂節點的下一個節點當做新的棧頂節點,
    //實質上就是移除連結串列中的頭結點,很簡單
    @Override
    public T pop() {
        if (isEmpty()) {
            new EmptyStackException();
        }

        T data = top.data;
        top = top.next;
        size--;
        return data;
    }

    //節點
    class Node<T> {
        public T data;
        public Node<T> next;

        public Node() {
        }

        public Node(T data) {
            this.data = data;
        }

        public Node(T data, Node<T> next) {
            this.data = data;
            this.next = next;
        }
}

    測試程式碼:

public static void main(String[] args) {
    LinkStack<String> sl = new LinkStack<>();
    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() + " , size : " + sl.size);
    }
    sl.push("T");
    System.out.println("sl.pop->" + sl.peek() + " , size : " + sl.size);
}

    測試結果:

sl.pop->C , size : 2
sl.pop->B , size : 1
sl.pop->A , size : 0
sl.pop->T , size : 1

    綜上可知,棧的實現還是很簡單的,下面講下棧的應用場景。
四:棧的應用
    棧是一種很重要的資料結構,在計算機中有著很廣泛的應用,如下一些操作都應用到了棧:
    符號匹配
    中綴表示式轉換為字尾表示式
    計算字尾表示式
    實現函式的巢狀呼叫
    HTML和XML檔案中的標籤匹配
    網頁瀏覽器中已訪問頁面的歷史記錄

    符號匹配:
    在編寫程式的過程中,我們經常會遇到諸如圓括號“()”與花括號“{}”,這些符號都必須是左右匹配的,這就是我們所說的符合匹配型別,當然符合不僅需要個數相等,而且需要先左後右的依次出現,否則就不符合匹配規則,如“)(”,明顯是錯誤的匹配,而“()”才是正確的匹配。有時候符合如括號還會巢狀出現,如“9-(5+(5+1))”,而巢狀的匹配原則是一個右括號與其前面最近的一個括號匹配,事實上編譯器幫我檢查語法錯誤是也是執行一樣的匹配原理,而這一系列操作都需要藉助棧來完成,接下來我們使用棧來實現括號”()”是否匹配的檢測。
    判斷原則如下(str=”((5-3)*8-2)”):
    a.設定str是一個表示式字串,從左到右依次對字串str中的每個字元char進行語法檢測,如果char是,左括號則入棧,如果char是右括號則出棧(有一對匹配就可以去匹配一個左括號,因此可以出棧),若此時出棧的字元char為左括號,則說明這一對括號匹配正常,如果此時棧為空或者出棧字元不為左括號,則表示缺少與char匹配的左括號,即目前不完整。
    b.重複執行a操作,直到str檢測結束,如果此時棧為空,則全部括號匹配,如果棧中還有左括號,是說明缺少右括號。
    接著我們用棧作為儲存容器通過程式碼來實現這個過程

public class CheckExpression {
 
    public static String isValid(String expstr){
        //建立棧
        LinkedStack<String> stack = new LinkedStack<>();
        int i = 0 ;
        //遍歷字串,挨個取出每個字元
        while(i < expstr.lenght()){
            char c = expstr.charAt(i);
            i++;
            switch (ch){
                //如果該字元是左括號,那麼放入棧中
                case '(' :
                    stack.push(ch + "");
                    break;
                //如果是右括號,那麼將左括號出棧
                case ")" :
                     if(stack.isEmpty() || !stack.pop().equals("(")) {
                         return "FAILURE";
                     }
             }    
        }
        //經過這麼一輪迴圈,如果棧為空,說明每個左括號都有
        //右括號和他匹配,這時候校驗通過,否則不通過
        if(stack.isEmpty()) {
            return "PASS";
        }else {
            return "FAILURE";
        }
    }
}

    測試程式碼:

String expstr="((5-3)*8-2)";
System.out.println(expstr + " check  " + check(expstr));        
String expstr1="((5-3)*8-2";
System.out.println(expstr1 + " check  " + check(expstr1));

    測試結果:

((5-3)*8-2) check  PASS
((5-3)*8-2 check  FAILURE

摘自:https://blog.csdn.net/javazejian/article/details/53362993

相關文章