Re:從零開始的DS生活 輕鬆和麪試官扯一個小時棧

引言:Re:從零開始的DS生活 輕鬆和麪試官扯一個小時棧,詳細介紹了棧的概念和性質,簡要的介紹了棧ADT並附兩種實現方式(鏈式、順序),列舉LeetCode第20題與嚴蔚敏老師棧和遞歸的講解加深對棧的應用,供讀者理解與學習,適合點贊+收藏。有什麼錯誤希望大家直接指出~
友情鏈接:
Re:從零開始的DS生活 輕鬆從0基礎寫出鏈表LRU算法Re:從零開始的DS生活 輕鬆從0基礎實現多種隊列Re:從零開始的DS生活 輕鬆從0基礎寫出Huffman樹與紅黑樹

導讀(有基礎的同學可以通過以下直接跳轉到感興趣的地方)

棧的基本概念和性質,棧ADT及其順序,鏈接實現;
         棧ADT
         棧的順序,鏈接實現
棧的應用
         括號匹配的檢驗-有效括號問題
         棧的經典應用-逆波蘭表達式法
         棧與遞歸
         JVM內存棧


棧的基本概念和性質,棧ADT及其順序,鏈接實現;

棧(Stack)又名後進先出表LIFO(Last In First Out),,它是一種運算受限的線性表。 其限制是僅允許在表的一端進行插入和刪除運算。這一端被稱爲棧頂,相對地,把另一端稱爲棧底(這句話是性質)。和彈夾的進出順序是一樣的。

進棧:入棧或壓棧,將新元素放到棧頂元素的上面,使之成爲新的棧頂元素。
出棧:退棧,將棧頂元素刪除掉,使得與其相鄰的元素成爲新的棧頂元素。


棧ADT

ADT Stack{
    數據對象: D={aijlai屬於ElemSet, i=1,2...n, n>=0}
    數據關係: R1={<ai-1,ai>/ai-1, ai屬於D, i=2,... n}
    約定an端爲棧頂, al端爲棧底。

    基本操作:
    InitStack(&S)    // 操作結果:構造一個空棧 S.
    DestroyStack(&S)    // 初始條件:棧S已存在    操作結果:棧S被銷燬
    ClearStack(&S)    // 初始條件:棧S已存在    操作結果:將S清爲空棧
    StackEmpty(S)    // 初始條件 :棧S已存在    操作結果:若棧S爲空棧,則返回TRUE,否則
    FALSE
    Stacklength(S)    // 初始條件:棧S已存在    操作結果:返回S的元素個數,即棧的長度
    GetTop(S, &e)    // 初始條件:棧S已存在且非空    操作結果:用e返回S的棧頂元素
    Push(&S, e)    // 初始條件:棧S已存在    操作結果:插入元素e爲新的棧頂元素
    Pop(&S, &e)    // 初始條件:棧S已存在且非空操作結果:刪除S的棧頂元素,並用e返回其值
    StackTraverse(S, visit()    //初始條件: 棧S已存在並非空    操作結果:從棧底到棧頂依次對S的每個數據元素調用函數visit(),一旦visit()失敗,則返回操作失敗。
}ADT Stack    出自《數據結構(C語言版)》嚴蔚敏、吳偉民著)

棧的順序,鏈接實現

棧的數組(順序)實現

/**
 * 棧的數組實現--java
 * 數組的長度是固定的,當棧空間不足時,將原數組數據複製到一個更長的數組中
 * 故入棧的時間複雜度爲O(N),出棧的時間複雜度依然爲O(1)
 *
 * @author macfmc
 * @date 2020/6/12-20:08
 */
public class MyArrayStack {
    /**
     * 容器
     */
    private Object[] stack;
    /**
     * 棧的默認大小
     */
    private static final int INIT_SIZE = 10;
    /**
     * 棧頂索引
     */
    private int index;

    /**
     * 初始化棧_默認構造方法
     */
    public MyArrayStack() {
        this.stack = new Object[INIT_SIZE];
        this.index = -1;
    }

    /**
     * 初始化棧,自定義長度
     */
    public MyArrayStack(int init_size) {
        if (init_size < 0) {
            throw new RuntimeException();
        }
        this.stack = new Object[init_size];
        this.index = -1;
    }

    /**
     * 判斷棧是否爲空
     *
     * @return
     */
    public boolean isEmpty() {
        return index == -1;
    }

    /**
     * 判斷是都棧滿
     *
     * @return
     */
    public boolean isFull() {
        return index >= stack.length - 1;
    }

    /**
     * 入棧
     *
     * @param obj
     */
    public synchronized void push(Object obj) {
        // 動態擴容
        if (isFull()) {
            Object[] temp = stack;
            // 創建一個二倍大小的數組
            stack = new Object[stack.length * 2];
            //
            System.arraycopy(temp, 0, stack, 0, temp.length);
        }
        stack[++index] = obj;
    }

    /**
     * 查看棧頂元素
     *
     * @return
     */
    public Object peek() {
        if (!isEmpty()) {
            return stack[index];
        }
        return null;
    }

    /**
     * 出棧
     *
     * @return
     */
    public synchronized Object pop() {
        if (!isEmpty()) {
            Object obj = peek();
            stack[index--] = null;
            return obj;
        }
        return null;
    }

    public static void main(String[] args) {
        MyArrayStack stack = new MyArrayStack();
        for (int i = 0; i < 100; i++) {
            stack.push("stack" + i);
        }
        for (int i = 0; i < 100; i++) {
            System.out.println(stack.pop());
        }
    }
}

棧的鏈表(鏈接)實現

/**
 * 棧的鏈表實現--java
 *
 * @author macfmc
 * @date 2020/6/12-20:08
 */
public class MyLinkedStack<T> {
    /**
     * 單鏈表
     *
     * @param <T>
     */
    private class Node<T> {
        //指向下一個節點
        Node next;
        //本節點存儲的元素
        T date;
    }

    /**
     * 棧中存儲的元素的數量
     */
    private int count;
    /**
     * 棧頂元素
     */
    private Node top;

    public MyLinkedStack() {
        count = 0;
        top = null;
    }

    /**
     * 判斷是都爲空
     *
     * @return true爲空
     */
    public boolean isEmpty() {
        return count == 0;
    }

    /**
     * 入棧
     *
     * @param element
     */
    public void push(T element) {
        Node<T> node = new Node<T>();//鏈表節點
        node.date = element;//鏈表節點數據
        node.next = top;//鏈表下一步節點
        top = node;//現在的棧頂
        count++;//棧數量
    }

    /**
     * 出棧
     *
     * @return
     */
    public T pop() {
        if (isEmpty()) {
            throw new RuntimeException();
        }
        T result = (T) top.date;
        top = top.next;
        count--;
        return result;
    }

    /**
     * 獲取棧頂
     *
     * @return
     */
    public T peek() {
        if (isEmpty()) {
            throw new RuntimeException();
        }
        return (T) top.date;
    }

    public static void main(String[] args) {
        MyLinkedStack<String> lind = new MyLinkedStack<String>();
        for (int i = 0; i < 10; i++) {
            lind.push("LindedStack" + i);
        }
        for (int i = 0; i < 10; i++) {
            System.out.println(lind.pop());
        }
    }
}

棧的應用

括號匹配的檢驗-有效括號問題

// https://leetcode-cn.com/problems/valid-parentheses/)
class Solution {
    public boolean isValid(String s) {
        Stack<Character> stack = new Stack<>();
        char[] chars = s.toCharArray();
        // 遍歷括號
        for (char aChar : chars) {
            if (stack.size() == 0) { // 初始化棧,沒有的話會報空棧異常EmptyStackException
                stack.push(aChar);
            } else if (isSym(stack.peek(), aChar)) { // 判斷棧頂和入棧元素是不是一對括號
                stack.pop();
            } else {
                stack.push(aChar);
            }
        }
        return stack.size() == 0;
    }
    
    private boolean isSym(char c1, char c2) {
        return (c1 == '(' && c2 == ')') || (c1 == '[' && c2 == ']') || (c1 == '{' && c2 == '}');
    }


    public boolean isValid2(String s) {
        Stack stack = new Stack();
        char[] chars = s.toCharArray();

        for (char c : chars)
            if (stack.size() == 0)
                stack.push(c);
            else if (isSym((char) stack.peek(), c))
                stack.pop();
            else
                stack.push(c);

        return stack.size() == 0;
    }

    private boolean isSym(char c1, char c2) {
        return (c1 == '(' && c2 == ')') || (c1 == '[' && c2 == ']') || (c1 == '{' && c2 == '}');
    }
}

棧的經典應用-逆波蘭表達式法

中綴表達式轉爲後綴表達式:

設置一個堆棧,初始時將堆棧頂設置爲#
順序讀入中綴表達式,到讀到的單詞爲數字時將其輸出,接着讀下一個單詞;
令x1 爲棧頂運算符變量,x2 爲掃描到的運算符變量,當順序從表達試中讀到的運算符時賦值給x2,然後比較x1 和 x2 的優先級,若x1 的優先級高於x2的優先級,將x1退棧並輸出,接着比較新的棧頂運算符x1,x2的優先級;若 x1的優先級低於x2的優先級,將x2 入棧;如果x1 = “(”且 x2 = “)”,將x1 退棧;若x1的優先級等於x2的優先級且x1 = “#”而x2=“#”時,算法結束

import java.util.ArrayList;
import java.util.Stack;

/**
 * @author macfmc
 * @date 2020/6/13-22:30
 */
public class ReversePolishNotation {

    /**
     * 測試的main方法
     */
    public static void main(String arg[]) {
        String s = "9+(3-1)*3+10/2";
        ArrayList postfix = transform(s);
        for (int i = 0, len = postfix.size(); i < len; i++) {
            System.out.println(postfix.get(i));
        }
        calculate(postfix);
    }

    /**
     * 將中綴表達式轉換成後綴表達式
     */
    public static ArrayList transform(String prefix) {
        System.out.println("transform");
        int i, len = prefix.length();// 用字符串保存前綴表達式
        prefix = prefix + '#';// 讓前綴表達式以'#'結尾
        Stack<Character> stack = new Stack<Character>();// 保存操作符的棧
        stack.push('#');// 首先讓'#'入棧
        ArrayList postfix = new ArrayList();//後綴數組集合

        // 保存後綴表達式的列表,可能是數字,也可能是操作符
        for (i = 0; i < len + 1; i++) {
            System.out.println(i + " " + prefix.charAt(i));
            if (Character.isDigit(prefix.charAt(i))) {// 當前字符是一個數字
                if (Character.isDigit(prefix.charAt(i + 1))) {// 當前字符的下一個字符也是數字(兩位數)
                    postfix.add(10 * (prefix.charAt(i) - '0') + (prefix.charAt(i + 1) - '0'));
                    i++;// 序號加1
                } else {// 當前字符的下一個字符不是數字(一位數)
                    postfix.add((prefix.charAt(i) - '0'));
                }
            } else {// 當前字符是一個操作符
                switch (prefix.charAt(i)) {
                    case '(':// 如果是開括號
                        stack.push(prefix.charAt(i));// 開括號只放入到棧中,不放入到後綴表達式中
                        break;
                    case ')':// 如果是閉括號
                        while (stack.peek() != '(') {
                            postfix.add(stack.pop());// 閉括號不入棧,將前一個不是“)”的操作符入棧
                        }
                        stack.pop();// '('出棧
                        break;
                    default:// 默認情況下:+ - * /
                        while (stack.peek() != '#' && compare(stack.peek(), prefix.charAt(i))) {// 比較運算符之間的優先級
                            postfix.add(stack.pop());// 不斷彈棧,直到當前的操作符的優先級高於棧頂操作符
                        }
                        if (prefix.charAt(i) != '#') {// 如果當前的操作符不是'#'(結束符),那麼入操作符棧
                            stack.push(prefix.charAt(i));// 最後的標識符'#'是不入棧的
                        }
                        break;
                }
            }
        }
        return postfix;
    }

    /**
     * 比較運算符之間的優先級
     * 如果是peek優先級高於cur,返回true,默認都是peek優先級要低
     */
    public static boolean compare(char peek, char cur) {
        if (peek == '*'
                && (cur == '+' || cur == '-' || cur == '/' || cur == '*')) {// 如果cur是'(',那麼cur的優先級高,如果是')',是在上面處理
            return true;
        } else if (peek == '/'
                && (cur == '+' || cur == '-' || cur == '*' || cur == '/')) {
            return true;
        } else if (peek == '+' && (cur == '+' || cur == '-')) {
            return true;
        } else if (peek == '-' && (cur == '+' || cur == '-')) {
            return true;
        } else if (cur == '#') {// 這個很特別,這裏說明到了中綴表達式的結尾,那麼就要彈出操作符棧中的所有操作符到後綴表達式中
            return true;// 當cur爲'#'時,cur的優先級算是最低的
        }
        return false;// 開括號是不用考慮的,它的優先級一定是最小的,cur一定是入棧
    }

    /**
     * 計算後綴表達式
     */
    public static double calculate(ArrayList postfix) {// 後綴表達式的運算順序就是操作符出現的先後順序
        System.out.println("calculate");
        int i, res = 0, size = postfix.size();
        Stack<Integer> stackNum = new Stack<Integer>();
        for (i = 0; i < size; i++) {
            if (postfix.get(i).getClass() == Integer.class) {// 判斷如果是操作數
                stackNum.push((Integer) postfix.get(i));//入棧
                System.out.println("push" + " " + (Integer) postfix.get(i));
            } else {// 如果是操作符
                System.out.println((Character) postfix.get(i));
                int a = stackNum.pop();// 出棧後一個操作數
                int b = stackNum.pop();// 出棧前一個操作數
                switch ((Character) postfix.get(i)) {
                    case '+':
                        res = b + a;
                        System.out.println("+ " + a + " " + b);
                        break;
                    case '-':
                        res = b - a;
                        System.out.println("- " + a + " " + b);
                        break;
                    case '*':
                        res = b * a;
                        System.out.println("* " + a + " " + b);
                        break;
                    case '/':
                        res = b / a;
                        System.out.println("/ " + a + " " + b);
                        break;
                }
                stackNum.push(res);//操作後的結果入棧
                System.out.println("push" + " " + res);
            }
        }
        res = stackNum.pop();//結果
        System.out.println("結果: " + " " + res);
        return res;
    }
}

棧與遞歸

棧:限定僅在表尾進行插入和刪除操作的線性表。
遞歸:直接調用自己或通過一系列的調用語句間接地調用自己的函數,稱做遞歸函數。

函數的遞歸調用和普通函數調用是一樣的。當程序執行到某個函數時,將這個函數進行入棧操作,在入棧之前,通常需要完成三件事。
1、將所有的實參、返回地址等信息傳遞給被調函數保存。
2、爲被調函數的局部變量分配存儲區。
3、將控制轉移到北調函數入口。

當一個函數完成之後會進行出棧操作,出棧之前同樣要完成三件事。
1、保存被調函數的計算結果。
2、釋放被調函數的數據區。
3、依照被調函數保存的返回地址將控制轉移到調用函數。

上述操作必須通過棧來實現,即將整個程序的運行空間安排在一個棧中。每當運行一個函數時,就在棧頂分配空間,函數退出後,釋放這塊空間。所以當前運行的函數一定在棧頂。

(注:摘自嚴蔚敏等人的數據結構c語言版)

JVM內存棧

JVM:在java編譯器和os平臺之間的虛擬處理器,JVM會遞歸調用爲例,一個棧幀包括局部變量表、操作數棧、棧數據區

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章