算法一看就懂之「 隊列 」

算法的系列文章中,以前我們已經聊過了「 數組和鏈表 」「 堆棧 」,今天我們再來繼續看看「 隊列 」這種數據結構。「 隊列 」和「 堆棧 」比較相似,都屬於線性表數據結構,而且都在操做上受到必定規則約束,都是很是經常使用的數據類型,我們掌握得再熟練也不爲過。算法

1、「 隊列 」是什麼?

隊列(queue)是一種先進先出的、操做受限的線性表。數組

隊列這種數據結構很是容易理解,就像咱們平時去超市買東西,在收銀臺結帳的時候須要排隊,先去排隊的就先結帳出去,排在後面的就後結帳,有其餘人再要過來結帳,必須排在隊尾不能在隊中間插隊。微信

「 隊列 」數據結構就是這樣的,先進入隊列的先出去,後進入隊列的後出去。必須從隊尾插入新元素,隊列中的元素只能從隊首出,這也就是「 隊列 」操做受限制的地方了。數據結構

與堆棧相似,隊列既能夠用 「 數組 」 來實現,也能夠用 「 鏈表 」 來實現。架構

下面主要介紹一下目前用的比較多的幾種「 隊列 」類型:大數據

  • 順序隊列spa

  • 鏈式隊列3d

  • 循環隊列指針

  • 優先隊列code

下面來依次瞭解一下:

  1. 用數組實現的隊列,叫作 順序隊列

    用數組實現的思路是這樣的:初始化一個長度爲n的數組,建立2個變量指針front和rear,front用來標識隊頭的下標,而rear用來標識隊尾的下標。由於隊列老是從對頭取元素,從隊尾插入數據。所以咱們在操做這個隊列的時候經過移動front和rear這兩個指針的指向便可。初始化的時候front和rear都指向第0個位置。

    當有元素須要入隊的時候,首先判斷一下隊列是否已經滿了,經過rear與n的大小比較能夠進行判斷,若是相等則說明隊列已滿(隊尾沒有空間了),不能再插入了。若是不相等則容許插入,將新元素賦值到數組中rear指向的位置,而後rear指針遞增長一(即向後移動了一位),不停的往隊列中插入元素,rear不停的移動,如圖:


    當隊列裝滿的時候,則是以下狀況:


    當須要作出隊操做時,首先要判斷隊列是否爲空,若是front指針和rear指針指向同一個位置(即front==rear)則說明隊列是空的,沒法作出隊操做。若是隊列不爲空,則能夠進行出隊操做,將front指針所指向的元素出隊,而後front指針遞增長一(即向後移動了一位),加入上圖的隊列出隊了2個元素:


    因此對於數組實現的隊列而言,須要用2個指針來控制(front和rear),而且不管是作入隊操做仍是出隊操做,front或rear都是日後移動,並不會往前移動。入隊的時候是rear日後移動,出隊的時候是front日後移動。出隊和入隊的時間複雜度都是O(1)的。

  2. 用鏈表實現的隊列,叫作 鏈式隊列

    用鏈表來實現也比較簡單,與數組實現相似,也是須要2個指針來控制(front和rear),如圖:


    當進行入隊操做時,讓新節點的Next指向rear的Next,再讓rear的Next指向新節點,最後讓rear指針向後移動一位(即rear指針指向新節點),如上圖右邊部分。

    當進行出隊操做時,直接將front指針指向的元素出隊,同時讓front指向下一個節點(即將front的Next賦值給front指針),如上圖左邊部分。

  3. 循環隊列

    循環隊列是指隊列是先後連成一個圓圈,它以循環的方式去存儲元素,但仍是會按照隊列的先進先出的原則去操做。循環隊列是基於數組實現的隊列,但它比普通數據實現的隊列帶來的好處是顯而易見的,它能更有效率的利用數組空間,且不須要移動數據。

    普通的數組隊列在通過了一段時間的入隊和出隊之後,尾指針rear就指向了數組的最後位置了,無法再往隊列裏插入數據了,可是數組的前面部分(front的前面)因爲舊的數據曾經出隊了,因此會空出來一些空間,這些空間就無法利用起來,如圖:


    固然能夠在數組尾部已滿的這種狀況下,去移動數據,把數據全部的元素都往前移動以填滿前面的空間,釋放出尾部的空間,以便尾部還能夠繼續插入新元素。可是這個移動也是消耗時間複雜度的。

    循環隊列就能夠自然的解決這個問題,下面是循環隊列的示意圖:


    循環隊列也是一種線性數據結構,只不過它的最後一個位置並非結束位。對於循環隊列,頭指針front始終指向隊列的前面,尾指針rear始終指向隊列的末尾。在最初階段,頭部和尾部的指針都是指向的相同的位置,此時隊列是空的,如圖:


    當有新元素要插入到這個循環隊列的時候(入隊),新元素就會被添加到隊尾指針rear指向的位置(rear和tail這兩個英文單詞都是表示隊尾指針的,不一樣人喜歡的叫法不同),而且隊尾指針就會遞增長一,指向下一個位置,如圖:

    當須要作出隊操做時,直接將頭部指針front指向的元素進行出隊(咱們經常使用 front 或 head 英文單詞來表示頭部指針,憑我的喜愛),而且頭部指針遞增長一,指向下一個位置,如圖:


    上圖中,D1元素被出隊列了,頭指針head也指向了D2,不過D1元素的實際數據並無被刪除,但即便沒有刪除,D1元素也不屬於隊列中的一部分了,隊列只認可隊頭和隊尾之間的數據,其它數據並不屬於隊列的一部分。

    當繼續再往隊列中插入元素,當tail到達隊列的尾部的時候:


    tail的下標就有從新變成了0,此時隊列已經真的滿了。

    不過此處有個知識點須要注意,在上述隊列滿的狀況下,其實仍是有一個空間是沒有存儲數據的,這是循環隊列的特性,只要隊列不爲空,那麼就必須讓head和tail之間至少間隔一個空閒單元,至關於浪費了一個空間吧。

    假如此時咱們將隊列中的D二、D三、D四、D5都出隊,那隊列就又有空間了,咱們又能夠繼續入隊,咱們將D九、D10入隊,狀態以下:


    此時,頭指針的下標已經大於尾指針的下標了,這也是正式循環隊列的特性致使的。

    因此能夠看到,整個隊列的入隊和出隊的過程,就是頭指針head和尾指針tail互相追趕的過程,若是tail追遇上了head就說明隊滿了(前提是相隔一個空閒單元),若是head追遇上了tail就說明隊列空了。

    所以循環隊列中,判斷隊列爲空的條件是:head==tail

    判斷隊列爲滿的狀況就是:tail+1=head(即tail的下一個是head,由於前面說了不爲空的狀況下二者之間需相隔一個單元),不過若是tail與head正好一個在隊頭一個在隊尾(即tail=7,head=0)的時候,隊列也是滿的,但上述公式就不成立了,所以正確判斷隊滿的公式應該是:(tail+1)%n=head

  4. 優先隊列

    優先隊列(priority Queue)是一種特殊的隊列,它不遵照先進先出的原則,它是按照優先級出隊列的。分爲最大優先隊列(是指最大的元素優先出隊)和最小優先隊列(是指最小的元素優先出隊)。

    通常用來實現優先隊列,在後面講的文章裏我會詳細再講,這裏瞭解一下便可。

2、「 隊列 」的算法實踐?

咱們看看常常涉及到 隊列 的 算法題(來源leetcode)

算法題1:使用棧實現隊列的下列操做:
    push(x) -- 將一個元素放入隊列的尾部。
    pop() -- 從隊列首部移除元素。
    peek() -- 返回隊列首部的元素。
    empty() -- 返回隊列是否爲空。

解題思路:堆棧是FILO先進後出,隊列是FIFO先進先出,要使用堆棧來實現隊列的功能,能夠採用2個堆棧的方式。堆棧A和堆棧B,當有元素要插入的時候,就往堆棧A裏插入。當要移除元素的時候,先將堆棧A裏的元素依次出棧放入到堆棧B中,再從堆棧B的頂部出數據。如此便基於2個堆棧實現了先進先出的原則了。

class MyQueue {

    private Stack<Integer> s1 = new Stack<>();
    private Stack<Integer> s2 = new Stack<>();
    private int fornt;


    /** Initialize your data structure here. */
    public MyQueue() {

    }

    /** Push element x to the back of queue. */
    public void push(int x) {
        if(s1.empty()) fornt = x;
        s1.push(x);
    }

    /** Removes the element from in front of queue and returns that element. */
    public int pop() {
        if(s2.empty()){
            while(!s1.empty()){
                s2.push(s1.pop());
            }
        }
         return s2.pop();
    }

    /** Get the front element. */
    public int peek() {
        if(s2.empty()){
            return fornt;
        }
        return s2.peek();
    }

    /** Returns whether the queue is empty. */
    public boolean empty() {
        return s1.empty()&&s2.empty();
    }
}   

入棧的時間複雜度爲O(1),出棧的時間複雜度爲O(1)


算法題2:使用隊列來實現堆棧的下列操做:
    push(x) -- 元素 x 入棧
    pop() -- 移除棧頂元素
    top() -- 獲取棧頂元素
    empty() -- 返回棧是否爲空

解題思路:因爲須要使用FIFO的隊列模擬出FILO的堆棧效果,所以須要使用2個隊列來完成,隊列A和隊列B,當須要進行入棧操做的時候,直接往隊列A中插入元素。當須要進行出棧操做的時候,先將隊列A中的前n-1個元素依次出隊移動到隊列B中,這樣隊列A中剩下的最後一個元素其實就是咱們所須要出棧的元素了,將這個元素出隊便可。

class MyStack {

    private Queue<Integer> q1 = new LinkedList<>();
    private Queue<Integer> q2 = new LinkedList<>();
    int front;

    /** Initialize your data structure here. */
    public MyStack() {

    }

    /** Push element x onto stack. */
    public void push(int x) {
        q1.add(x);
        front = x;
    }

    /** Removes the element on top of the stack and returns that element. */
    public int pop() {
        while(q1.size()>1){
            front = q1.remove();
            q2.add(front);
        }
        int val = q1.remove();
        Queue<Integer> temp = q2;
        q2 = q1;
        q1 = temp;
        return val;
    }

    /** Get the top element. */
    public int top() {
        return front;
    }

    /** Returns whether the stack is empty. */
    public boolean empty() {
        return q1.size()==0;
    }
}

入棧的時間複雜度爲O(1),出棧的時間複雜度爲O(n)

這道題其實還有另外一個解法,只須要一個隊列就能夠作到模擬出堆棧,思路就是:當須要進行入棧操做的時候,先將新元素插入到隊列的隊尾中,再將這個隊列中的其它元素依次出隊,隊列的特性固然是從隊頭出隊了,可是出來的元素再讓它們從隊尾入隊,這樣依次進行,留下剛纔插入的新元素不動,這個時候,這個新元素其實就被頂到了隊頭了,新元素入棧的動做就完成了。當須要進行出棧操做的時候,就直接將隊列隊頭元素出隊便是了。
思路已經寫出來了,代碼的話就留給你們練習了哦。

以上,就是對數據結構「 隊列 」的一些思考。

碼字不易啊,喜歡的話不妨轉發朋友吧。😊

本文原創發佈於微信公衆號「 不止思考 」,歡迎關注。涉及 思惟認知、我的成長、架構、大數據、Web技術 等。 

相關文章
相關標籤/搜索