資料結構與演算法學習筆記之後進先出的“桶”
前言
棧最為一種的常用的資料結構,用“桶”來形容最合適不過;今天我們就來學習一下
正文
一、棧的定義?
1.“後進先出,先進後出”的資料結構。
2.從操作特性來看,是一種“操作受限”的線性表,只可以在一端插入和刪除資料。
二、為什麼需要棧?
1.任何資料結構都是對特定應用場景的抽象,棧是一種操作受限的資料結構,其操作特性用陣列和連結串列均可實現,但卻暴露太多的操作介面,使用時容易出錯;
2.當某個資料集合只涉及在一端插入和刪除資料,且滿足後進者先出,先進者後出的操作特性時,我們應該首選棧這種資料結構
三、如何實現棧?
棧可以用陣列,連結串列來實現
1.以陣列為例
空間複雜度為O(1)
時間複雜度大多數為O(1),特殊情況自動擴容拷貝原陣列時為O(n);
均攤時間複雜度接近於O(1);
// 基於陣列實現的順序棧 public class ArrayStack { private String[] items; // 陣列 private int count; // 棧中元素個數 private int n; // 棧的大小 // 初始化陣列,申請一個大小為 n 的陣列空間 public ArrayStack(int n) { this.items = new String[n]; this.n = n; this.count = 0; } // 入棧操作 public boolean push(String item) { // 陣列空間不夠了,直接返回 false,入棧失敗。 if (count == n) return false; // 將 item 放到下標為 count 的位置,並且 count 加一 items[count] = item; ++count; return true; } // 出棧操作 public String pop() { // 棧為空,則直接返回 null if (count == 0) return null; // 返回下標為 count-1 的陣列元素,並且棧中元素個數 count 減一 String tmp = items[count-1]; --count; return tmp; } }
(程式碼來自於網路,如有侵權請告知我刪除)
2.以連結串列為例(網上找的)
空間複雜度為O(1)
時間複雜度大多數為O(1)
public class StackOfLinked<Item> implements Iterable<Item> { //定義一個內部類,就可以直接使用型別引數 private class Node{ Item item; Node next; } private Node first; private int N; //構造器 public StackOfLinked(){} //新增 public void push(Item item){ Node oldfirst = first; first = new Node(); first.item = item; first.next = oldfirst; N++; } //刪除 public Item pop(){ Item item = first.item; first = first.next; N--; return item; } //是否為空 public boolean isEmpty(){ return N == 0; } //元素數量 public int size(){ return N; } //返回棧中最近新增的元素而不刪除它 public Item peek(){ return first.item; } @Override public Iterator<Item> iterator() { return new LinkedIterator(); } //內部類:迭代器 class LinkedIterator implements Iterator{ int i = N; Node t = first; @Override public boolean hasNext() { return i > 0; } @Override public Item next() { Item item = (Item) t.item; t = t.next; i--; return item; } } }
(程式碼來自於網路,如有侵權請告知我刪除)
四、棧的應用
1.函式呼叫中的應用
作業系統給每個執行緒分配了一塊獨立的記憶體空間,這塊記憶體被組織成“棧”這種結構,用來儲存函式呼叫時的臨時變數。每進入一個函式,就會將其中的臨時變數作為棧幀入棧,當被呼叫函式執行完成,返回之後,將這個函式對應的棧幀出棧。
2.在表示式求值中的應用(比如:34+13*9+44-12/3)
利用兩個棧,一個用來儲存運算元,一個用來儲存運算子。我們從左向右遍歷表示式,當遇到數字,我們就直接壓入運算元棧;當遇到運算子,就與運算子棧的棧頂元素進行比較,若比運算子棧頂元素優先順序高,就將當前運算子壓入棧,若比運算子棧頂元素的優先順序低或者相同,從運算子棧中取出棧頂運算子,從運算元棧頂取出2個運算元,然後進行計算,把計算完的結果壓入運算元棧,繼續比較。
(圖片來自於王爭)
3.棧在括號匹配中的應用(比如:{}{[()]()})
用棧儲存為匹配的左括號,從左到右一次掃描字串,當掃描到左括號時,則將其壓入棧中;當掃描到右括號時,從棧頂取出一個左括號,如果能匹配上,則繼續掃描剩下的字串。如果掃描過程中,遇到不能配對的右括號,或者棧中沒有資料,則說明為非法格式。
當所有的括號都掃描完成之後,如果棧為空,則說明字串為合法格式;否則,說明未匹配的左括號為非法格式。
4.如何實現瀏覽器的前進後退功能?
我們使用兩個棧X和Y,我們把首次瀏覽的頁面依次壓如棧X,當點選後退按鈕時,再依次從棧X中出棧,並將出棧的資料一次放入Y棧。當點選前進按鈕時,我們依次從棧Y中取出資料,放入棧X中。當棧X中沒有資料時,說明沒有頁面可以繼續後退瀏覽了。當Y棧沒有資料,那就說明沒有頁面可以點選前進瀏覽了。
(圖片來自於王爭)
五、兩個問題
1. 我們在講棧的應用時,講到用函式呼叫棧來儲存臨時變數,為什麼函式呼叫要用“棧”來儲存臨時變數呢?用其他資料結構不行嗎?
答:因為函式呼叫的執行順序符合後進者先出,先進者後出的特點。
函式呼叫中經常巢狀,栗子:A呼叫B,B又呼叫C,那麼就需要先把C執行完,結果賦值給B中的臨時變數,B的執行結果再賦值給A的臨時變數,巢狀越深的函式越需要被先執行,這樣剛好符合棧的特點,因此每次遇到函式呼叫,只需要壓棧,最後依次從棧頂彈出依次執行即可,根據資料結構是特定應用場景的抽象的原則,我們優先考慮棧結構。
2.我們都知道,JVM 記憶體管理中有個“堆疊”的概念。棧記憶體用來儲存區域性變數和方法呼叫,堆記憶體用來儲存 Java 中的物件。那 JVM 裡面的“棧”跟我們這裡說的“棧”是不是一回事呢?如果不是,那它為什麼又叫作“棧”呢?
答:記憶體中的堆疊和資料結構堆疊不是一個概念,可以說記憶體中的堆疊是真實存在的物理區,資料結構中的堆疊是抽象的資料儲存結構。
記憶體空間在邏輯上分為三部分:程式碼區、靜態資料區和動態資料區,動態資料區又分為棧區和堆區。
程式碼區:儲存方法體的二進位制程式碼。高階排程(作業排程)、中級排程(記憶體排程)、低階排程(程序排程)控制程式碼區執行程式碼的切換。
靜態資料區:儲存全域性變數、靜態變數、常量,常量包括final修飾的常量和String常量。系統自動分配和回收。
棧區:儲存執行方法的形參、區域性變數、返回值。由系統自動分配和回收。
堆區:new一個物件的引用或地址儲存在棧區,指向該物件儲存在堆區中的真實資料。
作者:Dawnzhang
出處:https://www.cnblogs.com/clwydjgs/p/9792256.html