棧Stack |隊列Queue| 雙端隊列Deque| 優先隊列PriorityQueue
堆棧和隊列特點: 1. Stack - First In Last Out(FILO) 先入後出,先進來的被壓入棧底 .Array or Linked List 2. Queue - First In First Out(FIFO) 排隊時先來先出 .Array or Doubly Linked List
1. 棧Stack
Stack中文名可叫堆棧,不能叫堆,堆是heap
手寫棧堆比較少了,很多語言在標準庫都實現了。
1.1 概念與特性
棧 Stack -先入後出的容器結構
棧:查詢 O(n),平均情況,要看它棧中棧底的元素,要清空了才能看到。
插入和刪除它的棧頂元素只需要一次性操作,時間複雜度是O(1)。
1.2 如何實現一個“棧”?
從剛纔棧的定義裏,我們可以看出,棧主要包含兩個操作,入棧和出棧,也就是在棧頂插入一個數據和從棧頂刪除一個數據。理解了棧的定義之後,我們來看一看如何用代碼實現一個棧。
實際上,棧既可以用數組來實現,也可以用鏈表來實現。
用數組實現的棧,叫作順序棧,用鏈表實現的棧,叫作鏈式棧。
// 基於數組實現的順序棧
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;
}
}
不管是順序棧還是鏈式棧,我們存儲數據只需要一個大小爲 n 的數組就夠了。在入棧和出棧過程中,只需要一兩個臨時變量存儲空間,所以空間複雜度是 O(1)。
注意,這裏存儲數據需要一個大小爲 n 的數組,並不是說空間複雜度就是 O(n)。因爲,這 n 個空間是必須的,無法省掉。所以我們說空間複雜度的時候,是指除了原本的數據存儲空間外,算法運行還需要額外的存儲空間。
空間複雜度分析是不是很簡單?時間複雜度也不難。不管是順序棧還是鏈式棧,入棧、出棧只涉及棧頂個別數據的操作,所以時間複雜度都是 O(1)。
1.3 棧的應用
如何基於棧實現瀏覽器的前進和後退功能?
當你依次訪問完一串頁面 a-b-c 之後,點擊瀏覽器的後退按鈕,就可以查看之前瀏覽過的頁面 b 和 a。當你後退到頁面 a,點擊前進按鈕,就可以重新查看頁面 b 和 c。但是,如果你後退到頁面 b 後,點擊了新的頁面 d,那就無法再通過前進、後退功能查看頁面 c 了。
如何實現瀏覽器的前進、後退功能?其實,用兩個棧就可以非常完美地解決這個問題。
我們使用兩個棧,X 和 Y,我們把首次瀏覽的頁面依次壓入棧 X,當點擊後退按鈕時,再依次從棧 X 中出棧,並將出棧的數據依次放入棧 Y。當我們點擊前進按鈕時,我們依次從棧 Y 中取出數據,放入棧 X 中。當棧 X 中沒有數據時,那就說明沒有頁面可以繼續後退瀏覽了。當棧 Y 中沒有數據,那就說明沒有頁面可以點擊前進按鈕瀏覽了。
後進者先出,先進者後出,這就是典型的“棧”結構。
從棧的操作特性上來看,棧是一種“操作受限”的線性表,只允許在一端插入和刪除數據。
事實上,從功能上來說,數組或鏈表確實可以替代棧,但你要知道,特定的數據結構是對特定場景的抽象,而且,數組或鏈表暴露了太多的操作接口,操作上的確靈活自由,但使用時就比較不可控,自然也就更容易出錯。
當某個數據集合只涉及在一端插入和刪除數據,並且滿足後進先出、先進後出的特性,我們就應該首選“棧”這種數據結構。
① 棧在函數調用中的應用,經典的一個應用場景就是函數調用棧。
操作系統給每個線程分配了一塊獨立的內存空間,這塊內存被組織成“棧”這種結構, 用來存儲函數調用時的臨時變量。每進入一個函數,就會將臨時變量作爲一個棧幀入棧,當被調用函數執行完成,返回之後,將這個函數對應的棧幀出棧。
② 棧在表達式求值中的應用
我們再來看棧的另一個常見的應用場景,編譯器如何利用棧來實現表達式求值。
爲了方便解釋,我將算術表達式簡化爲只包含加減乘除四則運算,比如:34+13*9+44-12/3。
③ 棧在括號匹配中的應用
假設表達式中只包含三種括號,圓括號 ()、方括號 [] 和花括號{},並且它們可以任意嵌套。比如,{[{}]}或 [{()}([])] 等都爲合法格式,而{[}()] 或 [({)] 爲不合法的格式。那我現在給你一個包含三種括號的表達式字符串,如何檢查它是否合法呢?
這裏也可以用棧來解決。我們用棧來保存未匹配的左括號,從左到右依次掃描字符串。當掃描到左括號時,則將其壓入棧中;當掃描到右括號時,從棧頂取出一個左括號。如果能夠匹配,比如“(”跟“)”匹配,“[”跟“]”匹配,“{”跟“}”匹配,則繼續掃描剩下的字符串。如果掃描的過程中,遇到不能配對的右括號,或者棧中沒有數據,則說明爲非法格式。
當所有的括號都掃描完成之後,如果棧爲空,則說明字符串爲合法格式;否則,說明有未匹配的左括號,爲非法格式。
思考
內存中的堆棧和數據結構堆棧不是一個概念,可以說內存中的堆棧是真實存在的物理區,數據結構中的堆棧是抽象的數據存儲結構。
內存空間在邏輯上分爲三部分:代碼區、靜態數據區和動態數據區,動態數據區又分爲棧區和堆區。
代碼區:存儲方法體的二進制代碼。高級調度(作業調度)、中級調度(內存調度)、低級調度(進程調度)控制代碼區執行代碼的切換。
靜態數據區:存儲全局變量、靜態變量、常量,常量包括final修飾的常量和String常量。系統自動分配和回收。
棧區:存儲運行方法的形參、局部變量、返回值。由系統自動分配和回收。
堆區:new一個對象的引用或地址存儲在棧區,指向該對象存儲在堆區中的真實數據。
棧的應用場景
1) 子程序的調用:在跳往子程序前,會先將下個指令的地址存到堆棧中,直到子程序執行完後再將地址取出,以回到原來的程序中。
2) 處理遞歸調用:和子程序的調用類似,只是除了儲存下一個指令的地址外,也將參數、區域變量等數據存入堆棧中。
3) 表達式的轉換與求值(實際解決)。
4) 二叉樹的遍歷。
5) 圖形的深度優先(depth一first)搜索法。
1) 用數組模擬棧的使用,由於棧是一種有序列表,當然可以使用數組的結構來儲存棧的數據內容,
下面我們就用數組模擬棧的出棧,入棧等操作。
2) 實現思路分析,並畫出示意圖
代碼實現:
import scala.io.StdIn object ArrayStackDemo { def main(args: Array[String]): Unit = { //測試棧 val stack = new ArrayStack(4) var key = "" while (true) { println("list: 顯示棧") println("push: 入棧") println("pop: 出棧") println("peek: 查看棧頂") key = StdIn.readLine() key match { case "list" => stack.list() case "push" => println("請輸入一個數") val value = StdIn.readInt() stack.push(value) case "pop" => val res = stack.pop() if(res.isInstanceOf[Exception]) { println(res.asInstanceOf[Exception].getMessage) }else{ printf("取出棧頂的元素是%d", res) } case "peek" => val res = stack.peek() if(res.isInstanceOf[Exception]) { println(res.asInstanceOf[Exception].getMessage) }else{ printf("棧頂的元素是%d", res) } //默認處理 } } } } //ArrayStack 表示棧 class ArrayStack(arrMaxSize:Int) { var top = -1 val maxSize = arrMaxSize val arr = new Array[Int](maxSize) //判斷棧空 def isEmpty(): Boolean = { top == -1 } //棧滿 def isFull(): Boolean = { top == maxSize - 1 } //入棧操作 def push(num:Int): Unit = { if(isFull()) { println("棧滿,不能加入") return } top += 1 arr(top) = num } //出棧操作 def pop(): Any = { if(isEmpty()) { return new Exception("棧空,沒有數據") } val res = arr(top) top -= 1 return res } //遍歷棧 def list(): Unit = { if(isEmpty()) { println("棧空") return } //使用for for(i <- 0 to top reverse) { //逆序打印 printf("arr(%d)=%d\n", i, arr(i)) } println() } //查看棧的元素是什麼,但是不會真的把棧頂的數據彈出 def peek(): Any = { if(isEmpty()) { return new Exception("棧空") } return arr(top) } }
2. 隊列
2.1 概念和特性
隊列,排隊,先來先出,依次排隊。
棧:查詢 O(n),平均情況,要看它棧中棧底的元素,要清空了才能看到。
插入和刪除它的棧頂元素只需要一次性操作,時間複雜度是O(1)。
隊列:與棧類似, 查詢是O(n),插入和刪除是O(1)。
查詢操作O(n),因爲它是元素無序的 ,就必須把這個數據結構遍歷一遍
把它想象成排隊買票,先來的先買,後來的人只能站末尾,不允許插隊。先進者先出,這就是典型的“隊列”。
我們知道,棧只支持兩個基本操作:入棧 push()和出棧 pop()。
隊列跟棧非常相似,支持的操作也很有限,最基本的操作也是兩個:
入隊 enqueue(),放一個數據到隊列尾部;出隊 dequeue(),從隊列頭部取一個元素。
所以,隊列跟棧一樣,也是一種操作受限的線性表數據結構。
作爲一種非常基礎的數據結構,隊列的應用也非常廣泛,特別是一些具有某些額外特性的隊列,比如循環隊列、阻塞隊列、併發隊列。它們在很多偏底層系統、框架、中間件的開發中,起着關鍵性的作用。比如高性能隊列 Disruptor、Linux 環形緩存,都用到了循環併發隊列;Java concurrent 併發包利用 ArrayBlockingQueue 來實現公平鎖等。
2.2 順序隊列和鏈式隊列
隊列跟棧一樣,也是一種抽象的數據結構。它具有先進先出的特性,支持在隊尾插入元素,在隊頭刪除元素,那究竟該如何實現一個隊列呢?
跟棧一樣,隊列可以用數組來實現,也可以用鏈表來實現。用數組實現的棧叫作順序棧,用鏈表實現的棧叫作鏈式棧。
同樣,用數組實現的隊列叫作順序隊列,用鏈表實現的隊列叫作鏈式隊列。
1) 隊列是一個有序列表,可以用數組或是鏈表來實現。數據結構就是研究數據組織形式,並且爲算法打基礎。
2) 遵循先入先出的原則。即:先存入隊列的數據,要先取出。後存入的要後取出
3) 示意圖:(使用數組模擬隊列示意圖)
4) 線性結構,一對一關係
- 隊列本身是有序列表,若使用數組的結構來存儲隊列的數據,則隊列數組的聲明如下 其中 maxSize 是該隊列的最大容量。
- 因爲隊列的輸出、輸入是分別從前後端來處理,因此需要兩個變量 front及 rear分別記錄隊列前後端的下標,front 會隨着數據輸出而改變,而 rear則是隨着數據輸入而改變,如圖所示:
數組模擬隊列
rear 表示隊列尾,rear 指向隊列的最後一個元素
front 表示隊列的頭,表示指向隊列的第一個元素的前一個位置
- 當我們將數據存入隊列時稱爲”addQueue”,addQueue 的處理需要有兩個步驟:思路分析
1) 將尾指針往後移:rear+1 , front == rear 【隊列空】
2) 若尾指引 rear 小於隊列的最大下標 MaxSize-1,則將數據存入 rear所指的數組元素中,否則無法存入數據。 rear == MaxSize - 1[隊列滿]
代碼實現:
object ArrayQueueDemo { def main(args: Array[String]): Unit = { //創建一個隊列對象 val queue = new ArrayQueue(3) var key = "" //接收用戶的輸入 //簡單寫個菜單 while (true) { println("show : 顯示隊列") println("add: 添加數據到隊列") println("get: 從隊列頭取出元素") println("peek: 查看隊列頭元素") key = StdIn.readLine() key match { case "show" => queue.showQueue() case "add" => println("請輸入一個數吧") val value = StdIn.readInt() queue.addQueue(value) case "get" => val res = queue.getQueue()//取出 //判斷res 的類型 if(res.isInstanceOf[Exception]) { //輸出異常信息 println(res.asInstanceOf[Exception].getMessage) }else { printf("取出的隊列頭元素是%d", res) } case "peek" => val res = queue.peek()//查看 //判斷res 的類型 if(res.isInstanceOf[Exception]) { //輸出異常信息 println(res.asInstanceOf[Exception].getMessage) }else { printf("隊列頭元素是%d", res) } } } } } //定義一個類ArrayQueue 表隊列 //該類會實現隊列的相關方法 //數據結構 創建-遍歷-測試-修改-刪除 class ArrayQueue(arrMaxSize: Int) { val maxSize = arrMaxSize val arr = new Array[Int](maxSize) //隊列的數據存放的數組 var front = -1 // 表示指向隊列的第一個元素的前一個位置 var rear = -1 //表示指向隊列的最後個元素 //查看隊列的頭元素,但是不取出 def peek(): Any = { if(isEmpty()) { return new Exception("隊列空,沒有數據返回"); } return arr(front + 1) } //判斷隊列是否滿 def isFull(): Boolean = { rear == maxSize - 1 } //判斷隊列是空 def isEmpty(): Boolean = { rear == front } //從隊列中取出數據 //異常時可以加入業務邏輯 def getQueue(): Any = { if(isEmpty()) { return new Exception("隊列空,沒有數據返回"); } //將front 後移一位 front += 1 return arr(front) } //給隊列添加數據 def addQueue(num: Int): Unit = { if (isFull()) { println("隊列滿,不能加入!!") return } //添加時調整rear //1. 先將rear 後移 rear += 1 arr(rear) = num } //遍歷隊列 def showQueue(): Unit = { if (isEmpty()) { println("隊列空!!") return } for (i <- (front + 1) to rear) { printf("arr(%d)=%d\t", i, arr(i)) } println() } }
數組模擬環形隊列
- 對前面的數組模擬隊列的優化,充分利用數組. 因此將數組看做是一個環形的。(通過取模的方
式來實現即可)
- 分析說明(思路):
1) 尾索引的下一個爲頭索引時表示隊列滿,即將隊列容量空出一個作爲約定,這個在做判斷隊列滿的時候需要注意 (rear + 1) % maxSize == front 滿] rear == front [空]
2) 測試示意圖:
3) 代碼實現
object CircleArrayQueueDemo { def main(args: Array[String]): Unit = { //創建一個隊列對象 val queue = new CircleArrayQueue(4) var key = "" //接收用戶的輸入 //簡單寫個菜單 while (true) { println("show : 顯示隊列") println("add: 添加數據到隊列") println("get: 從隊列頭取出元素") println("peek: 查看隊列頭元素") key = StdIn.readLine() key match { case "show" => queue.showQueue() case "add" => println("請輸入一個數吧") val value = StdIn.readInt() queue.addQueue(value) case "get" => val res = queue.getQueue()//取出 //判斷res 的類型 if(res.isInstanceOf[Exception]) { //輸出異常信息 println(res.asInstanceOf[Exception].getMessage) }else { printf("取出的隊列頭元素是%d", res) } case "peek" => val res = queue.peek()//查看 //判斷res 的類型 if(res.isInstanceOf[Exception]) { //輸出異常信息 println(res.asInstanceOf[Exception].getMessage) }else { printf("隊列頭元素是%d", res) } } } } } //定義一個類CircleArrayQueue 表環形隊列 //該類會實現隊列的相關方法 //數據結構 創建-遍歷-測試--刪除 class CircleArrayQueue(arrMaxSize: Int) { val maxSize = arrMaxSize val arr = new Array[Int](maxSize) //隊列的數據存放的數組 var front = 0 // front 初始化 = 0 , front 約定 指向隊列的頭元素 var rear = 0 // rear 初始化爲 = 0, rear 指向隊列的 最後元素的後一個位置, 因爲需要空出一個位置做約定 //判斷隊列是否滿 def isFull(): Boolean = { (rear + 1) % maxSize == front } //判斷隊列是空,和前面一樣 def isEmpty(): Boolean = { rear == front } //從隊列中取出數據 //異常時可以加入業務邏輯 def getQueue(): Any = { if(isEmpty()) { return new Exception("隊列空,沒有數據返回"); } //返回front指向的值 //1. 先把 arr(front) 保存到一個臨時變量 //2. front += 1 //3. 返回臨時變量 var temp = arr(front) front = (front + 1) % maxSize return temp } //查看隊列的頭元素,但是不取出 def peek(): Any = { if(isEmpty()) { return new Exception("隊列空,沒有數據返回"); } return arr(front) } //給隊列添加數據 def addQueue(num: Int): Unit = { if (isFull()) { println("隊列滿,不能加入!!") return } //先把數據放入到arr(rear) 位置,然後後移rear arr(rear) = num rear = (rear + 1) % maxSize } //遍歷隊列 def showQueue(): Unit = { if (isEmpty()) { println("隊列空!!") return } //動腦筋 //1. 從front 開始打印 , 打印幾個元素 for (i <- front until (front + size()) ) { printf("arr(%d)=%d\t", (i % maxSize) , arr(i % maxSize)) } println() } //求出當前隊列共有多少個數據 def size(): Int = { (rear + maxSize - front) % maxSize } }
2.3 隊列的應用
隊列在線程池等有限資源池中的應用
CPU 資源是有限的,任務的處理速度與線程個數並不是線性正相關。相反,過多的線程反而會導致 CPU 頻繁切換,處理性能下降。所以,線程池的大小一般都是綜合考慮要處理任務的特點和硬件環境,來事先設置的。
當我們向固定大小的線程池中請求一個線程時,如果線程池中沒有空閒資源了,這個時候線程池如何處理這個請求?是拒絕請求還是排隊請求?各種處理策略又是怎麼實現的呢?
3. 雙端隊列 Deque: Double -End- Queue
可理解爲一個queue和stack的結合體,一個簡單的隊列,但它的頭 和 尾都可以進行元素的出和進
插入和刪除都是 O(1) 操作,查詢是O(n) ;
4. 優先隊列 PriorityQueue
插入O(1)
取出 O(logN) -- 按照元素的優先級取出;(取出相比棧和隊列變慢了,好處是它的順序不再是FIFO或者FILO,而是按照元素的優先級取出,VIP先行)
底層具體實現的數據結構較爲多樣和複雜:堆heap、 二叉搜索樹bst、 treap