8.棧-實現瀏覽器的前進後退

使用棧實現瀏覽器的前進後退

當你一次訪問 一、二、3 頁面以後,點擊瀏覽器的後退按鈕就能夠返回到 2 和 1.當後退到 1,點擊前進按鈕還能夠繼續查看頁面 二、3。可是當你退到 2 頁面,點擊了新的頁面 4,那就沒法繼續經過前進、後退查看頁面 3 了。java

咱們如何實現這個功能呢?git

什麼是棧

「棧」咱們都知道 Java 虛擬機 JVM 就有『本地方法棧』『虛擬機棧』的劃分,每一個方法執行的時候都會建立一個棧幀用於存放局部變量表、操做數棧、動態連接、方法出口信息。github

每個方法從調用到結束,就對應着一個棧幀在「虛擬機棧」的入棧與出棧的過程。這裏其實就是運用了「棧」數據結構的特性:後進先出、先進後出。就像一摞疊在一塊兒的盤子,入棧就像咱們放盤子,出棧就是咱們從上往下一個個取。算法

棧

棧是一種「操做受限」的線性表,只容許在一端插入和刪除數據。數組

是否是以爲這種數據結構有何意義,只有受限的操做,相比「數組」和「鏈表」感受沒有任何優點。爲什麼還要用這個「操做受限」的「棧」呢?瀏覽器

特定的數據結構用在特定的場景,數組與鏈表暴露太多的操做接口,操做靈活帶來的就是不可控,也就更加容易出錯。數據結構

當某個數據集合只涉及在一端插入和刪除數據,而且知足後進先出、先進後出的特性,咱們就應該首選「棧」這種數據結構ide

好比咱們的 JVM 棧結構,方法調用則是對應的入棧與出棧。函數

棧的實現

核心操做就是「入棧」「出棧」,也就是在棧頂插入元素、從棧頂取出元素。this

理解了兩個核心操做後,咱們可使用數組或者鏈表來實現。

  • 數組實現的棧,叫作 順序棧
  • 用鏈表實現,叫作 鏈式棧

這裏我經過數組實現一個順序棧,可用於實際開發中,我拓展了「清空棧」、「拓容」、「構建默認大小與最大限制」。代碼我放在 GitHub https://github.com/UniqueDong/algorithms.git上,本身擼一遍,再對比下是否寫的正確

這裏不只僅做爲一個 示例,個人例子還考慮了棧默認初始大小以及最大限制,當超過默認大小可是尚未達到最大限制的時候,還須要擴容操做。

import java.util.Arrays;

/**
 * 基於數組實現的棧
 * @param <T>
 */
public class ArrayStack<T> {
    /**
     * 默認大小
     */
    public static final int DEFAULT_SIZE = 128;
    /**
     * 默認最大限制,-1 表示無限制
     */
    private static final int DEFAULT_LIMIT = -1;
    /**
     * 初始化棧大小
     */
    private int size;
    /**
     * 棧最大限制數,-1 表示無限制
     */
    private final int limit;
    /**
     * 指向棧頂元素的下標,默認沒有數據 -1
     */
    private int index;
    /**
     * 保存數據
     */
    private Object[] stack;

    /**
     * 默認構造方法,建立一個 128 大小,無限制數量的棧
     */
    public ArrayStack() {
        this(DEFAULT_SIZE, DEFAULT_LIMIT);
    }

    // 指定大小與最大限制的棧
    public ArrayStack(int size, int limit) {
        this.index = -1;
        if (limit > DEFAULT_LIMIT && size > limit) {
            this.size = limit;
        } else {
            this.size = size;
        }
        this.limit = limit;
        this.stack = new Object[size];
    }

    /**
     * push obj to stack of top
     *
     * @param obj push data
     * @return true if push success
     */
    public boolean push(T obj) {
        index++;
        // 當下標達到 size,說明須要拓容了。判斷是否須要拓容
        if (index == size) {
            // 若超過限制則返回 false,不然執行拓容
            if (limit != DEFAULT_LIMIT && size >= limit) {
                index--;
                return false;
            } else {
                // 拓容
                expand();
            }
        }
        stack[index] = obj;
        return true;
    }

    /**
     * pop stack of top element
     *
     * @return top of stack element
     */
    public T pop() {
        if (index == -1) {
            return null;
        }
        T result = (T) stack[this.index];
        stack[index--] = null;
        return result;
    }

    /**
     * 清空棧數據
     */
    public void clear() {
        // 判斷是否空
        if (index > -1) {
            // 只須要將 index + 1 個元素設置 null,不須要遍歷 size
            for (int i = 0; i < index + 1; i++) {
                stack[i] = null;
            }
        }
        index = -1;
    }

    public int size() {
        return this.index + 1;
    }

    @Override
    public String toString() {
        return "ArrayStack{" +
                "size=" + size +
                ", limit=" + limit +
                ", index=" + index +
                ", stack=" + Arrays.toString(stack) +
                '}';
    }
}

咱們來分析下「棧」的空間複雜度與實踐複雜度。

在「入棧」「出棧」的操做中,存儲數據都是隻須要一個 最大限制 n 的數組,因此空間複雜度是 O(1)。

存儲數據爲 n 大小的數組,不是說空間複雜度是 O(n),這裏必定要注意。由於 這個 n 是必須的,沒法省。當咱們說空間複雜度的時候,指的是除本來數據存儲空間外,算法還須要額外的那部分存儲空間。

不論是鏈式棧仍是順序棧,出棧與入棧只是設計棧頂個別數據的操做,只是須要拓容的時候會 O(n),可是均攤之後最後仍是 O(1)。

因此棧的時間與空間複雜度都是 O(1)。

拓容實現

當容量達到指定默認值大小的時候再入棧數據則須要拓容知道拓容到最大限制大小。

數組拓容能夠經過 System.arraycopy(stack, 0, newStack, 0, size); 當空間不足的時候申請原數組兩倍大小的數組,而後把原始數據複製到新數組中。

拓容

/**
     * 擴容兩倍 ,如果兩倍數值超過 limit 則只能拓容到 limit
     */
    private void expand() {
        int newSize = size * 2;
        if (limit != DEFAULT_LIMIT && newSize > limit) {
            newSize = limit;
        }
        Object[] newStack = new Object[newSize];
        System.arraycopy(stack, 0, newStack, 0, size);
        this.stack = newStack;
        this.size = newSize;
    }

棧的應用場景

經典的應用場景就是 函數調用棧

操做系統給每一個線程分配了一塊獨立的內存空間,這塊內存被組織成「棧」這種結構, 用來存儲函數調用時的臨時變量。每進入一個函數,就會將臨時變量做爲一個棧幀入棧,當被調用函數執行完成,返回以後,將這個函數對應的棧幀出棧。

表達式求值

爲了方便解釋,我將算術表達式簡化爲只包含加減乘除四則運算,好比:34+13*9+44-12/3 。 對於這個四則運算,咱們人腦能夠很快求解出答案,可是對於計算機來講,理解這個表達式自己就是個挺難的事兒。若是換做你,讓你來實現這樣一個表達式求值的功能,你會怎麼作呢?

實際上編譯器就是經過兩個棧實現的。一個保存操做數的棧、一個則保存操做運算符的棧。

咱們從左向右遍歷表達式,當遇到數字,咱們就直接壓入操做數棧;當遇到運算符,就與運算符棧的棧頂元素進行比較。

若是比運算符棧頂元素的優先級高,就將當前運算符壓入棧;若是比運算符棧頂元素的優先級低或者相同,從運算符棧中取棧頂運算符,從操做數棧的棧頂取 2 個操做數,而後進行計算,再把計算完的結果壓入操做數棧,繼續比較。以下圖所示

8.棧-實現瀏覽器的前進後退

瀏覽器後退前進

咱們使用兩個棧,X 和 Y,咱們把首次瀏覽的頁面依次壓入棧 X,當點擊後退按鈕時,再依次從棧 X 中出棧,並將出棧的數據依次放入棧 Y。當咱們點擊前進按鈕時,咱們依次從棧 Y 中取出數據,放入棧 X 中。當棧 X 中沒有數據時,那就說明沒有頁面能夠繼續後退瀏覽了。當棧 Y 中沒有數據,那就說明沒有頁面能夠點擊前進按鈕瀏覽了。

好比你順序查看了 a,b,c 三個頁面,咱們就依次把 a,b,c 壓入棧,這個時候,兩個棧的數據就是這個樣子:

8.棧-實現瀏覽器的前進後退

點擊後退,從頁面 c 後退到頁面 a 以後,咱們就依次把 c 和 b 從棧 X 中彈出,而且依次放入到棧 Y。這個時候,兩個棧的數據就是這個樣子:

8.棧-實現瀏覽器的前進後退

這時候想看 b,因而你又點擊前進按鈕回到 b 頁面,咱們就把 b 再從棧 Y 中出棧,放入棧 X 中。此時兩個棧的數據是這個樣子:

8.棧-實現瀏覽器的前進後退

這個時候,你經過頁面 b 又跳轉到新的頁面 d 了,頁面 c 就沒法再經過前進、後退按鈕重複查看了,因此須要清空棧 Y。此時兩個棧的數據這個樣子:

8.棧-實現瀏覽器的前進後退

經過來兩個棧來操做,快速的實現了前進後退。

因爲篇幅緣由,具體實現代碼咱們下文再擼,關注我一手掌握最新文章。

MageByte

推薦閱讀

1.跨越數據結構與算法

2.時間複雜度與空間複雜度

3.最好、最壞、平均、均攤時間複雜度

4.線性表之數組

5.鏈表導論-心法篇

6.單向鏈表正確實現方式

7.雙向鏈表正確實現

原創不易,以爲有用但願讀者隨手「在看」「收藏」「轉發」三連。

相關文章
相關標籤/搜索