數據結構與算法的重溫之旅(七)——隊列

   上一章咱們講到了棧,此次咱們來說隊列。其實隊列和棧有不少類似的地方,好比它們都是線性表,操做都是受限。區別也是比較明顯,隊列主要是先進先出,和排隊同樣,可是棧是先進後出。隊列的先進先出這兩個操做對應的是入隊(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是如何實現的。


上一篇文章:數據結構與算法的重溫之旅(六)——棧​​​​​​​

下一篇文章:數據結構與算法的重溫之旅(八)——遞歸​​​​​​​ 

相關文章
相關標籤/搜索