對瀏覽器的前進、後退功能,你必定很熟悉。當你訪問完一串頁面a-b-c以後。點擊瀏覽器的後退按鈕,就能夠查看以前瀏覽過的頁面b和a,當後退到頁面a的時候,點擊前進按鈕,就能夠從新查看頁面b和c。可是,若後退到b以後,點擊了新的頁面d,那就沒法再經過前進、後退功能查看頁面c了。若是想實現這個功能的話,就須要用到「棧」這種數據結構。算法
這裏有個例子,就是一摞疊在一塊兒的盤子,咱們平時放盤子的時候,都是從下往上一個一個放;取的時候,從上往下一個一個依次取,不能從中間任意抽出。數組
後進者先出,先進者後出,這就是典型的「棧」結構。瀏覽器
從棧的操做特性來看,棧是一種「操做受限」的線性表,只容許在一端插入和刪除數據。數據結構
從功能上來說,數組和鏈表能夠替代棧,可是有一點要明白,特定的數據結構是對特定場景的抽象,而且,數組和鏈表暴露了太多的操做接口,操做上的確靈活自由,但使用時就比較不可控,天然也就更容易出錯。函數
當某個數據集合只涉及在一端插入和刪除數據,而且知足後進先出,先進後出的特性,咱們就應該首選「棧」這種數據結構。post
從上述的討論中能夠發現,棧主要包含兩個操做,入棧和出棧,也就是在棧頂插入一個數據和從棧頂刪除一個數據。理解了這個以後,咱們看一看如何用代碼實現一個棧。操作系統
事實上,棧既能夠用數組實現,又能夠經過鏈表實現,用數組實現的叫作順序棧,用鏈表實現的叫鏈式棧。線程
瞭解了棧的定義和基本操做,那棧操做的時間、空間複雜度是多少呢?指針
不論是順序棧仍是鏈式棧,咱們存儲數據只須要一個大小爲n的數組就夠了。在入棧和出棧的過程當中,只須要一兩個臨時變量存儲空間,因此空間複雜度是O(1)。接口
注意,這裏存儲數據須要一個大小爲n的數組,並非說空間複雜度就是O(n)。由於這個n的空間是必須的,沒法省掉。因此咱們說空間複雜度的時候,是指除了本來的數據存儲以外,算法運行還須要額外的存儲空間。
時間複雜度相似,不論是順序棧仍是鏈式棧,入棧和出棧操做只涉及棧頂個別數據的操做,因此時間複雜度都是O(1)。
基於固定數組實現的棧,是一個大小固定的棧,也就是說,在初始化棧的時候須要事先指定棧的大小。當棧滿了以後,就沒法再向棧裏添加數據了。儘管鏈式棧的大小不受限,但要存儲next指針,內存消耗的相對較多。那如何基於數組實現一個能夠支持動態擴容的棧呢?
當數組空間不夠時,咱們能夠從新申請一塊更大的內存,將原來數組中的數據通通拷貝過去,這樣就能夠實現一個支持動態擴容的數組。因此,若是要實現一個支持動態擴容的棧,咱們只須要底層依賴一個支持動態擴容的數組就能夠了。當棧滿了以後,咱們就申請一個更大的數組,將原來的數據搬移到新數組中。
支持動態擴容的順序棧的入棧,出棧操做的時間複雜度:
對於出棧操做來講,咱們不會涉及到內存的從新申請和數據的搬移,因此出棧的時間複雜度仍是O(1)。對於入棧來講,當棧中有空閒空間時,入棧操做的時間複雜度爲O(1)。可是當空間不夠時,須要從新申請內存和數據搬移,因此時間複雜度變成了O(n).
也就是說,對於入棧操做來講,最好時間複雜度是O(1),最壞時間複雜度是O(n)。利用攤還分析法來分析入棧的時間複雜度:
這裏作一些假設:
1)棧空間不夠時,咱們從新申請一個是原來大小兩倍的數組;
2)爲了簡化分析,假設只有入棧操做沒有出棧操做;
3)定義不涉及內存搬移的入棧操做爲simple-push操做,時間複雜度是O(1)。
若是當前棧大小爲K,而且已滿,當再有新的數據要入棧的時候,就須要從新申請2倍大小的內存空間,並進行K個數據的搬移操做,而後再入棧。可是,接下來的K-1次入棧操做,咱們都不須要再從新申請內存和搬移數據,因此這K-1次入棧操做都只須要一個simple-push操做就能夠完成。K次入棧操做,總共涉及了K個數據的搬移,以及K次simple-push操做。將K個數據搬移均攤到K次入棧操做,那每一個入棧操做只須要一個數據搬移和一個simple-push操做。以此類推,入棧操做的均攤時間複雜度就爲O(1)。
1)函數調用棧
咱們知道,操做系統給每一個線程分配了一塊獨立的內存空間,這塊內存被組織成了「棧」這種結構,用來存儲函數調用時的臨時變量。每進入一個函數就會將臨時變量做爲一個棧幀出棧。2)編譯器利用棧來實現表達式求值。
這裏作一下簡化來講明:若一個表達式只包含加減乘除四則運算,該如何用棧來實現求值功能呢?
實際上,編譯器經過兩個棧來實現。其中一個保存操做數,另外一個是保存運算符,咱們從左往右遍歷表達式,當遇到數字,就直接壓入操做數棧;當遇到運算符,就與運算符棧的棧頂元素進行比較。
若是比運算符棧頂元素的優先級高,就將當前運算符壓入棧;若是比運算符棧頂元素的優先級低或者相同,就從運算符棧中取棧頂元運算符,從操做數棧的棧頂取2個操做數,而後進行計算,再把計算完的結果壓入操做樹棧,繼續比較。
3)括號匹配
假設表達式中只包含三種括號,圓括號(),方括號[],和花括號{},而且它們能夠任意嵌套。好比,{[()]}或[{()}([])]等都爲合法格式,而{[}()]或[({)]爲不合法格式。那給一個包含三種括號的表達式字符串,如何檢查它是否合法?
這裏能夠用棧來解決,咱們能夠用棧來保存未匹配的左括號,從左到右依次掃描字符串,當掃描到左括號是,則將其壓入棧中,當掃描到右括號時,從棧頂取出一個左括號,若可以匹配,則繼續掃描剩餘的字符串。若是掃描的過程當中,遇到不能配對的右括號,或者棧中沒有數據,則說明Wie非法格式。
當全部括號都掃描爲完成以後,若是棧爲空,則說明字符串爲合法格式;不然說明有未匹配的左括號,爲非法格式。
如何實現瀏覽器的前進、後退功能?
實際上,用兩個棧就能夠很是完美地解決這個問題。
咱們使用兩個棧,X和Y,咱們把首次瀏覽的頁面依次壓入棧X,當點擊後退時,再依次從X中出棧,並將出棧的數據依次壓入棧Y。當咱們點擊前進按鈕時,咱們再依次從Y中取出數據,壓入棧X中。當X中沒有數據時,就說明沒有頁面能夠繼續後退瀏覽了,當Y中沒有數據時,就說明沒有頁面能夠點擊前進按鈕瀏覽了。
當從b頁面跳轉到d頁面時,頁面c就沒法再經過前進、後退按鈕重複查看了,因此須要清空棧Y。
棧是一種操做受限的數據結構,只支持入棧和出棧操做。後進先出是它最大的特色。棧既能夠用數組實現爲順序棧,也能夠經過鏈表實現爲鏈式棧。不論是基於數組仍是鏈表,入棧、出棧的時間複雜度都爲O(1)。此外,還有一種基於動態數組實現的支持動態擴容的棧,分析它的時間複雜度能夠採用攤還分析法來分析。