上一章咱們講到了棧,此次咱們來說隊列。其實隊列和棧有不少類似的地方,好比它們都是線性表,操做都是受限。區別也是比較明顯,隊列主要是先進先出,和排隊同樣,可是棧是先進後出。隊列的先進先出這兩個操做對應的是入隊(enqueue)和出隊(dequeue),入隊是從隊尾插入一個元素,出隊是從隊頭使一個元素出隊。算法
隊列的概念很好理解,基本操做也很容易掌握。做爲一種很是基礎的數據結構,隊列的應用也很是普遍,特別是一些具備某些額外特性的隊列,好比循環隊列、阻塞隊列、併發隊列。它們在不少偏底層系統、框架、中間件的開發中,起着關鍵性的做用。好比高性能隊列 Disruptor、Linux 環形緩存,都用到了循環併發隊列;Java concurrent 併發包利用 ArrayBlockingQueue 來實現公平鎖等。設計模式
隊列和棧同樣,能夠經過數組和棧來實現。若是是數組實現的就叫順序隊列,鏈表實現的叫鏈式隊列。下面經過JavaScript代碼來分別實現鏈式隊列和順序隊列:數組
class Node {
constructor(element) {
this.element = element
this.next = null
}
}
class linkedList {
constructor(length) {
this.head = null
this.length = length // 聲明隊列長度
this.currLength = 0 // 隊列實際長度
}
enqueue (item) {
let curr = this.head
let newNode = new Node(item)
if (curr == null) {
this.head = newNode
curr = this.head
}
else {
++this.currLength
if (this.currLength < this.length) {
while (curr) {
if (curr.next) {
curr = curr.next
}
else {
curr.next = newNode
break
}
}
}
else {
return false
}
}
}
dequeue () {
// 隊列爲空的狀況下直接返回null
let curr = this.head
if (this.head != null) {
let next = this.head.next
this.head = next
--this.currLength
}
return curr
}
}複製代碼
這段鏈式隊列的代碼很好理解。在入隊的時候將結點不斷的插入到鏈表的最後一個結點的next結點裏,出隊的時候直接將頭結點拿出來,並賦值給下一個結點。而順序鏈表的操做上則要有兩個指針,一個頭指針一個尾指針,每次出隊操做都會將數組往前移一位,代碼以下:緩存
class ArrayLisk {
constructor (length) {
this.queue = new Array(length)
this.head = 0
this.tail = 0
this.currLength = 0
}
enqueue (item) {
++this.currLength
if (this.currLength > this.queue.length) {
return false
}
++this.tail
this.queue[this.currLength - 1] = item
}
dequeue () {
if (this.head === this.tail) {
return null
}
else {
let current = this.queue[0]
for (let a = 0; a < this.queue.length - 1; a++) {
this.queue[a] = this.queue[a + 1]
if (a === this.queue.length - 2) {
this.queue[a + 1] = undefined
}
}
--this.currLength
this.tail = this.currLength
return current
}
}
}複製代碼
上面講到一些特殊隊列有某些額外特性,如循環隊列、阻塞隊列、併發隊列等等,下面咱們主要來看一下這三個特殊隊列的具體用途。安全
首先講的是循環隊列。在上面的代碼中,若是是用數組來實現一個隊列的話,當進行出隊操做的時候,數組裏的元素都會往前移一位,元素的移動會形成須要更多的時間,這個時候咱們就能夠用一個循環隊列來解決這個問題。循環隊列,顧名思義,它長得像一個環。本來數組是有頭有尾的,是一條直線。如今咱們把首尾相連,扳成了一個環。以下圖:bash
咱們能夠看到,圖中這個隊列的大小爲 10,當前 front=1,rear=·10。當有一個新的元素 a 入隊時,咱們放入下標爲 10 的位置。但這個時候,咱們並不把 tail 更新爲 11,而是將其在環中後移一位,到下標爲 1 的位置。當再有一個元素 b 入隊時,咱們將 b 放入下標爲 1 的位置,而後 tail 加 1 更新爲 2。經過這樣的方法,咱們成功避免了數據搬移操做。看起來不難理解,可是循環隊列的代碼實現難度要比前面講的非循環隊列難多了。要想寫出沒有 bug 的循環隊列的實現代碼,我我的以爲,最關鍵的是,肯定好隊空和隊滿的斷定條件。數據結構
在用數組實現的非循環隊列中,隊滿的判斷條件是 tail == n,隊空的判斷條件是 head == tail。那針對循環隊列,如何判斷隊空和隊滿呢?隊列爲空的判斷條件仍然是 head == tail且當前head下標對應的值爲空。但隊列滿的判斷條件就稍微有點複雜了,當隊滿時,(tail+1)%n=head+1且當前tail下標對應的值不爲空。下面用代碼來實現一下:多線程
class ArrayLisk {
constructor (length) {
this.queue = new Array(length)
this.head = 0
this.tail = 0
}
enqueue (item) {
if ((this.tail + 1) % this.queue.length === this.head + 1 && this.queue[this.tail] !== undefined) {
return false
}
this.queue[this.tail] = item
if (this.tail === this.queue.length - 1) {
this.tail = 0
}
else {
++this.tail
}
}
dequeue () {
if (this.head === this.tail && this.queue[this.head] === undefined) {
return false
}
this.queue[this.head] = undefined
if (this.head === this.queue.length - 1) {
this.head = 0
}
else {
++this.head
}
}
}複製代碼
前面講的內容理論比較多,看起來很難跟實際的項目開發扯上關係。確實,隊列這種數據結構很基礎,平時的業務開發不大可能從零實現一個隊列,甚至都不會直接用到。而一些具備特殊特性的隊列應用卻比較普遍,好比阻塞隊列和併發隊列。阻塞隊列其實就是在隊列基礎上增長了阻塞操做。簡單來講,就是在隊列爲空的時候,從隊頭取數據會被阻塞。由於此時尚未數據可取,直到隊列中有了數據才能返回;若是隊列已經滿了,那麼插入數據的操做就會被阻塞,直到隊列中有空閒位置後再插入數據,而後再返回。併發
/**
* @description 這裏用JavaScript單線程同步狀態來模擬了一個阻塞隊列,
* 當入隊請求遠大於出隊請求而且隊列已滿的狀況下記錄入隊數據,
* 下一次出隊操做的時候將從溢出的入隊數據中讀取。
* 當出隊請求遠大於入隊請求而且隊列爲空的狀況下記錄出隊數量,
* 下一次入隊操做的時候將把這數據返回並減小出隊數量。
* */
class BlockingQueue extends Array{
constructor(args, size) {
super(...args)
this.sizeLength = args.length // 數組初始長度
this.maxSize = size // 數組限定最長長度
this.pushArr = [] // 溢出的入隊數據
this.popSum = 0 // 溢出的出隊請求數量
}
enqueue(val) {
if (this.maxSize === this.sizeLength) {
this.pushArr.push(val)
return false
}
if (this.popSum > 0) {
--this.popSum
return val
}
++this.sizeLength
return super.push(val)
}
dequeue() {
if (this.pushArr.length > 0) {
super.push(this.pushArr.shift())
return super.shift()
}
if (this.sizeLength === 0) {
++this.popSum
return false
}
--this.sizeLength
return super.shift()
}
}複製代碼
這裏的代碼按照上面的狀況下作了一點修改,當生產者生產的數據遠遠多於消費者消費的數據的時候,咱們這個時候就要阻塞生產者讓消費者可以及時的處理完數據。同理,反過來講若是消費者消費的數據遠遠多於生產者生產的數據的時候,就阻塞消費者讓生產者可以及時的生產數據。這個模式就是設計模式裏生產者消費者模式。框架
上面的例子裏能夠改造一下,好比Java裏的DelayQueue,就是加了個延時器當過了一段時間纔開始生產數據或者消費數據。
前面咱們講了阻塞隊列,在多線程狀況下,會有多個線程同時操做隊列,這個時候就會存在線程安全問題,那如何實現一個線程安全的隊列呢?
線程安全的隊列咱們叫做併發隊列。。最簡單直接的實現方式是直接在 enqueue()、dequeue() 方法上加鎖,可是鎖粒度大併發度會比較低,同一時刻僅容許一個存或者取操做。實際上,基於數組的循環隊列,利用 CAS 原子操做,能夠實現很是高效的併發隊列。這也是循環隊列比鏈式隊列應用更加普遍的緣由。因爲JavaScript是單線程的,因此模擬Java的多線程的時候通常都是採用異步來實現,這裏因爲博主水平不夠,目前暫時不會用異步的方式實現一個線程安全的併發隊列,因此就不貼代碼了,感興趣的能夠看Java是如何實現的。
上一篇文章:數據結構與算法的重溫之旅(六)——棧
下一篇文章:數據結構與算法的重溫之旅(八)——遞歸