【数据结构与算法】栈与队列
棧
一、什么是棧?
1.后進者先出,先進者后出,這就是典型的“棧”結構。
2.從棧的操作特性來看,是一種“操作受限”的線性表,只允許在端插入和刪除數據。
二、為什么需要棧?
1.棧是一種操作受限的數據結構,其操作特性用數組和鏈表均可實現。
2.但,任何數據結構都是對特定應用場景的抽象,數組和鏈表雖然使用起來更加靈活,但卻暴露了幾乎所有的操作,難免會引發錯誤操作的風險。
3.所以,當某個數據集合只涉及在某端插入和刪除數據,且滿足后進者先出,先進者后出的操作特性時,我們應該首選棧這種數據結構。
三、如何實現棧?
1.棧的API
public class Stack {
//壓棧
public void push(Item item){}
//彈棧
public Item pop(){}
//是否為空
public boolean isEmpty(){}
//棧中數據的數量
public int size(){}
//返回棧中最近添加的元素而不刪除它
public Item peek(){}
}
實現
2.1 數組實現(自動擴容)
時間復雜度分析:根據均攤復雜度的定義,可以得數組實現(自動擴容)符合大多數情況是O(1)級別復雜度,個別情況是O(n)級別復雜度,比如自動擴容時,會進行完整數據的拷貝。
空間復雜度分析:在入棧和出棧的過程中,只需要一兩個臨時變量存儲空間,所以O(1)級別。我們說空間復雜度的時候,是指除了原本的數據存儲空間外,算法運行還需要額外的存儲空間。
2.2 鏈表實現
時間復雜度分析:壓棧和彈棧的時間復雜度均為O(1)級別,因為只需更改單個節點的索引即可。
空間復雜度分析:在入棧和出棧的過程中,只需要一兩個臨時變量存儲空間,所以O(1)級別。我們說空間復雜度的時候,是指除了原本的數據存儲空間外,算法運行還需要額外的存儲空間。
實現代碼:(見另一條留言)
三、棧的應用
1.棧在函數調用中的應用
操作系統給每個線程分配了一塊獨立的內存空間,這塊內存被組織成“棧”這種結構,用來存儲函數調用時的臨時變量。每進入一個函數,就會將其中的臨時變量作為棧幀入棧,當被調用函數執行完成,返回之后,將這個函數對應的棧幀出棧。
2.棧在表達式求值中的應用(比如:34+13*9+44-12/3)
利用兩個棧,其中一個用來保存操作數,另一個用來保存運算符。我們從左向右遍歷表達式,當遇到數字,我們就直接壓入操作數棧;當遇到運算符,就與運算符棧的棧頂元素進行比較,若比運算符棧頂元素優先級高,就將當前運算符壓入棧,若比運算符棧頂元素的優先級低或者相同,從運算符棧中取出棧頂運算符,從操作數棧頂取出2個操作數,然后進行計算,把計算完的結果壓入操作數棧,繼續比較。
3.棧在括號匹配中的應用(比如:{}{()})
用棧保存為匹配的左括號,從左到右一次掃描字符串,當掃描到左括號時,則將其壓入棧中;當掃描到右括號時,從棧頂取出一個左括號,如果能匹配上,則繼續掃描剩下的字符串。如果掃描過程中,遇到不能配對的右括號,或者棧中沒有數據,則說明為非法格式。
當所有的括號都掃描完成之后,如果棧為空,則說明字符串為合法格式;否則,說明未匹配的左括號為非法格式。
4.如何實現瀏覽器的前進后退功能?
我們使用兩個棧X和Y,我們把首次瀏覽的頁面依次壓如棧X,當點擊后退按鈕時,再依次從棧X中出棧,并將出棧的數據一次放入Y棧。當點擊前進按鈕時,我們依次從棧Y中取出數據,放入棧X中。當棧X中沒有數據時,說明沒有頁面可以繼續后退瀏覽了。當Y棧沒有數據,那就說明沒有頁面可以點擊前進瀏覽了。
JVM 內存管理中有個“堆棧”的概念。棧內存用來存儲局部變量和方法調用,堆內存用來存儲 Java 中的對象。那 JVM 里面的“棧”跟數據結構中的“棧”是不是一回事呢?
內存中的堆棧和數據結構堆棧不是一個概念,可以說內存中的堆棧是真實存在的物理區,數據結構中的堆棧是抽象的數據存儲結構。
內存空間在邏輯上分為三部分:代碼區、靜態數據區和動態數據區,動態數據區又分為棧區和堆區。
- 代碼區:存儲方法體的二進制代碼。高級調度(作業調度)、中級調度(內存調度)、低級調度(進程調度)控制代碼區執行代碼的切換。
- 靜態數據區:存儲全局變量、靜態變量、常量,常量包括final修飾的常量和String常量。系統自動分配和回收。
- 棧區:存儲運行方法的形參、局部變量、返回值。由系統自動分配和回收。
- 堆區:new一個對象的引用或地址存儲在棧區,指向該對象存儲在堆區中的真實數據。
leetcode上關于棧的題目:20,155,232,844,224,682,49
隊列
一、什么是隊列?
1.先進者先出,這就是典型的“隊列”結構。
2.支持兩個操作:入隊enqueue(),放一個數據到隊尾;出隊dequeue(),從隊頭取一個元素。
3.所以,和棧一樣,隊列也是一種操作受限的線性表。
二、如何實現隊列?
1.隊列API
public interface Queue {
public void enqueue(T item); //入隊
public T dequeue(); //出隊
public int size(); //統計元素數量
public boolean isNull(); //是否為空
}
2.實現
** 2.1 數組**
// 用數組實現的隊列 public class ArrayQueue {// 數組:items,數組大小:nprivate String[] items;private int n = 0;// head表示隊頭下標,tail表示隊尾下標private int head = 0;private int tail = 0;// 申請一個大小為capacity的數組public ArrayQueue(int capacity) {items = new String[capacity];n = capacity;}// 入隊public boolean enqueue(String item) {// 如果tail == n 表示隊列已經滿了if (tail == n) return false;items[tail] = item;++tail;return true;}// 出隊public String dequeue() {// 如果head == tail 表示隊列為空if (head == tail) return null;// 為了讓其他語言的同學看的更加明確,把--操作放到單獨一行來寫了String ret = items[head];++head;return ret;}// 入隊操作,將item放入隊尾,如圖public boolean enqueue(String item) {// tail == n表示隊列末尾沒有空間了if (tail == n) {// tail ==n && head==0,表示整個隊列都占滿了if (head == 0) return false;// 數據搬移for (int i = head; i < tail; ++i) {items[i-head] = items[i];}// 搬移完之后重新更新head和tailtail -= head;head = 0;}items[tail] = item;++tail;return true;} }
2.2 循環鏈表思想
關鍵隊空條件 head == tail
隊滿判斷條件 (tail+1)%n=head
2.3 鏈表實現
public class LinkedQueue { //定義一個節點類 private class Node{ String value; Node next; } //記錄隊列元素個數 private int size = 0; //head指向隊頭結點,tail指向隊尾節點 private Node head; private Node tail; //申請一個隊列 public LinkedQueue(){} //入隊 public boolean enqueue(String item){ Node newNode = new Node(); newNode.value = item; if (size == 0) head = newNode; else tail.next = newNode; tail = newNode; size++; return true; } //出隊 public String dequeue(){ String res = null; if(size == 0) return res; if(size == 1) tail = null; res = head.value; head = head.next; size--; return res; }三、隊列有哪些常見的應用?
1.阻塞隊列
1)在隊列的基礎上增加阻塞操作,就成了阻塞隊列。
2)阻塞隊列就是在隊列為空的時候,從隊頭取數據會被阻塞,因為此時還沒有數據可取,直到隊列中有了數據才能返回;如果隊列已經滿了,那么插入數據的操作就會被阻塞,直到隊列中有空閑位置后再插入數據,然后在返回。
3)從上面的定義可以看出這就是一個“生產者-消費者模型”。這種基于阻塞隊列實現的“生產者-消費者模型”可以有效地協調生產和消費的速度。當“生產者”生產數據的速度過快,“消費者”來不及消費時,存儲數據的隊列很快就會滿了,這時生產者就阻塞等待,直到“消費者”消費了數據,“生產者”才會被喚醒繼續生產。不僅如此,基于阻塞隊列,我們還可以通過協調“生產者”和“消費者”的個數,來提高數據處理效率,比如配置幾個消費者,來應對一個生產者。
2.并發隊列
1)在多線程的情況下,會有多個線程同時操作隊列,這時就會存在線程安全問題。能夠有效解決線程安全問題的隊列就稱為并發隊列。
2)并發隊列簡單的實現就是在enqueue()、dequeue()方法上加鎖,但是鎖粒度大并發度會比較低,同一時刻僅允許一個存或取操作。
3)實際上,基于數組的循環隊列利用CAS原子操作,可以實現非常高效的并發隊列。這也是循環隊列比鏈式隊列應用更加廣泛的原因。
3.線程池資源枯竭是的處理
在資源有限的場景,當沒有空閑資源時,基本上都可以通過“隊列”這種數據結構來實現請求排隊。
[劍指offer][JAVA]面試題第[09]題[用兩個棧實現隊列][LinkedList]
主要整理參考作者:姜威
筆記整理來源: 王爭 數據結構與算法之美
總結
以上是生活随笔為你收集整理的【数据结构与算法】栈与队列的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: npm的下载与安装
- 下一篇: [密码学基础][每个信息安全博士生应该知