數(shù)據(jù)結(jié)構(gòu)-隊(duì)列結(jié)構(gòu)

如何理解“隊(duì)列”逞带?

隊(duì)列這個(gè)概念非常好理解。你可以把它想象成排隊(duì)買(mǎi)票纱新,先來(lái)的先買(mǎi)展氓,后來(lái)的人只能站末尾,不允許插隊(duì)脸爱。先進(jìn)者先出遇汞,這就是典型的“隊(duì)列”。

我們知道簿废,棧只支持兩個(gè)基本操作:入棧 push()和出棧 pop()空入。隊(duì)列跟棧非常相似,支持的操作也很有限族檬,最基本的操作也是兩個(gè):入隊(duì) enqueue()歪赢,放一個(gè)數(shù)據(jù)到隊(duì)列尾部;出隊(duì) dequeue()单料,從隊(duì)列頭部取一個(gè)元素埋凯。

順序隊(duì)列和鏈?zhǔn)疥?duì)列

我們知道了,隊(duì)列跟棧一樣扫尖,也是一種抽象的數(shù)據(jù)結(jié)構(gòu)白对。它具有先進(jìn)先出的特性,支持在隊(duì)尾插入元素换怖,在隊(duì)頭刪除元素甩恼,那究竟該如何實(shí)現(xiàn)一個(gè)隊(duì)列呢?

跟棧一樣沉颂,隊(duì)列可以用數(shù)組來(lái)實(shí)現(xiàn)条摸,也可以用鏈表來(lái)實(shí)現(xiàn)。用數(shù)組實(shí)現(xiàn)的棧叫作順序棧兆览,用鏈表實(shí)現(xiàn)的棧叫作鏈?zhǔn)綏G取M瑯樱脭?shù)組實(shí)現(xiàn)的隊(duì)列叫作順序隊(duì)列抬探,用鏈表實(shí)現(xiàn)的隊(duì)列叫作鏈?zhǔn)疥?duì)列子巾。

我們先來(lái)看下基于數(shù)組的實(shí)現(xiàn)方法帆赢。

IQueue 接口定義

package com.s4.queue;

public interface IQueue {

    // 隊(duì)列的已用長(zhǎng)度
    public abstract int getLength();

    // 隊(duì)列的預(yù)設(shè)長(zhǎng)度
    public abstract int getPresetLength();
    
    // 入隊(duì)操作
    public abstract boolean enqueue(Object value);

    public abstract boolean isEmpty();

    public abstract boolean isFull();

    // 出隊(duì)操作,復(fù)雜度是 O(1)
    public abstract Object dequeue();

}

ArrayQueue 關(guān)鍵代碼

    /*
     * 入隊(duì)操作: 這個(gè)是優(yōu)化后的方案线梗。入隊(duì)操作的時(shí)間復(fù)雜度還是 O(1)
     */
    @Override
    public boolean enqueue(Object value) {
        if (this.isFull()) {
            if (this.head == 0) {
                return false;
            }
            // 進(jìn)行遷移[head, tail) 左移 head 位
            for (int i = this.head; i < this.tail; i++) {
                this.datas[i - this.head] = this.datas[i];
            }
            // 搬移完之后更新 head 和 tail
            this.tail -= this.head;
            this.head = 0;
        }
        this.datas[this.tail] = value;
        this.tail++;
        return true;
    }

    // 入隊(duì)操作: 這是一個(gè)錯(cuò)誤示例椰于,一個(gè)較差的方法
    public boolean enqueueBad(Object value) {
        if (this.isFull()) {
            return false;
        }
        this.datas[this.tail] = value;
        this.tail++;
        return true;
    }

    /*
     * 出隊(duì)操作,復(fù)雜度是 O(1)
     */
    @Override
    public Object dequeue() {
        if (this.isEmpty()) {
            return null;
        }
        Object ret = this.datas[this.head];
        this.head++;
        return ret;
    }

    // 每次出隊(duì)都會(huì)遷移仪搔,會(huì)增加時(shí)間復(fù)雜度為O(n)瘾婿,因此不推薦這么做
    @Deprecated
    public Object dequeue2() {
        if (this.isEmpty()) {
            return null;
        }
        // 每次進(jìn)行出隊(duì)操作都相當(dāng)于刪除數(shù)組下標(biāo)為 0 的數(shù)據(jù)
        Object ret = this.datas[0];
        // [1, tail -1] 整體左移一位 [1, tail) | [0, tail - 1)
        for (int i = 0; i < tail - 1; i++) {
            this.datas[i] = this.datas[i + 1];
        }
        this.tail--;
        this.datas[this.tail] = null;
        return ret;
    }

}

為了解決隨著不停地進(jìn)行入隊(duì)、出隊(duì)操作烤咧,head 和 tail 都會(huì)持續(xù)往后移動(dòng)偏陪。當(dāng) tail 移動(dòng)到最右邊,即使數(shù)組中還有空閑空間煮嫌,也無(wú)法繼續(xù)往隊(duì)列中添加數(shù)據(jù)了的問(wèn)題笛谦?

除了 dequeue2 這種思路外,可以在依舊保留出隊(duì)代碼不變的基礎(chǔ)上昌阿,在入隊(duì)的時(shí)候使用 enqueue 進(jìn)行數(shù)據(jù)遷移饥脑。

接下來(lái),我們?cè)賮?lái)看下基于鏈表的隊(duì)列實(shí)現(xiàn)方法懦冰。

基于鏈表的實(shí)現(xiàn)灶轰,我們同樣需要兩個(gè)指針:head 指針和 tail 指針。它們分別指向鏈表的第一個(gè)結(jié)點(diǎn)和最后一個(gè)結(jié)點(diǎn)刷钢。如圖所示笋颤,入隊(duì)時(shí),tail->next= new_node, tail = tail->next闯捎;出隊(duì)時(shí)椰弊,head = head->next。我將具體的代碼放到 GitHub 上瓤鼻,你可以自己試著實(shí)現(xiàn)一下,然后再去 GitHub 上跟我實(shí)現(xiàn)的代碼對(duì)比下贤重,看寫(xiě)得對(duì)不對(duì)茬祷。

LinkedQueue 關(guān)鍵代碼

    // 入隊(duì)操作
    public boolean enqueue(Object value) {
        if (this.isFull()) {
            return false;
        }
        Node node = new Node();
        node.data = value;
        node.next = null;
        if (this.isEmpty()) {
            this.head = node;
        } else {
            this.tail.next = node;
        }
        this.tail = node;
        this.length++;
        return true;
    }
    
    // 出隊(duì)操作,復(fù)雜度是 O(1)
    public Object dequeue() {
        if (this.isEmpty()) {
            return null;
        }
        Node nextNode = this.head.next;
        Object ret = this.head.data;
        this.head.data = null;
        this.head.next = null;
        this.head = nextNode;
        if (nextNode == null) {
            this.tail = null;
        }
        this.length--;
        return ret;
    }

循環(huán)隊(duì)列

我們剛才用數(shù)組來(lái)實(shí)現(xiàn)隊(duì)列的時(shí)候并蝗,在 tail==n 時(shí)祭犯,會(huì)有數(shù)據(jù)搬移操作,這樣入隊(duì)操作性能就會(huì)受到影響滚停。那有沒(méi)有辦法能夠避免數(shù)據(jù)搬移呢沃粗?我們來(lái)看看循環(huán)隊(duì)列的解決思路。

循環(huán)隊(duì)列键畴,顧名思義最盅,它長(zhǎng)得像一個(gè)環(huán)突雪。原本數(shù)組是有頭有尾的,是一條直線∥屑現(xiàn)在我們把首尾相連咏删,扳成了一個(gè)環(huán)。我畫(huà)了一張圖问词,你可以直觀地感受一下督函。

我們可以發(fā)現(xiàn),圖中這個(gè)隊(duì)列的大小為 8激挪,當(dāng)前 head=4辰狡,tail=7。當(dāng)有一個(gè)新的元素 a 入隊(duì)時(shí)垄分,我們放入下標(biāo)為 7 的位置宛篇。但這個(gè)時(shí)候,我們并不把 tail 更新為 8锋喜,而是將其在環(huán)中后移一位些己,到下標(biāo)為 0 的位置。當(dāng)再有一個(gè)元素 b 入隊(duì)時(shí)嘿般,我們將 b 放入下標(biāo)為 0 的位置段标,然后 tail 加 1 更新為 1。所以炉奴,在 a逼庞,b 依次入隊(duì)之后,循環(huán)隊(duì)列中的元素就變成了下面的樣子:

通過(guò)這樣的方法瞻赶,我們成功避免了數(shù)據(jù)搬移操作赛糟。看起來(lái)不難理解砸逊,但是循環(huán)隊(duì)列的代碼實(shí)現(xiàn)難度要比前面講的非循環(huán)隊(duì)列難多了璧南。要想寫(xiě)出沒(méi)有 bug 的循環(huán)隊(duì)列的實(shí)現(xiàn)代碼,我個(gè)人覺(jué)得师逸,最關(guān)鍵的是司倚,確定好隊(duì)空和隊(duì)滿的判定條件。

  • 隊(duì)列為空的判斷條件仍然是 head == tail
  • 當(dāng)隊(duì)滿時(shí)篓像,(tail+1)%n=head,
  • 當(dāng)隊(duì)滿時(shí)动知,圖中的 tail 指向的位置實(shí)際上是沒(méi)有存儲(chǔ)數(shù)據(jù)的。所以员辩,循環(huán)隊(duì)列會(huì)浪費(fèi)一個(gè)數(shù)組的存儲(chǔ)空間盒粮。

CircleQueue 關(guān)鍵代碼

    /*
     * 入隊(duì)
     */
    @Override
    public boolean enqueue(Object value) {
        if (this.isFull()) {
            return false;
        }
        this.datas[this.tail] = value;
        this.tail = indexPlus(this.tail);
        return true;
    }

    /*
     * 出隊(duì),復(fù)雜度是 O(1)
     */
    @Override
    public Object dequeue() {
        if (this.isEmpty()) {
            return null;
        }
        Object ret = this.datas[this.head];
        this.head = this.indexPlus(this.head);
        return ret;
    }   

阻塞隊(duì)列和并發(fā)隊(duì)列

前面講的內(nèi)容理論比較多奠滑,看起來(lái)很難跟實(shí)際的項(xiàng)目開(kāi)發(fā)扯上關(guān)系丹皱。確實(shí)妒穴,隊(duì)列這種數(shù)據(jù)結(jié)構(gòu)很基礎(chǔ),平時(shí)的業(yè)務(wù)開(kāi)發(fā)不大可能從零實(shí)現(xiàn)一個(gè)隊(duì)列种呐,甚至都不會(huì)直接用到宰翅。而一些具有特殊特性的隊(duì)列應(yīng)用卻比較廣泛,比如阻塞隊(duì)列和并發(fā)隊(duì)列爽室。

阻塞隊(duì)列其實(shí)就是在隊(duì)列基礎(chǔ)上增加了阻塞操作汁讼。簡(jiǎn)單來(lái)說(shuō),就是在隊(duì)列為空的時(shí)候阔墩,從隊(duì)頭取數(shù)據(jù)會(huì)被阻塞嘿架。因?yàn)榇藭r(shí)還沒(méi)有數(shù)據(jù)可取,直到隊(duì)列中有了數(shù)據(jù)才能返回啸箫;如果隊(duì)列已經(jīng)滿了耸彪,那么插入數(shù)據(jù)的操作就會(huì)被阻塞,直到隊(duì)列中有空閑位置后再插入數(shù)據(jù)忘苛,然后再返回蝉娜。

你應(yīng)該已經(jīng)發(fā)現(xiàn)了,上述的定義就是一個(gè)“生產(chǎn)者 - 消費(fèi)者模型”扎唾!是的召川,我們可以使用阻塞隊(duì)列,輕松實(shí)現(xiàn)一個(gè)“生產(chǎn)者 - 消費(fèi)者模型”胸遇!

這種基于阻塞隊(duì)列實(shí)現(xiàn)的“生產(chǎn)者 - 消費(fèi)者模型”荧呐,可以有效地協(xié)調(diào)生產(chǎn)和消費(fèi)的速度。當(dāng)“生產(chǎn)者”生產(chǎn)數(shù)據(jù)的速度過(guò)快纸镊,“消費(fèi)者”來(lái)不及消費(fèi)時(shí)倍阐,存儲(chǔ)數(shù)據(jù)的隊(duì)列很快就會(huì)滿了。這個(gè)時(shí)候逗威,生產(chǎn)者就阻塞等待峰搪,直到“消費(fèi)者”消費(fèi)了數(shù)據(jù),“生產(chǎn)者”才會(huì)被喚醒繼續(xù)“生產(chǎn)”凯旭。

而且不僅如此罢艾,基于阻塞隊(duì)列,我們還可以通過(guò)協(xié)調(diào)“生產(chǎn)者”和“消費(fèi)者”的個(gè)數(shù)尽纽,來(lái)提高數(shù)據(jù)的處理效率。比如前面的例子童漩,我們可以多配置幾個(gè)“消費(fèi)者”弄贿,來(lái)應(yīng)對(duì)一個(gè)“生產(chǎn)者”。

前面我們講了阻塞隊(duì)列矫膨,在多線程情況下差凹,會(huì)有多個(gè)線程同時(shí)操作隊(duì)列期奔,這個(gè)時(shí)候就會(huì)存在線程安全問(wèn)題,那如何實(shí)現(xiàn)一個(gè)線程安全的隊(duì)列呢危尿?

線程安全的隊(duì)列我們叫作并發(fā)隊(duì)列呐萌。最簡(jiǎn)單直接的實(shí)現(xiàn)方式是直接在 enqueue()、dequeue() 方法上加鎖谊娇,但是鎖粒度大并發(fā)度會(huì)比較低肺孤,同一時(shí)刻僅允許一個(gè)存或者取操作。實(shí)際上济欢,基于數(shù)組的循環(huán)隊(duì)列赠堵,利用 CAS 原子操作,可以實(shí)現(xiàn)非常高效的并發(fā)隊(duì)列法褥。這也是循環(huán)隊(duì)列比鏈?zhǔn)疥?duì)列應(yīng)用更加廣泛的原因茫叭。在實(shí)戰(zhàn)篇講 Disruptor 的時(shí)候,我會(huì)再詳細(xì)講并發(fā)隊(duì)列的應(yīng)用半等。

內(nèi)容小結(jié)

我的代碼實(shí)現(xiàn)
https://gitee.com/kaiLee/struct/tree/master/src/main/java/com/s4

今天我們講了一種跟棧很相似的數(shù)據(jù)結(jié)構(gòu)揍愁,隊(duì)列。關(guān)于隊(duì)列杀饵,你能掌握下面的內(nèi)容莽囤,這節(jié)就沒(méi)問(wèn)題了。

隊(duì)列最大的特點(diǎn)就是先進(jìn)先出凹髓,主要的兩個(gè)操作是入隊(duì)和出隊(duì)烁登。跟棧一樣,它既可以用數(shù)組來(lái)實(shí)現(xiàn)蔚舀,也可以用鏈表來(lái)實(shí)現(xiàn)饵沧。用數(shù)組實(shí)現(xiàn)的叫順序隊(duì)列,用鏈表實(shí)現(xiàn)的叫鏈?zhǔn)疥?duì)列赌躺。特別是長(zhǎng)得像一個(gè)環(huán)的循環(huán)隊(duì)列狼牺。

在數(shù)組實(shí)現(xiàn)隊(duì)列的時(shí)候,會(huì)有數(shù)據(jù)搬移操作礼患,要想解決數(shù)據(jù)搬移的問(wèn)題是钥,我們就需要像環(huán)一樣的循環(huán)隊(duì)列。循環(huán)隊(duì)列是我們這節(jié)的重點(diǎn)缅叠。要想寫(xiě)出沒(méi)有 bug 的循環(huán)隊(duì)列實(shí)現(xiàn)代碼悄泥,關(guān)鍵要確定好隊(duì)空和隊(duì)滿的判定條件,具體的代碼你要能寫(xiě)出來(lái)肤粱。

除此之外弹囚,我們還講了幾種高級(jí)的隊(duì)列結(jié)構(gòu),阻塞隊(duì)列领曼、并發(fā)隊(duì)列鸥鹉,底層都還是隊(duì)列這種數(shù)據(jù)結(jié)構(gòu)蛮穿,只不過(guò)在之上附加了很多其他功能。阻塞隊(duì)列就是入隊(duì)毁渗、出隊(duì)操作可以阻塞践磅,并發(fā)隊(duì)列就是隊(duì)列的操作多線程安全。

參考

09 | 隊(duì)列:隊(duì)列在線程池等有限資源池中的應(yīng)用
https://time.geekbang.org/column/article/41330

java/09_queue · 編程語(yǔ)言算法集/algo - 碼云 - 開(kāi)源中國(guó) https://gitee.com/TheAlgorithms/algo/tree/master/java/09_queue

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末灸异,一起剝皮案震驚了整個(gè)濱河市府适,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌绎狭,老刑警劉巖细溅,帶你破解...
    沈念sama閱讀 211,948評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異儡嘶,居然都是意外死亡喇聊,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)蹦狂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)誓篱,“玉大人,你說(shuō)我怎么就攤上這事凯楔〈芙荆” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,490評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵摆屯,是天一觀的道長(zhǎng)邻遏。 經(jīng)常有香客問(wèn)我,道長(zhǎng)虐骑,這世上最難降的妖魔是什么准验? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,521評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮廷没,結(jié)果婚禮上糊饱,老公的妹妹穿的比我還像新娘。我一直安慰自己颠黎,他們只是感情好另锋,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,627評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著狭归,像睡著了一般夭坪。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上过椎,一...
    開(kāi)封第一講書(shū)人閱讀 49,842評(píng)論 1 290
  • 那天台舱,我揣著相機(jī)與錄音,去河邊找鬼。 笑死竞惋,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的灰嫉。 我是一名探鬼主播拆宛,決...
    沈念sama閱讀 38,997評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼讼撒!你這毒婦竟也來(lái)了浑厚?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,741評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤根盒,失蹤者是張志新(化名)和其女友劉穎钳幅,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體炎滞,經(jīng)...
    沈念sama閱讀 44,203評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡敢艰,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,534評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了册赛。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钠导。...
    茶點(diǎn)故事閱讀 38,673評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖森瘪,靈堂內(nèi)的尸體忽然破棺而出牡属,到底是詐尸還是另有隱情,我是刑警寧澤扼睬,帶...
    沈念sama閱讀 34,339評(píng)論 4 330
  • 正文 年R本政府宣布逮栅,位于F島的核電站,受9級(jí)特大地震影響窗宇,放射性物質(zhì)發(fā)生泄漏措伐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,955評(píng)論 3 313
  • 文/蒙蒙 一担映、第九天 我趴在偏房一處隱蔽的房頂上張望废士。 院中可真熱鬧,春花似錦蝇完、人聲如沸官硝。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,770評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)氢架。三九已至,卻和暖如春朋魔,著一層夾襖步出監(jiān)牢的瞬間岖研,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,000評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留孙援,地道東北人害淤。 一個(gè)月前我還...
    沈念sama閱讀 46,394評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像拓售,于是被迫代替她去往敵國(guó)和親窥摄。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,562評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容