手把手帶你利用棧來實現一個簡易版本的計算器


什么是棧

我們來看一下百度百科中對棧的定義:棧(stack)又名堆棧,它是一種運算受限的線性表。限定僅在表尾進行插入和刪除操作的線性表。這一端被稱為棧頂,相對地,把另一端稱為棧底。

向一個棧插入新元素又稱作進棧、入棧或壓棧,它是把新元素放到棧頂元素的上面,使之成為新的棧頂元素;從一個棧刪除元素又稱作出棧或退棧,它是把棧頂元素刪除掉,使其相鄰的元素成為新的棧頂元素。

棧的實現

棧和數組,鏈表一樣,也是一種線性的數據結構。在有些編程語言中並沒有棧這種數據結構,但是要實現一個棧卻也簡單,通過數組或者鏈表都可以來實現一個棧。

通過數組實現

Java 中,棧(Stack 類)就是通過數組來實現的,下面我們就自己利用數組來實現一個簡單的棧:

package com.lonely.wolf.note.stack;

import java.util.Arrays;

/**
 * 基於數組來實現自定義棧
 */
public class MyStackByArray<E> {
    public static void main(String[] args) {
        MyStackByArray stack = new MyStackByArray();
        stack.push(1);
        System.out.println("stack有效元素個數:" + stack.size);//輸出 1
        System.out.println("查看棧頂元素:" + stack.peek());//輸出 1
        System.out.println("棧是否為空:" + stack.isEmpty());//輸出 false
        System.out.println("彈出棧頂元素:" + stack.pop());// 輸出 1
        System.out.println("棧是否為空:" + stack.isEmpty());//輸出 true
        stack.push(2);
        stack.push(3);
        stack.push(4);
        System.out.println("stack有效元素個數:" + stack.size);//輸出 3
        System.out.println("彈出棧頂元素:" + stack.pop()); //輸出 4
    }

    private Object[] element;//存儲元素的數組
    private int size;//棧內有效元素
    private int DEFAULT_SIZE = 2;//默認數組大小

    public MyStackByArray() {
        element = new Object[DEFAULT_SIZE];
    }

    /**
     * 判斷是否為空,注意不能直接用數組的長度
     * @return
     */
    public boolean isEmpty(){
        return size == 0;
    }

    /**
     * 查看棧頂元素
     * @return
     */
    public synchronized E peek() {
        if (size == 0){
            return null;
        }
        return (E)element[size-1];
    }


    /**
     * 查看並彈出棧頂元素
     * @return
     */
    public E pop() {
        if (size == 0){
            return null;
        }
        E obj = peek();
        size--;//利用 size 屬性省略元素的移除
        return obj;
    }

    /**
     * 壓棧
     * @param item
     * @return
     */
    public E push(E item) {
        ensureCapacityAndGrow();
        element[size++] = item;
        return item;
    }

    /**
     * 擴容
     */
    private void ensureCapacityAndGrow() {
        int len = element.length;
        if (size + 1 > len){//擴容
            element = Arrays.copyOf(element,len * 2);
        }
    }
}

通過隊列實現

除了通過數組,其實通過鏈表等其他數據結構也能實現,實現棧最關鍵就是要注意棧的后進先出特性。

leetcode 中的第 225 是利用兩個隊列來實現一個棧,具體要求是這樣的:

請你僅使⽤兩個隊列實現⼀個后⼊先出(LIFO)的棧,並⽀持普通棧的全部四種操作(push、top、pop 和 empty),你只能使用隊列的基本操作(也就是 push to backpeek/pop from frontsizeis empty 這些操作)。

在解決這個問題之前,我們先要明白隊列的特性,隊列最主要的一個特性就是先進先出,所以我們不管先把元素放到哪個隊列,最后出來的元素依然是先進先出,似乎實現不了棧的后進先出特性。

實現思路

為了滿足棧的特性,我們就必須要讓先入隊的元素最后出隊,所以我們可以這么做:

使用一個隊列作為主隊列,每次出棧都從主隊列(mainQqueue)獲取元素,另外一個隊列作為輔助隊列(secondQueue),僅僅用來存儲元素,每次存儲元素的時候先存入 secondQueue,然后將 mainQueue 內的元素依次放入 secondQueue,最后再將兩個隊列互換,這樣每次出隊的時候只需要從 mainQueue 依次獲取元素即可。

下面我們一起來畫一個流程圖幫助理解這個過程:

  1. 放入元素 1

先將元素放入 secondQqueue,這時候 mainQueue 為空,所以不需要移動元素,直接交換兩個隊列即可,所以最終得到的依然是 mainQueue 內有一個元素 1,而 secondQueue 中沒有元素:

  1. 繼續放入元素 2

這時候元素 2 依然先放入 secondQqueue,然后此時發現 mainQueue 里面有元素,一次取出來,入隊 secondQueue,然后繼續將兩個隊列交換:

  1. 繼續放入元素 3

繼續放入元素 3 也是一樣的道理,依然先放入 secondQqueue,然后將 mainQueue 中的兩個元素一次放回到 secondQueue,最后再將兩個隊列進行交換:

大部分算法都是這樣,關鍵是理清思路,思路理清了剩下的就是代碼翻譯的過程,這個過程只要做好好邊界控制及其他注意事項,相對來說就比較容易實現了:

package com.lonely.wolf.note.stack;

import java.util.LinkedList;
import java.util.Queue;

public class MyStackByTwoQueue {
    public static void main(String[] args) {
        MyStackByTwoQueue queue = new MyStackByTwoQueue();
        queue.push(1);
        queue.push(2);
        System.out.println(queue.pop());
    }

    private Queue<Integer> mainQueue = new LinkedList<>();
    private Queue<Integer> secondQueue = new LinkedList<>();


    public void push(int e){
        secondQueue.add(e);
        if (!mainQueue.isEmpty()){
            secondQueue.add(mainQueue.poll());
        }
        //交換連個 queue,此時新加入的元素 e 即為 mainQueue 的頭部元素
        Queue temp = mainQueue;
        mainQueue = secondQueue;
        secondQueue = temp;
    }


    public int top(){
        return mainQueue.peek();
    }

    public int pop(){
        return mainQueue.poll();
    }

    public boolean empty() {
        return mainQueue.isEmpty();
    }
}

棧的經典應用場景

因為棧是一種操作受限的數據結構,所以其使用場景也比較有限,下面我們列舉幾個比較經典的應用場景。

瀏覽器前進后退

瀏覽器的前進后退就是典型的先進后出,因為有前進后退,所以我們需要定義兩個棧:forwardStackbackStack。當我們從頁面 1 訪問到頁面 4,那么我們就把訪問過的頁面依次壓入 backStack

后退的時候直接從 backStack 出棧就可以了,當 backStack 為空就說明不能繼續后退了;而且當從 backStack 出棧的同時又將頁面壓入 forwardStack,這樣前進的時候就可以從 forwardStack 依次出棧:

括號配對

利用棧來驗證一個字符串中的括號是否完全配對會非常簡單,因為右括號一定是和最靠近自己的一個左括號配對的,這就滿足了后進先出的特性。所以我們可以直接遍歷字符串,遇到左括號就入棧,遇到右括號就看看是否和當前棧頂的括號匹配,如果不匹配則不合法,如果匹配則將棧頂元素出棧,並繼續循環,直到循環完整個字符串之后,如果棧為空就說明括號恰好全部配對,當前字符串是有效的。

leetcode 20 題

leetcode 中的第 20 題就是一道括號配對的題,題目是這樣的:

給定一個只包括 '(',')','{','}','[',']' 的字符串 s ,判斷字符串是否有效。有效字符串需滿足:左括號必須用相同類型的右括號閉合;左括號必須以正確的順序閉合。

知道了上面的解題思路,代碼實現起來還是比較簡單的:

public static boolean isValid(String s){
       if (null == s || s.length() == 0){
           return false;
       }

       Stack<Character> stack = new Stack<>();
       Map<Character,Character> map = new HashMap<>();
       map.put(')','(');
       map.put(']','[');
       map.put('}','{');
       for (int i=0;i<s.length();i++){
           char c = s.charAt(i);
           if (c == '(' || c == '[' || c == '{'){
               stack.push(c);//左括號入棧
           }else{
               if (stack.isEmpty() || map.get(c) != stack.peek()){
                   return false;
               }
               stack.pop();//配對成功則出棧
           }
       }
       return stack.isEmpty();
   }

表達式求值

算法表達式也是棧的一個經典應用場景,為了方便講解,我們假設表達式中只有 +、-、*、/ 四種符號,然后我們要對表示式 18-12/3+5*4 進行求解應該如何做呢?

其實這道題也可以利用兩個棧來實現,其中一個用來保存操作數,稱之為操作數棧,另一個棧用來保存運算符,稱之為運算符棧。具體思路如下:

  1. 遍歷表達式,當遇到操作數,將其壓入操作數棧。
  2. 遇到運算符時,如果運算符棧為空,則直接將其壓入運算符棧。
  3. 如果運算符棧不為空,那就與運算符棧頂元素進行比較:如果當前運算符優先級比棧頂運算符高,則繼續將其壓入運算符棧,如果當前運算符優先級比棧頂運算符低或者相等,則從操作數符棧頂取兩個元素,從棧頂取出運算符進行運算,並將運算結果壓入操作數棧。
  4. 繼續將當前運算符與運算符棧頂元素比較。
  5. 繼續按照以上步驟進行遍歷,當遍歷結束之后,則將當前兩個棧內元素取出來進行運算即可得到最終結果。

leetcode 227 題

題目:給你一個有效的字符串表達式 s,請你實現一個基本計算器來計算並返回它的值,整數除法僅保留整數部分,s 由整數和算符 ('+', '-', '*', '/') 組成,中間由一些空格隔開。

這道題目可以利用我們上面講述的思路進行解決,不過除此之外,在審題時我們應該還需要考慮兩個點:

  1. 表達式中有空格,我們需要將空格處理掉
  2. 操作數可能有多位,也就是說我們需要將操作數先計算出來。

使用兩個棧求解

這道題如果我們按照上面的思路,使用兩個棧來做的話,雖然代碼有點繁瑣,但是思路還是清晰的,具體代碼如下:

public static int calculateByTwoStack(String s){
       if (null == s || s.length() == 0){
           return 0;
       }
       Stack<Integer> numStack = new Stack<>();//操作數棧
       Stack<Character> operatorStack = new Stack<>();//運算符棧

       int num = 0;
       for (int i = 0;i<s.length();i++){
           char c = s.charAt(i);
           if (Character.isDigit(c)){//數字
               num = num * 10 + (c - '0');
               if (i == s.length() - 1 || s.charAt(i+1) == ' '){//如果是最后一位或者下一位是空格,需要將數字入棧
                   numStack.push(num);
                   num = 0;
               }
               continue;
           }
           if (c == '+' || c == '-' || c == '*' || c == '/'){
               if (s.charAt(i-1) != ' '){//如果前一位不是空格,那需要將整數入棧
                   numStack.push(num);
                   num = 0;
               }
               if (c == '*' || c == '/'){//如果是乘除法,那么需要將當前運算法棧內的乘除法先計算出來
                   while (!operatorStack.isEmpty() && (operatorStack.peek() == '*' || operatorStack.peek() == '/')){
                       numStack.push(sum(numStack,operatorStack.pop()));//將計算出的結果再次入棧
                   }
               } else {//如果是加減法,優先級已經是最低,那么當前運算符棧內所有數據都需要被計算掉
                   while (!operatorStack.isEmpty()){
                       numStack.push(sum(numStack,operatorStack.pop()));
                   }
               }
               operatorStack.push(c);
           }
       }
       //最后開始遍歷:兩個操作數,一個運算符進行計算
       while (!numStack.isEmpty() && !operatorStack.isEmpty()){
           numStack.push(sum(numStack,operatorStack.pop()));//計算結果再次入棧
       }
       return numStack.pop();//最后一定剩余一個結果入棧了
   }

   private static int sum(Stack<Integer> numStack,char operator){
       int num1 = numStack.pop();
       int num2 = numStack.pop();
       int result = 0;
       switch (operator){
           case '+':
               result = num2 + num1;
               break;
           case '-':
               result = num2 - num1;
               break;
           case '*':
               result = num2 * num1;
               break;
           case '/':
               result = num2 / num1;
               break;
           default:
       }
       return result;
   }

上面題目中我們也可以先使用正則把表達式中所有空格去除,這樣的話也可以省去空格的判斷

使用一個棧求解

其實這道題因為只有加減乘除法,所以我們其實可以取巧,只利用一個棧也可以實現。

因為乘除法一定優先於加減法,所以可以先把乘除法計算出來后將得到的結果放回表達式中,最后得到的整個表達式就是加減法運算,具體做法為:

遍歷字符串 s,並用變量 preOperator 記錄每個數字之前的運算符,對於表達式中的第一個數字,我們可以默認其前一個運算符為加號。每次遍歷到數字末尾時(即:讀到一個運算符,或者讀到一個空格,或者遍歷到字符串末尾),根據 preOperator 來決定計算方式:

  • 加號:將數字直接壓入棧內。
  • 減號:將對應的負數壓入棧內。
  • 乘/除號:計算數字與棧頂元素,並將棧頂元素替換為計算結果。

這樣最終只需要將棧內的所有數據相加就可以得到結果,具體代碼示例如下:

public static int calculateOneStack(String s){
    if (null == s || s.length() == 0){
        return 0;
    }
    Stack<Integer> stack = new Stack<>();
    char preOperator = '+';//默認前一個操作符是加號
    int num = 0;
    for (int i = 0;i<s.length();i++){
        char c = s.charAt(i);
        if (Character.isDigit(c)){
            num = num * 10 + (c - '0');
        }
        if ((!Character.isDigit(c) && c != ' ') || i == s.length()-1){//判斷數字處理是否已經結束,如果結束需要將數字入棧或者計算結果入棧
            switch (preOperator){
                case '+':
                    stack.push(num);//加法則直接將數字入棧
                    break;
                case '-':
                    stack.push(-num);//減法則將負數入棧
                    break;
                case '*':
                    stack.push(stack.pop() * num);//乘法則需要計算結果入棧
                    break;
                case '/':
                    stack.push(stack.pop() / num);//除法則需要計算結果入棧
                    break;
                default:
            }
            preOperator = c;
            num = 0;
        }
    }
    int result = 0;
    while (!stack.isEmpty()){//最后將棧內所有數據相加即可得到結果
        result+=stack.pop();
    }
    return result;
}

函數調用

除了上面的三個經典場景,其實我們平常的方法調用也是用的棧來實現的,我們每次調用一個新的方法就會定義一個臨時變量,並將其作為一個棧幀進行入棧,當方法執行完畢之后,就會將當前方法對應的棧幀進行出棧。

總結

本文主要講述了棧這種操作受限的數據結構,並通過數組實現了一個簡易版的棧,同時講述了如何通過兩個隊列實現一個棧。最后我們列舉了棧的四大經典應用場景:括號配對,表達式求值,瀏覽器前進后退,函數調用等,而其中括號配對和表達式求值這兩種場景又會衍生出不同的算法題。


免責聲明!

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



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