數據結構-02 |棧 |隊列| 雙端隊列| 優先隊列

 

 棧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)
  }

}
View Code

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()
  }

}
View Code

數組模擬環形隊列

  • 對前面的數組模擬隊列的優化,充分利用數組. 因此將數組看做是一個環形的。(通過取模的方
    式來實現即可)
  • 分析說明(思路):

  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
  }

}
View Code

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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章