本片部落格參考到銀國徽老師的《Js版資料結構與演算法》
佇列
佇列的定義
相信通過上篇部落格大家對棧有了一個大概的認識,今天我們開始對佇列的學習。
我們還是先看一下百度百科上對佇列的定義:
說白了就是佇列是一種受限的線性表,''受限''體現在只能從表的前端(隊頭)進行刪除,只能從表的後端(隊尾)進行插入。接下來我還是用一個比喻更形象的說明一下我對佇列的理解。
佇列的理解
相信大家平時都會路過一些網紅店如‘喜茶’,‘一點點’,‘BONUS’(僱人排隊的行為不值得提倡?),每次路過時都是門庭若市,我家樓下的也有一家烤腸店,每到週末都會排起很長的隊伍。
圖 1
這個時候排隊的大家就形成了一個佇列
圖片中最前面的藍色衣服的大叔就是隊頭
在最後面拍照片的我呢就是隊尾了
這時候當大叔交完錢拿到自己的烤腸後就可以離開我們的'佇列'(稱為出隊,只能從隊頭離開)
如果隔壁家的小孩被饞哭了的話他也只能去我的後面開始排隊(稱為入隊,只能從隊尾入隊)
注意: 大叔是先去排隊的,所以他也是先拿到自己的烤腸離開的,小朋友是後來的,所以他也會後拿到自己的烤腸,這也就是我們常說的'先進先出'。
對比棧的後進先出,是不是還挺有趣的?
js實現一個簡單的順序佇列
首先我們要清楚的是佇列分為順序佇列和迴圈佇列,上邊的例子我們排隊買烤腸就是順序佇列,至於迴圈佇列我會在接下來的例題中進行說明,我們先實現一個簡單的順序佇列。
圖 2
佇列有兩個指標,分別為隊頭指標front和隊尾指標rear,
front指向隊頭元素,rear指向下一個入隊元素的儲存位置。(如圖2)
下方的ArrayQuene函式就實現一個簡單功能的順序佇列:
function ArrayQueue(){
var arr = [];
//入隊操作
this.push = function(element){
arr.push(element);
return true;
}
//出隊操作
this.pop = function(){
return arr.shift();
}
//獲取隊首
this.getFront = function(){
return arr[0];
}
//獲取隊尾
this.getRear = function(){
return arr[arr.length - 1]
}
//清空佇列
this.clear = function(){
arr = [];
}
//獲取隊長
this.size = function(){
return length;
}
} 複製程式碼
例題:設計迴圈佇列
迴圈佇列理解
在實際使用佇列時,為了使佇列空間能重複使用,往往對佇列的使用方法稍加改進:無論插入或刪除,一旦rear指標增1或front指標增1時超出了所分配的佇列空間,就讓它指向這片連續空間的起始位置。
這實際上是把佇列空間想象成一個環形空間,環形空間中的儲存單元迴圈使用,用這種方法管理的佇列也就稱為迴圈佇列。除了一些簡單應用之外,真正實用的佇列是迴圈佇列。
在上文中提到過順序佇列中有兩個指標,front指向隊頭元素,rear指標指向下一個入隊的元素的儲存位置。結合下面這張圖我們分析一下迴圈佇列在不同情況下front指標和rear指標的指向。
圖 3
- (a)在初始空隊時,front和rear指標同時指向隊頭初始的位置(即0的位置,佇列長度為6)
- (b)a,b,c三個元素入隊時,rear指標會隨著新增元素的個數移動到相應的位置(三個元素,向前移動1位,此時rear由0移動到3)
- (c)a出隊,front指標也會隨著刪除元素的個數移動到相應的位置(刪除1個元素,向前移動1位,此時front由0移動到1)
- (d)d,e,f,g入隊,rear向前移動4位,由3移動到1,此時佇列空間全部佔滿,rear和front同時指向1
- (e)g出隊(由於g處於隊頭的位置,所以可以出隊),front向前移動1位,由0移動到1
相信大家對迴圈佇列有了一個初步的認識,話不多說,我們直接上例題
例題
原題地址(LeetCode 622題)
設計你的迴圈佇列實現。 迴圈佇列是一種線性資料結構,其操作表現基於 FIFO(先進先出)原則並且隊尾被連線在隊首之後以形成一個迴圈。它也被稱為“環形緩衝器”。
迴圈佇列的一個好處是我們可以利用這個佇列之前用過的空間。在一個普通佇列裡,一旦一個佇列滿了,我們就不能插入下一個元素,即使在佇列前面仍有空間。但是使用迴圈佇列,我們能使用這些空間去儲存新的值。
你的實現應該支援如下操作:
MyCircularQueue(k)
: 構造器,設定佇列長度為 k 。Front
: 從隊首獲取元素。如果佇列為空,返回 -1 。Rear
: 獲取隊尾元素。如果佇列為空,返回 -1 。enQueue(value)
: 向迴圈佇列插入一個元素。如果成功插入則返回真。deQueue()
: 從迴圈佇列中刪除一個元素。如果成功刪除則返回真。isEmpty()
: 檢查迴圈佇列是否為空。isFull()
: 檢查迴圈佇列是否已滿。
題目不是很難,我們簡單的分析一下題目裡面需要我們實現的操作:
- 建立一個長度為k的陣列arr,並宣告front和rear指標,初始值都設定為0,當插入或刪除元素時我們需要手動操作指標的位置。
- 隊首元素直接返回arr[front]即可。
- 如果rear不指向0時,隊尾元素是arr[rear-1],如果rear指向0的時候隊尾元素就是arr[k-1]。
- 插入元素,直接將插入元素賦值給arr[rear],注意要先判斷下佇列是否是滿的狀態。
- 刪除元素,將元素arr[front]賦值為空,注意要先判斷一下佇列是否是空的狀態。
- 檢查佇列是否為空,當front和rear的指向相同且隊首元素也為空時,佇列為空。
- 檢查佇列是否已滿,當front和rear的指向相同且隊首元素有值時,佇列已滿。
程式碼如下:
class MyCircularQueue{
// 構造器
constructor (k) {
// 建立長度為k的迴圈佇列
this.arr= Array(k)
// 宣告隊首指標front
this.front = 0
// 宣告隊尾指標rear
this.rear = 0
}
// 獲取隊首元素
Front () {
// 如果佇列為空,返回-1 否則直接返回front指向的值
return this.isEmpty()? -1 : this.arr[this.front]
}
// 獲取隊尾元素
Rear () {
// 如果此時rear指標指向0的位置,返回佇列最後一位,否則返回下一位
let rearItem = rear -1
return this.arr[rearItem < 0 ? k-1 : rearItem]
}
// 插入元素
enQuene (value) {
// 先判斷佇列是否是已滿狀態
if(this.isFull()){
return false
}else{
// 插入元素
this.arr[this.rear] = value
// 移動rear指標位置,考慮到迴圈佇列指標的變動,用取模的方式
this.rear = (this.rear+1) % k
return true
}
}
// 刪除元素
deQuene () {
// 先判斷佇列是否為空
if(this.isEmpty()){
return false
}else{
let deleteItem = this.arr[this.front]
this.arr[this.front] = ''
this.front = (this.front+1) % k
return deleteItem
}
}
// 判斷佇列是否為空
isEmpty () {
// 頭尾指標指向同一個地址並且隊首元素為空
retutn this.rear === this.front && !this.arr[this.front]
}
// 判斷佇列是否已滿
isFull () {
// 頭尾指標指向同一個地址並且隊首元素不為空
return this.rear === this.front && !!this.arr[this.front]
}
}複製程式碼
總結
棧和佇列是相對簡單的資料結構,相信通過兩篇部落格大家對這兩種資料結構都有了簡單的認識,接下來我將介紹相對複雜一些的資料結構。