棧及其基本應(yīng)用

本文首發(fā)于 LOGI'S BLOG,由作者轉(zhuǎn)載。

是一種操作受限的線性表,只支持從一端插入和刪除。后進(jìn)先出是它的最大特點(diǎn)。棧既可用數(shù)組也可用鏈表實(shí)現(xiàn),前者叫順序棧,后者叫鏈棧,無(wú)論哪種,出入棧操作的時(shí)間復(fù)雜度都是 O(1)。從功能上說,數(shù)組和鏈表都可代替棧,但特定的數(shù)據(jù)結(jié)構(gòu)是對(duì)特定場(chǎng)景的抽象,盡管數(shù)組和鏈表操作靈活,但它們暴露了太多接口,使用時(shí)不可控因素增多,自然也就更容易出錯(cuò)。當(dāng)某個(gè)數(shù)據(jù)集合只涉及在一端插入和刪除數(shù)據(jù),并且滿足后進(jìn)先出、先進(jìn)后出的特性時(shí),就應(yīng)首選棧這種數(shù)據(jù)結(jié)構(gòu)。

現(xiàn)實(shí)生活中的棧

我們平時(shí)放盤子的時(shí)候,都是從下往上一個(gè)一個(gè)放;取的時(shí)候從上往下一個(gè)一個(gè)取,不能從中間任意抽出,這種先進(jìn)后出的結(jié)構(gòu)便是棧。

順序棧的實(shí)現(xiàn)

class ArrayBasedStack {
    Object[] items;
    int capacity;
    int size;

    public ArrayBasedStack(int capacity) {
        this.items = new Object[capacity];
        this.capacity = capacity;
        this.size = 0;
    }

    // 入棧
    public void push(Object item) {
        // 動(dòng)態(tài)擴(kuò)容
        if (this.capacity == this.size) {
            this.capacity = this.capacity << 1;
            Object[] newItems = new Object[this.capacity];
            System.arraycopy(items, 0, newItems, 0, this.size);
            /*
            for (int i = 0; i < this.size; i++) {
                newItems[i] = this.items[i];
            }
            */
            this.items = newItems;
        }
        this.items[this.size++] = item;
    }

    // 出棧
    public Object pop() {
        return this.size == 0 ? null : this.items[--this.size];
    }
}

鏈?zhǔn)綏5膶?shí)現(xiàn)

class SinglyLinkedListBasedStack {
    SinglyLinkedListNode head;
    int size;

    public void push(Object item) {
        SinglyLinkedListNode current = new SinglyLinkedListNode(item);
        current.next = this.head.next;
        this.head.next = current;
        this.size++;
    }

    public Object pop() {
        SinglyLinkedListNode current = this.head.next;
        if (current != null) {
            this.head.next = this.head.next.next;
        }
        this.size--;
        return current;
    }
}

class SinglyLinkedListNode {
    SinglyLinkedListNode next;
    Object item;

    public SinglyLinkedListNode(Object item) {
        this.item = item;
    }
}

出入棧的時(shí)空復(fù)雜度分析

對(duì)于鏈?zhǔn)綏?,push 操作實(shí)際上是鏈表頭插,時(shí)間復(fù)雜度為 O(1)。pop 操作是刪除第一個(gè)有效節(jié)點(diǎn),時(shí)間復(fù)雜度同樣為 O(1)。兩個(gè)操作都未使用額外空間,因此空間復(fù)雜度為 O(1)。

對(duì)于順序棧,push 操作在鏈表滿時(shí)需要數(shù)據(jù)搬移,時(shí)間復(fù)雜度 O(n),空間復(fù)雜度 O(n),這是最壞情況。最好情況是數(shù)組未滿,直接將 item 放入 size 位置,時(shí)空復(fù)雜度都是 O(1)。pop 操作直接返回 size-1 位置,時(shí)空復(fù)雜度都是 O(1)。

我們發(fā)現(xiàn),順序棧的 push 操作,大多數(shù)情況下可在 O(1) 時(shí)間完成,只有少數(shù)情況需要 O(n) 時(shí)間,并且它們之間存在時(shí)間先后關(guān)系,具體是,每 n-1 次 O(1) 操作跟隨 1 次 O(n) 操作。在 算法的時(shí)空復(fù)雜度分析 這篇文章中,我們介紹過,遇到上述情況,在分析平均復(fù)雜度時(shí),應(yīng)使用攤還分析代替平均加權(quán),得到均攤復(fù)雜度,并且通常情況下,均攤復(fù)雜度等于最好情況復(fù)雜度。本例中,將 n 個(gè)數(shù)據(jù)搬移均攤到 n 次上,相當(dāng)于一次只需一次搬移,均攤復(fù)雜度為 O(1),這也更符合較長(zhǎng)時(shí)間的性能測(cè)量。

// 順序棧 push 操作的時(shí)間復(fù)雜度
// 第一行為次數(shù)
// 第二行為每次的時(shí)間復(fù)雜度
|-  n  -| 1 |- n-1 -| 1 |- n-1 -|
1,1,...,1,n,1,1,...,1,n,1,1,...,1,...

棧的常規(guī)應(yīng)用

處理函數(shù)調(diào)用

我們知道,操作系統(tǒng)給每個(gè)線程分配了一塊獨(dú)立的內(nèi)存空間,這塊內(nèi)存空間被組織成棧,用來(lái)存儲(chǔ)函數(shù)調(diào)用時(shí)的臨時(shí)變量。每進(jìn)入一個(gè)函數(shù),操作系統(tǒng)便將臨時(shí)變量作為棧幀入棧,當(dāng)調(diào)用結(jié)束返回后,再將該棧幀出棧。通過模擬以下代碼的執(zhí)行過程,可以得到函數(shù)調(diào)用棧的結(jié)構(gòu)。

int main() {
   int a = 1;
   int ret = 0;
   int res = 0;
   ret = add(3, 5);
   res = a + ret;
   printf("%d", res);
   return 0;
}

int add(int x, int y) {
   int sum = 0;
   sum = x + y;
   return sum;
}

下圖顯示了執(zhí)行到 add 函數(shù)時(shí),函數(shù)調(diào)用棧的棧幀分布情況。

|  ·  |-----
|  ·  |  |
|  ·  |
|sum=0| add
|  x=3|
|  y=5|  |
|  ·  |-----
|  ·  |  |
|  ·  |
|res=0| main
|ret=0|
|  a=1|  |
-----------

為什么函數(shù)調(diào)用要用 “棧” 來(lái)保存臨時(shí)變量呢?用其他數(shù)據(jù)結(jié)構(gòu)不行嗎?

其實(shí),我們不一定非要用棧來(lái)保存臨時(shí)變量,只不過如果這個(gè)函數(shù)調(diào)用符合后進(jìn)先出的特性,用棧這種數(shù)據(jù)結(jié)構(gòu)是最順理成章的選擇。

從調(diào)用函數(shù)進(jìn)入被調(diào)用函數(shù),對(duì)于數(shù)據(jù)來(lái)說,變化的是什么呢?是作用域。所以從根本上,只要能保證每進(jìn)入一個(gè)新的函數(shù),都是一個(gè)新的作用域就可以。而要實(shí)現(xiàn)它,用棧就非常方便。在進(jìn)入被調(diào)用函數(shù)的時(shí)候,分配一段棧空間給這個(gè)函數(shù)的變量,在函數(shù)結(jié)束的時(shí)候,將棧頂復(fù)位,正好回到調(diào)用函數(shù)的作用域內(nèi)。

瀏覽器的前進(jìn)后退

思路:

  1. 初始化兩個(gè)棧 X 和 Y,把首次瀏覽的頁(yè)面壓入 X
  2. 當(dāng)點(diǎn)擊后退時(shí),依次從 X 出棧,并將其壓入 Y 中
  3. 當(dāng)點(diǎn)擊前進(jìn)時(shí),依次從 Y 出棧,并將其壓入 X 中
  4. 打開新頁(yè)面時(shí),應(yīng)清空前進(jìn)棧 Y
  5. 當(dāng) X 中無(wú)數(shù)據(jù)時(shí),則說明沒有頁(yè)面可以后退了
  6. 當(dāng) Y 中無(wú)數(shù)據(jù)時(shí),則說明沒用頁(yè)面可以前進(jìn)了

舉例:

  • 打開 A,B,C 三個(gè)頁(yè)面,X 和 Y 的棧幀如下
|C|        | |
|B|        | |
|A|        | |
---        ---
 X          Y
  • 點(diǎn)擊兩次后退,從 C 退 B 再退到 A 頁(yè)面
| |        | |
| |        |B|
|A|        |C|
---        ---
 X          Y
  • 點(diǎn)擊前進(jìn),回到 B 頁(yè)面
| |        | |
|B|        | |
|A|        |C|
---        ---
 X          Y
  • 打開新頁(yè)面 D,從邏輯上講,D 時(shí)最新頁(yè)面,不存在更前面的頁(yè)面了,需要清空 Y
|D|        | |
|B|        | |
|A|        | |
---        ---
 X          Y

括號(hào)匹配

問題:

假設(shè)表達(dá)式中允許三種括號(hào):圓括號(hào)、方括號(hào)和大括號(hào)。編寫一個(gè)算法,判斷表達(dá)式中的各種左括號(hào)是否與右括號(hào)匹配。例如,輸入 2+(3+4)2+{[3]}-8,輸出匹配正確;輸入 2+(3+4[2)+{[3]}-8,輸出匹配錯(cuò)誤。

思路:

初始化一個(gè)棧,從左到右掃描表達(dá)式。掃描到左括號(hào)時(shí)入棧。掃描到右括號(hào)時(shí),棧頂彈出,若能匹配,則繼續(xù)掃描,否則匹配失敗。當(dāng)表達(dá)式掃描結(jié)束后,若棧為空,則匹配成功,否則匹配失敗。

復(fù)雜度:

時(shí)間復(fù)雜度 O(n),空間復(fù)雜度 O(n)。

實(shí)現(xiàn):

public boolean braceMatching(String expression) {
    ArrayBasedStack<Character> stack = new ArrayBasedStack<>(8);
    for (int i = 0; i < expression.length(); i++) {
        char c = expression.charAt(i);
        switch (c) {
            case '(':
            case '[':
            case '{':
                stack.push(c);
                break;
            case ')':
                if (stack.pop() != '(') {
                    return false;
                }
                break;
            case ']':
                if (stack.pop() != '[') {
                    return false;
                }
                break;
            case '}':
                if (stack.pop() != '{') {
                    return false;
                }
                break;
        }
    }
    if (stack.size != 0) {
        return false;
    }
    return true;
}

算術(shù)表達(dá)式求值

問題:

輸入算術(shù)表達(dá)式,輸出其運(yùn)算結(jié)果。為了簡(jiǎn)化問題,我們只考慮 +、-*、/ 四種運(yùn)算,他們的優(yōu)先級(jí)規(guī)則如下:

  1. 先乘除,后加減
  2. 從左算到右
  3. 先括號(hào)內(nèi),再括號(hào)外

算符優(yōu)先法

思路:

  1. 設(shè)置兩個(gè)工作棧,一個(gè)存儲(chǔ)操作數(shù),另一個(gè)存儲(chǔ)操作符
  2. 自左向右掃描算術(shù)表達(dá)式,遇到操作數(shù)直接入操作數(shù)棧
  3. 若遇到操作符,則根據(jù)算符優(yōu)先規(guī)則決定下一步操作:若優(yōu)先級(jí)高于棧頂操作符則入棧。否則,彈出棧頂算符,并從操作數(shù)棧彈出兩個(gè)操作數(shù)計(jì)算,將計(jì)算結(jié)果入操作數(shù)棧。繼續(xù)與棧頂操作符比較優(yōu)先級(jí),若相等或低于,則再計(jì)算,直至大于之,此時(shí)方可將操作符入棧
  4. 左括號(hào)一定入棧,且優(yōu)先級(jí)低于后續(xù)任何算符,即優(yōu)先級(jí)高于括號(hào)前,低于括號(hào)中。右括號(hào)一定出棧計(jì)算,直至遇到左括號(hào),即低于括號(hào)中
  5. 掃描完畢后,由于剩余算符的優(yōu)先級(jí)相同,直接依次出棧計(jì)算,結(jié)果入操作數(shù)棧,當(dāng)操作符??諘r(shí),計(jì)算結(jié)束

復(fù)雜度:

時(shí)間復(fù)雜度 O(n),空間復(fù)雜度 O(n)。

實(shí)現(xiàn):

public class StackReview {
    public static void main(String[] args) {
        String arithmeticExpression = "2-6/(1+3*2-1)+(4+4)*5+1";
        System.out.println(arithmeticExpression + " = " + new StackReview().calculate(arithmeticExpression));
    }

    public double calculate(String arithmeticExpression) {
        SinglyLinkedListBasedStack<Double> operands = new SinglyLinkedListBasedStack<>();
        SinglyLinkedListBasedStack<Character> operators = new SinglyLinkedListBasedStack<>();
        for (int i = 0; i < arithmeticExpression.length(); i++) {
            char c = arithmeticExpression.charAt(i);
            switch (c) {
                case '(':
                    operators.push(c);
                    break;
                case '+':
                case '-':
                case '*':
                case '/':
                    while (operators.size > 0 && !this.isGreater(c, operators.getTop())) {
                        operands.push(calSubExpress(operands.pop(), operands.pop(), operators.pop()));
                    }
                    operators.push(c);
                    break;
                case ')':
                    while (operators.getTop() != '(') {
                        operands.push(calSubExpress(operands.pop(), operands.pop(), operators.pop()));
                    }
                    // '()' 匹配完畢,'(' 出棧
                    operators.pop();
                    break;
                default:
                    operands.push((double) (c - '0'));
                    break;
            }
        }
        // 剩余算符優(yōu)先級(jí)相同,直接依次出棧
        while (operators.size > 0) {
            operands.push(calSubExpress(operands.pop(), operands.pop(), operators.pop()));
        }
        return operands.pop();
    }

    public boolean isGreater(char operator1, char operator2) {
        if (operator1 == '/' || operator2 == '(') {
            // 自左向右,/ 比 * 優(yōu)先級(jí)高。括號(hào)里面必須先算
            return true;
        } else if (operator1 == '-' && operator2 == '+') {
            // 自左向右,- 比 + 優(yōu)先級(jí)高
            return true;
        } else if (operator1 == '*' && (operator2 == '+' || operator2 == '-')) {
            return true;
        }
        return false;
    }

    public double calSubExpress(double operand1, double operand2, char operator) {
        double result = 0;
        switch (operator) {
            case '+':
                // 第二個(gè)數(shù)在前,因?yàn)樗热霔?                result = operand2 + operand1;
                break;
            case '-':
                result = operand2 - operand1;
                break;
            case '*':
                result = operand2 * operand1;
                break;
            case '/':
                result = operand2 / operand1;
                break;
        }
        return result;
    }
}

后綴表達(dá)式法

步驟:

  1. 將中綴表達(dá)式轉(zhuǎn)為后綴
  2. 根據(jù)后綴表達(dá)式求值

后綴的定義:

后綴表達(dá)式,指的是不包含括號(hào),運(yùn)算符放在兩個(gè)運(yùn)算對(duì)象的后面,所有的計(jì)算按運(yùn)算符出現(xiàn)的順序,嚴(yán)格從左向右進(jìn)行(不再考慮運(yùn)算符的優(yōu)先規(guī)則)。

舉例:

  1. 前綴表達(dá)式:操作符在操作數(shù)的前面,比如 +-543
  2. 中綴表達(dá)式:操作符在操作數(shù)的中間,這也是人類最容易識(shí)別的算術(shù)表達(dá)式 3+4-5
  3. 后綴表達(dá)式:操作符在操作數(shù)的后面,比如 34+5-

中綴轉(zhuǎn)后綴:

  1. 設(shè)置兩個(gè)棧,一個(gè)儲(chǔ)存算符,另一個(gè)儲(chǔ)存后綴表達(dá)式
  2. 自左向右掃描中綴表達(dá)式,遇到數(shù)字直接入后綴表達(dá)式棧
  3. 遇到操作符,比較其與算符棧頂?shù)膬?yōu)先級(jí):
    1. 若不高于棧頂算符優(yōu)先級(jí),則棧頂出棧并入后綴表達(dá)式棧,繼續(xù)比較后續(xù)棧頂符號(hào),直到遇到低于自己的符號(hào),此時(shí)入操作符棧
    2. 高于棧頂算符優(yōu)先級(jí),直接入操作符棧
    3. 左括號(hào)直接入操作符棧
    4. 右括號(hào)不入棧,且依次彈出棧內(nèi)操作符并入后綴表達(dá)式棧,直到遇到左括號(hào),將左括號(hào)也出棧
  4. 掃描完畢后,將操作符棧依次彈出并壓入后綴表達(dá)式棧
  5. 逆序輸出后綴表達(dá)式棧

根據(jù)后綴表達(dá)式求值:

  1. 設(shè)置一個(gè)操作數(shù)棧
  2. 從左到右掃描后綴表達(dá)式,遇到操作數(shù)入棧
  3. 遇到算符便從棧中彈出兩個(gè)操作數(shù)計(jì)算,結(jié)果入棧
  4. 直到算符處理完畢,最后入棧的操作數(shù)便是表達(dá)式的值

復(fù)雜度:

時(shí)間復(fù)雜度 O(n),空間復(fù)雜度 O(n)。

實(shí)現(xiàn):

public class StackReview {
    public static void main(String[] args) {
        String arithmeticExpression = "2-6/(1+3*2-1)+(4+4)*5+1";
        StackReview stackReview = new StackReview();
        StringBuilder suffixExpression = stackReview.InfixToSuffix(arithmeticExpression);
        System.out.println(arithmeticExpression + " = " + stackReview.calculateWithSuffixExpression(suffixExpression));
    }

    public double calculateWithSuffixExpression(StringBuilder suffixExpression) {
        ArrayBasedStack<Double> operands = new ArrayBasedStack<>(8);
        for (int i = suffixExpression.length() - 1; i >= 0; i--) {
            char c = suffixExpression.charAt(i);
            switch (c) {
                case '+':
                case '-':
                case '*':
                case '/':
                    operands.push(this.calSubExpress(operands.pop(), operands.pop(), c));
                    break;
                default:
                    operands.push((double) c - '0');
            }
        }
        return operands.pop();
    }

    public StringBuilder InfixToSuffix(String arithmeticExpression) {
        ArrayBasedStack<Character> operators = new ArrayBasedStack<>(8);
        ArrayBasedStack<Character> suffixExpression = new ArrayBasedStack<>(8);
        for (int i = 0; i < arithmeticExpression.length(); i++) {
            char c = arithmeticExpression.charAt(i);
            switch (arithmeticExpression.charAt(i)) {
                case '(':
                    operators.push(c);
                    break;
                case '+':
                case '-':
                case '*':
                case '/':
                    while (operators.size > 0 && !this.isGreater(c, operators.getTop())) {
                        suffixExpression.push(operators.pop());
                    }
                    operators.push(c);
                    break;
                case ')':
                    while (operators.size > 0 && operators.getTop() != '(') {
                        suffixExpression.push(operators.pop());
                    }
                    operators.pop();
                    break;
                default:
                    suffixExpression.push(c);
            }
        }
        while (operators.size > 0) {
            suffixExpression.push(operators.pop());
        }
        StringBuilder expression = new StringBuilder();
        while (suffixExpression.size > 0) {
            expression.append(suffixExpression.pop());
        }
        return expression;
    }

    public boolean isGreater(char operator1, char operator2) {
        if (operator1 == '/' || operator2 == '(') {
            // 自左向右,/ 比 * 優(yōu)先級(jí)高。括號(hào)里面必須先算
            return true;
        } else if (operator1 == '-' && operator2 == '+') {
            // 自左向右,- 比 + 優(yōu)先級(jí)高
            return true;
        } else if (operator1 == '*' && (operator2 == '+' || operator2 == '-')) {
            return true;
        }
        return false;
    }

    public double calSubExpress(double operand1, double operand2, char operator) {
        double result = 0;
        switch (operator) {
            case '+':
                // 第二個(gè)數(shù)在前,因?yàn)樗热霔?                result = operand2 + operand1;
                break;
            case '-':
                result = operand2 - operand1;
                break;
            case '*':
                result = operand2 * operand1;
                break;
            case '/':
                result = operand2 / operand1;
                break;
        }
        return result;
    }
}

JVM 中的棧

JVM 的內(nèi)存結(jié)構(gòu)大概分為:

  • 堆(Heap):線程共享。所有的對(duì)象實(shí)例以及數(shù)組都要在堆上分配。回收器主要管理的對(duì)象
  • 方法區(qū)(Method Area):線程共享。存儲(chǔ)類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼
  • 方法棧(JVM Stack):線程私有。存儲(chǔ)局部變量表、操作棧、動(dòng)態(tài)鏈接、方法出口,對(duì)象指針
  • 本地方法棧(Native Method Stack):線程私有。為虛擬機(jī)使用到的 Native 方法服務(wù)。如 Java 使用 c 或者 c++ 編寫的接口服務(wù)時(shí),代碼在此區(qū)運(yùn)行
  • 程序計(jì)數(shù)器(Program Counter Register):線程私有。有些文章也翻譯成 PC 寄存器(PC Register),同一個(gè)東西。它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。指向下一條要執(zhí)行的指令

參考文獻(xiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 棧:如何實(shí)現(xiàn)瀏覽器的前進(jìn)和后退功能? 瀏覽器的前進(jìn)、后退功能,我想你肯定很熟悉吧? 當(dāng)你依次訪問完一串頁(yè)面 a-b...
    GhostintheCode閱讀 979評(píng)論 0 2
  • 官網(wǎng) 中文版本 好的網(wǎng)站 Content-type: text/htmlBASH Section: User ...
    不排版閱讀 4,737評(píng)論 0 5
  • 第5章 引用類型(返回首頁(yè)) 本章內(nèi)容 使用對(duì)象 創(chuàng)建并操作數(shù)組 理解基本的JavaScript類型 使用基本類型...
    大學(xué)一百閱讀 3,692評(píng)論 0 4
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,691評(píng)論 1 32
  • 首先,拋出一個(gè)問題,如何使用棧來(lái)完成瀏覽器的前進(jìn)后退功能??? 如何理解"棧"???關(guān)于棧,有一個(gè)非常貼切的例子,...
    初心myp閱讀 852評(píng)論 0 0

友情鏈接更多精彩內(nèi)容