1. 程式人生 > >看完這篇你還不知道這些佇列,我這些圖白作了

看完這篇你還不知道這些佇列,我這些圖白作了

佇列(queue)是一種採用先進先出(FIFO)策略的抽象資料結構,即最先進佇列的資料元素,同樣要最先出佇列。佇列跟我們排隊買票一樣,先來排隊的肯定先買票,後來排隊的的後買到票。佇列如下圖所示:

佇列有兩個重要的概念,一個叫隊頭,一個叫隊尾,隊頭指向的是第一個元素,而隊尾指向的是最後一個元素。佇列跟棧一樣也是訪問受限制的,所以佇列也只有兩個主要的操作:入隊(enqueue)操作 和 出隊(dequeue)操作 。入隊操作就是將一個元素新增到隊尾,出隊操作就是從隊頭取出一個元素。

佇列的底層實現可以用陣列和連結串列,基於陣列實現的佇列叫作順序佇列,基於連結串列實現的佇列叫作鏈式佇列,下面我們分別用陣列和連結串列來簡單的實現這兩種佇列。

基於陣列的佇列

不管使用那種方式來實現佇列,都需要定義兩個指標分別指向隊頭和隊尾,本文中我們用head指向隊頭,tail指向隊尾,後面的示例中這將預設使用這個,有特殊的地方我會進行說明,先來看看順序佇列的入隊、出隊操作。

圖中可以看出,入隊時,隊尾往後移動,隊頭保持不變,出隊是隊頭往後移動,隊尾保持不變。入隊、出隊操作的邏輯都比較簡單,可能你有疑問的地方是:出隊時為什麼隊頭要往後移動而不是一直指向陣列下標為0的位置? 為什麼呢?如果我們保持隊頭一直指向陣列下標為0的位置,那每次出隊操作後,後面的資料都需要往前挪一位,換句話說每次出隊操作都需要進行資料遷移,而資料遷移的代價比較大,每次資料遷移的時間複雜度為O(n),這樣會極大的影響佇列的使用效能。如果我們出隊時,隊頭往後移動一位,這樣我們就避免每次出隊都進行資料遷移,我們只需要在只有在tail

等於陣列大小且head不等於0時,進行一次資料遷移,將已經出隊留下的空間繼續供入隊時使用。下圖是資料遷移的過程:

資料遷移時,從head位置開始的資料都需要往前移動head位,這樣就把出隊後的空間騰出來,供後續入隊操作使用。

基於陣列的佇列實現程式碼:

/**
 * 基於陣列的佇列
 */
public class ArrayQueue {

    // 存放資料的陣列
    private String[] items;
    // 容器的大小
    private int size = 0;
    // 第一個節點
    private int head = 0;
    // 最後一個節點
    private int tail = 0;

    // 建構函式
    public ArrayQueue(int size){
        this.size = size;
        items = new String[size];
    }

    /**
     * 入隊操作
     * @param data
     * @return
     */
    public int enqueue(String data){
        // 如果最後一個節點等於容器大小,說明佇列滿了
        /**
         * 判斷佇列滿了的條件,tail = size,head = 0,
         */
        if (tail == size && head == 0) return -1;

        /**
         * 如果tail = size,但是head != 0,說明前有資料刪除,佇列未滿,需要資料遷移
         */
        if (tail == size){
            // head 後面的資料都需要往前遷移 head 位
            for (int i= head;i< size;i++){
                items[i-head] = items[i];
            }
            // 將最後一個元素遷移 head 位
            tail -=head;
            // 第一個元素指向 0
            head = 0;
        }
        // 向佇列中新增元素
        items[tail] = data;

        tail++;

        return 1;
    }

    /**
     * 出隊操作
     * @return
     */
    public String dequeue(){
        // 第一個元素和最後一個元素相等時,佇列為空
        if (head == tail) return null;

        String result = items[head];
        // 第一個元素後移一次,這樣做的好處是在出隊時不需要資料遷移
        head ++ ;

        return result;
    }
}

鏈式佇列

鏈式佇列實現起來相對順序佇列來說要簡單很多,我們先來看看鏈式佇列的入隊、出隊操作:


從圖中可以看出鏈式佇列入隊操作是將tailnext指向新增的節點,然後將tail指向新增的節點,出隊操作時,將head節點指向head.next節點。鏈式佇列與順序佇列比起來不需要進行資料的遷移,但是鏈式佇列增加了儲存成本。

基於連結串列的佇列實現程式碼

/**
 * 基於連結串列的佇列
 */
public class LinkQueue {

    // 指向隊首
    private Node head;
    // 指向隊尾
    private Node tail;

    /**
     * 入隊操作
     * @param data
     * @return
     */
    public int enqueue(String data){
        Node node = new Node(data,null);
        // 判斷佇列中是否有元素
        if (tail == null) {
            tail = node;
            head = node;
        }else {
            tail.next = node;
            tail = node;
        }
        return 1;
    }

    /**
     * 出隊操作
     * @return
     */
    public String dequeue(){
        if (head==null) return null;
        String data = head.data;
        head = head.next;
        // 取出元素後,頭指標為空,說明佇列中沒有元素,tail也需要製為空
        if (head == null){
            tail = null;
        }
        return data;
    }

    class Node{
        private String data;
        private Node next;

        public Node(String data,Node node){
            this.data = data;
            next = node;
        }
    }
}

迴圈佇列

迴圈佇列是對順序佇列的改進,因為順序佇列不可避免的資料遷移操作,資料遷移操作會導致佇列的效能下降,為了避免這個問題,將佇列改造成迴圈的,當tail到達陣列的最大下標時,重新指回陣列下標為0的位置,這樣就避免了資料遷移。先來看看迴圈佇列的出隊、入隊操作:


因為佇列是迴圈佇列,所以在進行入隊、出隊操作時,就不能像順序佇列那樣對tailhead進行簡單的加1操作,我們需要對tailhead1後與陣列的大小進行求餘操作,來得出tailhead的值,這樣才能進行迴圈操作。迴圈佇列需要犧牲一個儲存空間,對於一個儲存空間為n的迴圈佇列來說只能存放n-1為資料,因為如果不犧牲一個儲存空間的話,當tail==head時,就有可能存在隊空或者隊滿的情況。

迴圈佇列的實現程式碼

/**
 * 環形佇列,不需要資料遷移,提高效能
 */
public class CircularQueue {

    // 存放資料的陣列
    private String[] items;
    // 容器的大小
    private int size = 0;
    // 第一個節點
    private int head = 0;
    // 最後一個節點
    private int tail = 0;

    // 建構函式
    public CircularQueue(int size){
        this.size = size;
        items = new String[size];
    }

    /**
     * 入隊操作
     * @param data
     * @return
     */
    public int enqueue(String data){
        /**
         * 判斷環形佇列滿了的條件,(tail+1)求餘等於head
         */
        if ((tail+1)%size == head) return -1;

        // 向佇列中新增元素
        items[tail] = data;
        // 因為是環形佇列,所以下邊是陣列長度的餘數
        tail= (tail+1)%size;

        return 1;
    }

    /**
     * 出隊操作
     * @return
     */
    public String dequeue(){
        // 第一個元素和最後一個元素相等時,佇列為空
        if (head == tail) return null;

        String result = items[head];
        // 因為是環形佇列,所以下邊是陣列長度的餘數
        head = (head+1)% size ;

        return result;
    }
}

雙端佇列

雙端佇列是一種隊頭、隊尾都可以進行入隊、出隊操作的佇列,雙端佇列採用雙向連結串列來實現,先來看一下雙端佇列的入隊、出隊操作:

可以從動態圖中看出,雙端佇列的每一端都是一個棧,都符合棧先進後出的特性,如果我們對雙端佇列進行禁止隊頭入隊和隊尾出隊操作的限制,雙端佇列又變成了一個鏈式佇列,雙端佇列是一種多功能的資料結構,我們可以使用它來提供佇列和棧兩種功能。

雙端佇列的實現程式碼

/**
 * 雙端佇列,使用雙向連結串列實現
 */
public class DoubleEndsQueue {

    private static class Node {
        String item;
        Node next;
        Node prev;

        Node(Node prev, String element, Node next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
    // 第一個節點
    private Node first;
    // 最後一個節點
    private Node last;

    /*
     * 在第一個節點前面入隊
     */
    public void enqueueFirst(String e) {
        final Node f = first;
        final Node newNode = new Node(null, e, f);
        // 第一個節點指向新節點
        first = newNode;
        if (f == null)
            // 最後一個節點也指向該節點
            last = newNode;
        else
            // 當前節點的前節點指向新節點
            f.prev = newNode;
    }

    /**
     * 在最後一個元素後面入隊
     * @param e
     */
    public void enqueueLast(String e) {
        final Node l = last;
        final Node newNode = new Node(l, e, null);
        // 最後一個節點指向新節點
        last = newNode;
        if (l == null)
            // 第一個節點指向新節點
            first = newNode;
        else
            // 當前節點的下節點指向新節點
            l.next = newNode;
    }

    /**
     * 從第一個節點出隊
     * @return
     */
    public String dequeueFirst() {
        if (first == null) return null;
        final Node f = first;
        String element = f.item;
        Node next = f.next;
        f.item = null;
        f.next = null;
        // 第一個節點指向當先節點的next節點
        first = next;
        if (next == null)
            // 說明佇列為空
            last = null;
        else
            next.prev = null;
        return element;
    }

    /**
     * 從最後節點出隊
     * @return
     */
    public String dequeueLast() {
        final Node l = last;
        if (last == null) return null;
        String element = l.item;
        Node prev = l.prev;
        l.item = null;
        l.prev = null;
        last = prev;
        if (prev == null)
            first = null;
        else
            prev.next = null;
        return element;
    }
    
    // 輸出佇列全部內容
    public void displayAll() {
        while (first !=null){
            System.out.print(first.item+" ");
            first = first.next;
        }
        System.out.println("===============");
    }
}

優先佇列

優先佇列為一種不必遵循佇列先進先出(FIFO)特性的特殊佇列,優先佇列跟普通佇列一樣都只有一個隊頭和一個隊尾並且也是從隊頭出隊,隊尾入隊,不過在優先佇列中,每次入隊時,都會按照入隊資料項的關鍵值進行排序(從大到小、從小到大),這樣保證了關鍵字最小的或者最大的項始終在隊頭,出隊的時候優先順序最高的就最先出隊,這個就像我們醫院就醫一樣,急救的病人要比普通的病人先就診。一起來看看優先佇列的出隊、入隊操作:

在示例中,我們規定數值越小優先順序越高。我們每執行一次入隊操作時,小的元素都會靠近頭隊,在出隊的時候,元素小的也就先出隊。

優先佇列的程式碼實現

這裡使用的陣列實現優先佇列,用陣列實現主要原因是更好理解優先佇列的思想。一般都是使用堆來實現優先佇列,因為陣列實現在插入的時候對資料的排序代價比較大。

/**
 * 優先佇列
 */
public class PriorityQueue {

    // 存放資料的陣列
    private Integer[] items;
    // 容器的大小
    private int size = 0;
    // 第一個節點
    private int head = 0;

    // 建構函式
    public PriorityQueue(int size){
        this.size = size;
        items = new Integer[size];
    }

    /**
     * 入隊操作
     * @param data
     * @return
     */
    public int enqueue(Integer data){
        int j;
        if (head == 0){
            items[head++] = data;
        }
        else {
            for (j=head-1;j>=0;j--){
                // 將小的數往後排
                if (data > items[j]){
                    items[j+1] = items[j];
                }else {
                    break;
                }
            }
            items[j+1] = data;
            head++;
        }
        return 1;
    }

    public Integer dequeue(){
        return items[--head];
    }
}

總結

  • 佇列是一種遵循先進先出(FIFO)的資料結構
  • 佇列可以使用陣列和連結串列實現,陣列實現叫作順序佇列,連結串列實現叫作鏈式佇列
  • 迴圈佇列解決了順序佇列的資料遷移帶來的效能損耗的問題
  • 雙端佇列是隊頭和隊尾都可以進行入隊、出隊操作的佇列
  • 優先佇列是一種不必遵循先進先出規則的佇列,任意元素加入時,都會講優先順序最高的放入到隊頭

最後

打個小廣告,歡迎掃碼關注微信公眾號:「平頭哥的技術博文」,一起進步吧。

相關推薦

知道這些佇列這些

佇列(queue)是一種採用先進先出(FIFO)策略的抽象資料結構,即最先進佇列的資料元素,同樣要最先出佇列。佇列跟我們排隊買票一樣,先來排隊的肯定先買票,後來排隊的的後買到票。佇列如下圖所示: 佇列有兩個重要的概念,一個叫隊頭,一個叫隊尾,隊頭指向的是第一個元素,而隊尾指向的是最後一個元素。佇列跟棧一樣

大寫的服懂RocketMQ算

## 目錄 1. RocketMQ介紹 2. RocketMQ概念 3. 為什麼要用RocketMQ? 1. 非同步解耦 2. 削峰填谷 3. 分散式事務最終一致性 4. 資料分發 4. RocketMQ架構 5. RocketMQ訊息型別 1. 普通訊息 2.

文章懂Python裝飾器?

1、必備   2.需求來了 初創公司有N個業務部門,1個基礎平臺部門,基礎平臺負責提供底層的功能,如:資料庫操作、redis呼叫、監控API等功能。業務部門使用基礎功能時,只需呼叫基礎平臺提供的功能即可。如下: 目前公司有條不紊的進行著,但是,以前基礎平臺的開發

再不會 View 的動畫框架跪搓衣板

引言 眾所周知,一款沒有動畫的 app,就像沒有靈魂的肉體,給使用者的體驗性很差。現在的 android 在動畫效果方面早已空前的發展,1.View 動畫框架 2.屬性動畫框架 3.Drawable 動畫。相比後後兩者,View 動畫框架在 Android 的最開始就已經出現,即有著非常容易學習的有點,卻也有

在問什麼是JavaScript建構函式、例項、原型物件以及原型鏈?就懂

## 1概述 ES6, 全稱 ECMAScript 6.0 ,2015.06 發版。在ES6之前,物件不是基於**類**建立的,而是用一種稱為**建構函式**的**特殊函式**來定義物件和它們的特徵。 ## 2建構函式 建構函式是一種特殊的函式,主要用來初始化物件,即為物件成員變數賦初始值,它總與 new

Nginx可以做什麼?就懂

本文只針對Nginx在不載入第三方模組的情況能處理哪些事情,由於第三方模組太多所以也介紹不完,當然本文字身也可能介紹的不完整,畢竟只是我個人使用過和了解到過得,歡迎留言交流。 Nginx能做什麼 ——反向代理 ——負載均衡 ——HTTP伺服器(動靜分離) ——正向代

就能完全操作git 遠端分支的增、刪、改、查

最近專案中又用到了git所以在此總結一番,這篇主要針對的是怎麼建立遠端分支,如何刪除遠端分支。 首先,如何建立遠端分支。將一系列前期準備工作準備完成後(建立\新增ssh); 在終端鍵入 git branch -va 可以檢視本地分支與遠端分支的具體情況,當如果是一個新建的專案的時候,顯示,“ * ”號顯示的

CSS的flex佈局就懂

我們之前已經學過一些佈局模型,比如說浮動,決定定位等等,但是這些佈局方式一是不夠簡潔,而是使用的範圍確實是太窄了。 flex模型擁有比較多的屬性,來設定多樣的佈局方式,接下來我們就詳細介紹各種屬性對佈局的改變,最後再對屬性做一個彙總 先看一下flex的基本模型,如下圖所示

如果文章懂卷積那就過來掐死

卷積 最近總是和卷積打交道,工作需要,每天都要碰到它好幾次,不勝煩惱,因為在大學時候學訊號與系統的時候就沒學會,我於是心想一定要把卷積完全搞明白。正好同辦公室的同學也問我什麼是卷積,師姐昨天也告訴我說:"我也早就想把這個問題搞明白了!"經過一段時間的思考之後,有一些很

為什麼要有紅黑樹?什麼是紅黑樹?畫20張就明白

為什麼要有紅黑樹 想必大家對二叉樹搜尋樹都不陌生,首先看一下二叉搜尋樹的定義: 二叉搜尋樹(Binary Search Tree),或者是一棵空樹,或者是具有下列性質的二叉樹: 若它的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值; 若它的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值; 它的

async,await執行流懂?以後再也

昨天有朋友在公眾號發訊息說看不懂await,async執行流,其實看不懂太正常了,因為你沒經過社會的毒打,沒吃過牢飯就不知道自由有多重要,沒生過病就不知道健康有多重要,沒用過ContinueWith就不知道await,async有多重要,下面我舉兩個案例佐證一下? 一:案例一 【巢狀下的非同步】 寫了這麼多年

final、finally 和 finalize 和麵試官扯皮就沒問題

> 我把自己以往的文章彙總成為了 Github ,歡迎各位大佬 star > https://github.com/crisxuan/bestJavaer > 已提交此篇文章 `final` 是 Java 中的關鍵字,它也是 Java 中很重要的一個關鍵字,final 修飾的類、方法、變數有不同的含義

文章知道解軟體測試

測試行業小故事 我曾今不止一次見過:對軟體測試行業不甚瞭解的新人與已經在測試行業摸爬滾打幾年的前輩之間的互相指責和爭執。 新人因為不瞭解測試行業,也不瞭解提問的藝術,所以問的都是自己比較關注的問題和一些基礎問題。前輩因為對測試行業有一定的瞭解,掌握一定的知識技術,早就

別說懂Hinton大神的膠囊網路capsule network

倒計時 2 天 來源 | 王的機器(公眾號ID:MeanMachine1031) 作者 | 王聖元 0 引言 斯蒂文認為機器學習有時候像嬰兒學習,特別是在物體識別上。比如嬰兒首先學會識別邊界和顏色,然後將這些資訊用於識別形狀和圖形等更復雜的實體。比如在人臉識別上

媽媽會擔心Docker入門?

“上週物件突然心血來潮說想養個小寵物,我問想養啥她又說隨便,你看著辦!!! 這我真的比較難辦啊!但是咱們程式設計師能有個物

最詳細的python 操作 mongodb教程!會隨時找

條件 cnblogs 江蘇 。。 location flag pre del 修改字段 準備 我的本機環境是: Python3.6 mongodb3.4.3 IDE: PyCharm Professional 因為要使用Python來操作數

文章就清楚的知道 ZooKeeper的 概念

問題 sta leader 監聽服務 生成 項目 一輪 ges 服務註冊 前言 相信大家對 ZooKeeper 應該不算陌生。但是你真的了解 ZooKeeper 是個什麽東西嗎?如果別人/面試官讓你給他講講 ZooKeeper 是個什麽東西,你能回答到什麽地步呢? 我本人曾

知道Python生成器是什麼

生成器是 Python 初級開發者最難理解的概念之一,雖被認為是 Python 程式設計中的高階技能,但在各種專案中可以隨處見到生成器的身影,你得不得去理解它、使用它、甚至愛上它。 提到生成器,總不可避免地要把迭代器拉出來對比著講,生成器就是一個在行為上和迭代器非常類似的物件,如果把迭代器比

就會知道 Lineage OS 系統的一切

前幾天看到新聞,發現 CM 團隊做出的新專案 Lineage OS 系統,一經官方釋出,就備受刷機黨們的關注。下面我來詳細講述一下我刷這個系統的整個過程以及體驗。   Lineage OS 系統介紹 大夥還記得 CyanogenMod 嗎?給安卓手機刷過機的使用者基本都知

應該知道什麼是Linux~

Linux上的檔案系統一般來說就是EXT2或EXT3,但這篇文章並不準備一上來就直接講它們,而希望結合Linux作業系統並從檔案系統建立的基礎——硬碟開始,一步步認識Linux的檔案系統。 1.機械硬碟的物理儲存機制 現代計算機大部分檔案儲存功能都是由機械硬碟這種裝置