1. 程式人生 > >資料結構與演算法之棧篇

資料結構與演算法之棧篇

1、如何理解“棧”?

   棧是一種典型“後進者先出,先進者後出”的結構。從棧的操作特性上來看,棧是一種“操作受限”的線性表,只允許在一端插入和刪除資料。

2、如何讓實現一個“棧”?

   對於棧,主要包含兩個操作,入棧和出棧,也就是在棧頂插入一個數據和從棧頂刪除一個數據,實際上棧既可以用陣列來實現,也可以用連結串列來實現。用陣列實現的棧我們叫做順序棧,用連結串列實現的棧,我們叫作鏈式棧。

  舉個例子:

 下面實現一個基於陣列的順序棧。

 

       不管是順序棧還是鏈式棧,我們儲存資料只需要一個大小為n的陣列就可以。在出棧和入棧過程中,只需要一兩個臨時變數儲存空間,所以空間複雜度為O(1)。

   注意,這裡儲存資料需要一個大小為n的陣列,並不是說空間複雜度就是O(n)。因為,這n個空間是必須的,無法省掉。所以說空間複雜度的時候,是指除了原本資料儲存空間外,演算法執行還需要額外的儲存空間。

   對於時間複雜度,不管是順序棧還是鏈式棧,入棧、出棧只涉及張定的個別資料的操作,所以時間複雜度都是O(1)。

3、支援動態擴容的順序棧

      當基於陣列實現的棧,是一個固定大小的棧,也就是說,在初始化棧時需要事先指定棧的大小。當棧滿之後,就無法再往棧裡新增資料了。儘管鏈式棧的大小不受限,但要儲存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)。

      通過以上例子,印證了前面分析,均攤時間複雜度一般等於最好時間複雜度。因為在大部分情況下,如棧操作的時間複雜度為O(1),只有個別時刻才會退化為O(n)。

4、棧在函式呼叫中的應用

       棧作為一個比較基礎的資料結構,應用場景還是蠻多的,比較經典的一個應用場景就是函式呼叫棧。

      作業系統給每個執行緒分配了一塊獨立的記憶體空間,這塊組織成“棧”這種結構,用來儲存函式呼叫的臨時變數。每進入一個函式,就會將臨時變數作為一個棧幀入棧,當被呼叫函式執行完成,返回之後,將這個函式對應的棧幀出棧。舉個例子:

 

       從程式碼中看,main()函式呼叫了add()函式,獲取計算結果,並且與臨時變數a相加,最後列印res的值。下圖是執行到add()函式時,函式呼叫棧的情況。

5、棧在表示式求值中的應用

    棧的另一個常見的應用場景,編譯器如何利用棧來實現表示式求值。比如:3+5*8-6。實際上,編譯器就是通過兩個棧來實現的。其中一個儲存運算元的棧,另一個是儲存運算子的棧。我們從從左向右遍歷表示式,當遇到數字,我們就直接壓入操作棧;當遇到運算子,就與運算子棧的棧頂元素進行比較。如果比運算子棧頂的元素優先順序高,就將當前運算子壓入棧;如果比運算子棧頂元素的優先順序低或者相同,從運算子棧中取棧頂運算子,從運算元棧的棧頂取2個運算元,然後進行計算,再把計算玩的結果壓入運算元棧,繼續比較。

 

  

6、棧在括號匹配中的應用

        舉個列子:藉助棧來檢查表示式中的括號是否匹配?

       假設:表示式中只包含三種括號,圓括號()、方括號[]和花括號{},並且他們可以任意巢狀。比如{[{}]}或[{()}([])等都為合法格式,而{[}()]或[({)]為不合法的格式,那麼如何讓用棧來解決呢?

       我們用棧來儲存未匹配的左括號,從左到右依次掃描字串。當掃描到左括號時,則將其壓入棧中;當掃描到右括號時,從棧頂取出一個括號。如果能夠匹配,比如"("跟")"匹配,"["跟"]"匹配,"{"跟"}"匹配,則繼續掃描剩下的字串。如果掃描的過程中,遇到不能匹配的的右括號,或者棧中沒有資料,則說明為非法格式。當所有的括號都掃描完成之後,如果棧為空,說明字串為合法格式;否則,說明有未匹配的左括號,非法格式。

7、開篇問題

如何實現瀏覽器的前進、後退功能?

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

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

       當你通過瀏覽器的後退按鈕,從頁面c後退到頁面a之後,我們就依次把c和b從棧X中彈出,並且依次放入到棧Y。這個時候,兩個棧的資料就是這個樣子:  

        這個時候你又想看頁面b,於是你有點選前進按鈕回到b頁面,我們就把b再從棧Y中出棧,放入到棧X中。此時兩個棧的資料就是這個樣子

這個時候,你又通過頁面b又跳轉到新的頁面d了,頁面c就無法再通過前進、後退按鈕重複查看了,所以需要清空棧Y。此時兩個棧的資料就這個樣子:

 

歡迎大家掃碼關注微信公眾號,其中含有有大量免費的人工智慧、影象處理、IT資料:

                                                                            Change,There is no better way!