棧:如何實現瀏覽器的前進和後退功能?
本文是學習演算法的筆記,《資料結構與演算法之美》,極客時間的課程
我們瀏覽網頁,經常會用到前進,後退的功能。比如依次瀏覽了a-b-c 三個頁面,這時點後退,可以回到 b 頁面,再次點選回到 a 頁面。如果這時,又進入 新頁面d ,那就無法通過前進,後退功能回到b、c頁面了。
這樣一個功能,要如何實現呢?
其實這樣一個功能,可以藉助棧來實現。
什麼是棧?它是一個數據結構,資料先進後出,後進先出。打一個比方,放一疊盤子,放的時候,都放最上面,取的時候也從最上面取。典型的後進先出。
棧,可以用陣列來實現,也可以用連結串列來實現。前者叫順序棧,後者是鏈式棧。
為什麼會有棧這種資料結構,直接用陣列或是連結串列不就好了麼?棧有嚴格的限制,只能從棧頂壓入資料,從棧頂取資料,可以滿足特定場景的需求,而連結串列或是陣列暴露了太多的介面,容易出錯。
如何實現一個棧
public class ArrayStack{ private String [] items; // 陣列 private int count; // 棧中元素個數 private int n; //棧的大小 // 初始化陣列,申請一個大小為 n 的陣列空間 public ArrayStack(int n){ items = new String [n]; this.n = n; count = 0; } // 入棧操作 public boolean push(String item){ if(count == n){ return false; } items[count-1] = item; count++; return true; } // 出棧操作 public String pop(){ if(count == 0){ return null; } String temp = items[count-1]; count--; return temp; } }
棧的實際應用
棧作為一個基礎的資料結構,經典的應用場景是函式呼叫。
我們知道,作業系統,為每個執行緒分配了獨立的記憶體空間,這些記憶體空間組織成棧的結構,來儲存函式呼叫產生的臨時變數。每次進入一個函式,將臨時變數作為棧楨入棧,當函式呼叫完成,有結果返回時,函式對應的棧楨出棧。便於理解,我們看一看下面的一段程式碼
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; }
為了清晰地看到這個函式呼叫的過程中,出棧、入棧的操作,貼一張圖
棧在表示式求值中的應用
再看一個常見的應用場景,編譯器如何利用棧來實現表示式求值。
我們用一個簡單的四則運算來說明這個問題,比如3+5*8-6,對於計算機來說,如何理解這個表示式,是個不太容易的事兒。
事實上,編譯器是通過兩個棧來實現的,其中一個儲存數字,暫且叫A,一個儲存運算子暫且叫B。從左到右遍歷表示式,當遇到數字,直接壓入A棧,當遇到運符時,就與B棧中棧頂元素進行比較。
如果比棧頂元素的優先順序高,就將運算子壓入棧,如果相同或者低,從B棧中取出棧頂元素,從A棧的棧頂取出兩個數字,進行計算,再把計算結果壓入A棧,繼續比較。如下圖
解答開篇的那個問題
我們使用兩個棧,X和Y,首次瀏覽的頁面,依次壓入棧X。當點選回退時,依次從X棧中取出資料,並依次放入Y棧。點選前進時,從Y棧中取出資料,並依次放入X棧。當X棧中沒有資料時,就說明不能再回退了;當Y中沒有資料時,就說明不能再前進了。
比如你依次瀏覽了a b c 三個頁面,我們依次把 a b c 壓入棧,這時兩個棧是這個樣子
瀏覽器通過後退按鈕,回退到頁面 a 時,我們把 c b 依次壓入Y棧,這時兩個棧是這個樣子
這時候,你又通過前進按鈕,又回到 b 頁面,這時兩個棧的資料是這個樣子
這時,你由 b 頁面開啟新的頁面 d,那麼你就不能再通過後退回到 c 頁面,Y棧資料要清空。這時兩個棧是這個樣子