重溫:數(shù)據(jù)結(jié)構(gòu)與算法 - 07隊列

xwzz.jpg

重溫:數(shù)據(jù)結(jié)構(gòu)與算法 - 開篇
重溫:數(shù)據(jù)結(jié)構(gòu)與算法 - 復(fù)雜度分析(一)
重溫:數(shù)據(jù)結(jié)構(gòu)與算法 - 復(fù)雜度分析(二)
重溫:數(shù)據(jù)結(jié)構(gòu)與算法 - 數(shù)組
重溫:數(shù)據(jù)結(jié)構(gòu)與算法 - 鏈表(一)
重溫:數(shù)據(jù)結(jié)構(gòu)與算法 - 鏈表(二)
重溫:數(shù)據(jù)結(jié)構(gòu)與算法 - 棧

叨叨兩句

最近提了離職,給公司找交接人也面試了不少人,上次作為面試官還是兩年前事情;
最大感受大環(huán)境還是太過浮躁,五年前如此,今亦是如此,福報996,中年危機(jī),卷王爭霸,各種負(fù)面信息纏繞在周圍;
作為個人,沒資格去評論他人的職業(yè)規(guī)劃,給他人灌輸雞湯,更是害人不淺。僅告誡自己:靜心、學(xué)習(xí)、自省、前行。

什么是隊列

言歸正傳,本章介紹另一種“操作受限”的線性表數(shù)據(jù)結(jié)構(gòu):隊列。

結(jié)構(gòu)相同,它僅支持添加和刪除操作:

  • 添加操作(入隊 - enqueue)
  • 刪除操作(出隊 - dequeue)

不同點(diǎn)在于與棧的先進(jìn)后出相反,它具有"先進(jìn)先出"特性:

排隊.png

圖中,排隊買票就是典型的隊列結(jié)構(gòu),先排隊的人優(yōu)先買到票。

隊列結(jié)構(gòu).png

我們把入隊操作稱之"enqueue()",出隊操作則為"dequeue()",一個簡單的隊列操作約束大致如下:

// 隊列基本操作約束接口
public interface Queue<T> {

    // 入隊
    boolean enqueue(T item);

    // 出隊
    T dequeue();

    // 是否為空
    boolean isEmpty();

    // 隊中數(shù)據(jù)的數(shù)量
    int size();

    // 清空
    void clear();
}

一個滿足先進(jìn)先出的隊列容器,其關(guān)鍵在于enqueue()dequeue()函數(shù)的實(shí)現(xiàn)上,而基礎(chǔ)容器的選擇同上篇的棧結(jié)構(gòu)一樣,使用數(shù)組或者鏈表都可以,使用數(shù)組實(shí)現(xiàn)隊列稱之:順序隊列,使用鏈表實(shí)現(xiàn)則為:鏈?zhǔn)疥犃?/strong>。
對于資源限制嚴(yán)謹(jǐn)?shù)膱鼍跋峦扑]使用順序隊列,而對資源限制沒有要求,僅需先進(jìn)后出特性的場景可使用鏈?zhǔn)疥犃小?/p>

順序隊列(數(shù)組)

以下是以數(shù)組實(shí)現(xiàn)順序隊列的思路,大家不妨看看:

順序隊列:

  • 1、定義一個items[]數(shù)組來存儲數(shù)據(jù);

  • 2、定義一個head指針和tail指針,分別指向當(dāng)前隊列的首下標(biāo)和尾下標(biāo);

  • 3、入隊操作enqueue()中,校驗隊列是否已滿:

    • 已滿 ,返回false,入隊失??;
    • 未滿,將數(shù)據(jù)存儲到隊尾(tail指針位置),且tail指針后移一位 ;
  • 4、出隊操作dequeue()中,校驗隊列是否已空:

    • 已空:返回null
    • 非空:返回隊首數(shù)據(jù)(head指針位置),且將head指針后移一位。

按照這個思路,我把實(shí)現(xiàn)代碼貼在了下方:

public class ArrayQueue<T> implements Queue<T> {

    private static final int DEFAULT_SIZE = 10;

    // 隊列中數(shù)據(jù)數(shù)量
    private int count;

    private Object[] items;

    // 當(dāng)前隊列能存儲的最大容量
    private int maxSize;

    // head指向隊首下標(biāo) , tail指向隊尾下標(biāo)
    private int head = 0, tail = 0;

    public ArrayQueue() {
        this(DEFAULT_SIZE);
    }

    public ArrayQueue(int intSize) {
        items = new Object[intSize];
        this.maxSize = intSize;
    }

    @Override
    public boolean enqueue(T item) {
        // 隊列滿了,存儲失敗
        if (tail == maxSize) return false;

        // 存儲,隊尾下標(biāo)后移一位
        items[tail] = item;
        ++tail;
        ++count;
        return true;
    }

    @Override
    public T dequeue() {
        // 隊空,返回null
        if (isEmpty()) return null;

        // 取出數(shù)據(jù),隊首下標(biāo)后移一位
        Object item = items[head];
        ++head;
        --count;
        return (T) item;
    }

    @Override
    public boolean isEmpty() {
        return count == 0;
    }

    @Override
    public int size() {
        return count;
    }

    @Override
    public void clear() {
        if (isEmpty()) return;
        for (int i = 0; i < size(); i++) {
            items[i] = null;
        }
        count = 0;
    }


    public void print() {
        for (int i = head; i < tail; i++) {
            System.out.print(items[i] + "\t");
        }
        System.out.println();
    }
}

測試一下:

ArrayQueue<String> queue = new ArrayQueue<>(3);
queue.enqueue("1");
queue.enqueue("2");
queue.enqueue("3");
queue.enqueue("4");
queue.enqueue("5");
queue.print();
String data1 = queue.dequeue();
System.out.println("出隊:" + data1);
String data2 = queue.dequeue();
System.out.println("出隊:" + data2);
queue.print();
System.out.println("大?。? + queue.size());

// 問題:隊列有空閑空間卻無法添加數(shù)據(jù)
System.out.println(queue.enqueue("6"));
queue.print();
System.out.println("大?。? + queue.size());

輸出:

1   2   3   
出隊:1
出隊:2
3   
大?。?

false
3   
大小:1

通過測試,我們創(chuàng)建了一個大小為3的隊列,并一口氣增加了5個元素,因隊列只能容納3個元素,后續(xù)添加的4、5元素都失敗了;緊接出隊兩個元素1、2,隊列最終只剩下元素3,滿足隊列的先進(jìn)先出原則。

然而,當(dāng)我想嘗試?yán)^續(xù)添加元素6時,卻失敗了!隊列當(dāng)前大小依舊是1,但我們知道隊列總大小明明是3,卻不能繼續(xù)向隊列添加數(shù)據(jù),顯然這并不是我想要的結(jié)果,通過下圖我們可以看到原因:

入隊失敗.png

當(dāng)我們在第一步入隊操作后,tail指針就已到隊尾,無論第二步出隊多少個元素,tail指針始終沒有被重置,顯然這是導(dǎo)致此問題的根本原因。

如何解決?
數(shù)據(jù)搬移!沒錯,在數(shù)組章節(jié)提到過插入一個數(shù)據(jù)進(jìn)數(shù)組中,需要將插入位置及后面所有的數(shù)據(jù)都向后搬移一位;同理,當(dāng)隊列中隊首元素出隊時,將隊首之后的所有元素向前搬移一位,并將tail--,這樣就可以保證tail指針始終指向的是隊內(nèi)實(shí)際尾元素下標(biāo),而不是隊列實(shí)際大小的尾下標(biāo)。

按照上面思路,的確解決了問題,不過每次出隊都需要進(jìn)行一次數(shù)據(jù)搬移,這使得dequeue()函數(shù)時間復(fù)雜度由原先的 O(1) --》O(n).

有沒有更好的方案?
換個角度,來看看enqueue()入隊函數(shù),還是上面問題,當(dāng)我們執(zhí)行第3步,入隊元素6時,由于tail指針指向了隊尾,入隊失敗。但通過上圖可以看到head指針前是存在空閑空間的,此時我們進(jìn)行數(shù)據(jù)搬移可否?

已上圖為例,當(dāng)執(zhí)行第三步時,tail指針指向隊尾,但head指針并不在隊首,那么將3搬移至下標(biāo)為0的位置,重置head、tail指針位置,再入隊元素6,按照這個思路修改enqueue()函數(shù)如下:

    public boolean enqueue(T item) {
        // 隊列到達(dá)末尾
        if (tail == maxSize) {
            // 隊列滿了,存儲失敗
            if (head == 0) return false;

            // 隊列頭部有空閑空間,進(jìn)行數(shù)據(jù)搬移
            for (int i = head; i < tail; i++) {
                items[i - head] = items[i];
            }
            // 修正head、tail下標(biāo)
            tail = tail - head;
            head = 0;
        }

        // 存儲,隊尾下標(biāo)向后移動一位
        items[tail] = item;
        ++tail;
        ++count;
        return true;
    }

修改后的enqueue()函數(shù)時間復(fù)雜度,又是多少呢?分析如下:

  • 當(dāng)tail未達(dá)尾部時,可直接入隊,時間復(fù)雜度:O(1)
  • 當(dāng)tail達(dá)到尾部時,隊首有空余空間,進(jìn)行數(shù)據(jù)搬移,時間復(fù)雜度:O(n)
  • 每次入隊操作,tail可能在0n任意位置,0n-1位置的時間復(fù)雜度為O(1),在n位置的時間復(fù)雜度為O(n),可將這n次搬移操作平攤給前n-1次操作,所以最終平均時間復(fù)雜度為:O(1)

鏈?zhǔn)疥犃校ㄦ湵恚?/h3>

OK,接下來學(xué)習(xí)鏈?zhǔn)疥犃校?br> 鏈?zhǔn)疥犃邢噍^順序隊列簡單很多,不需關(guān)心存儲空間不夠,或數(shù)據(jù)搬移問題。

鏈?zhǔn)疥犃校?/p>

  • 1、定義一個head指針和tail指針,用于指向當(dāng)前隊列的首結(jié)點(diǎn)和尾結(jié)點(diǎn);
  • 2、入隊操作enqueue()時,只需tail.next -> newNode , tail -> tail.next
  • 3、出隊操作dequeue()時,head -> head.next
public class LinkedQueue<T> implements Queue<T> {

    private Node<T> head, tail;
    private int count = 0;

    @Override
    public boolean enqueue(T item) {
        Node<T> newNode = new Node<>(item);
        if (head == null) {
            head = tail = newNode;
        } else {
            tail.next = newNode;
            tail = tail.next;
        }
        count++;
        return true;
    }

    @Override
    public T dequeue() {
        if (head == null) return null;
        T item = head.data;
        head = head.next;
        count--;
        return item;
    }

    @Override
    public boolean isEmpty() {
        return count == 0;
    }

    @Override
    public int size() {
        return count;
    }

    @Override
    public void clear() {
        head = null;
    }

    public void print() {
        Node<T> temp = head;
        while (temp != null) {
            System.out.print(temp.data + "\t");
            temp = temp.next;
        }
        System.out.println();
    }

    static class Node<T> {

        public T data;          // 存儲數(shù)據(jù)
        public Node<T> next;    // 下一個結(jié)點(diǎn)

        public Node(T data) {
            this.data = data;
        }

    }
}

測試:

        LinkedQueue<String> queue = new LinkedQueue<>();
        queue.enqueue("1");
        queue.enqueue("2");
        queue.enqueue("3");
        queue.enqueue("4");
        queue.enqueue("5");

        queue.print();

        System.out.println("出隊:" + queue.dequeue());
        System.out.println("出隊:" + queue.dequeue());
        System.out.println("出隊:" + queue.dequeue());

        queue.print();

        queue.enqueue("6");

        queue.print();

輸出:

出隊:1
出隊:2
出隊:3
4   5   
4   5   6   

可以看到鏈?zhǔn)疥犃?,無論是入隊enqueue()還是出隊dequeue(),都可通過指針直接訪問到對應(yīng)結(jié)點(diǎn),所以鏈?zhǔn)疥犃械某鲫犎腙爼r間復(fù)雜度都是:O(1)

進(jìn)階

循環(huán)隊列

前面提到,在內(nèi)存資源有限的場景下,推薦使用順序隊列,反之可使用鏈?zhǔn)疥犃?。如果在有限資源下,又希望能擁有像鏈?zhǔn)疥犃幸粯痈咝У娜腙牪僮?,這樣的隊列能否實(shí)現(xiàn)?

循環(huán)隊列.png

如圖所示,我們將數(shù)組的首尾相連:

  • 入隊時,當(dāng)數(shù)據(jù)填充到尾部且繼續(xù)填充時,重置tail指針指向下標(biāo)為0的內(nèi)存空間,諾該空間處在空閑狀態(tài)則入隊成功,否則已滿入隊失??;
  • 出隊時,通過公式head = ( head + 1 ) % maxSize 來計算實(shí)際的head指針位置。
/**
 * 循環(huán)隊列
 */
public class CircularQueue<T> implements Queue<T> {

    private static final int DEFAULT_SIZE = 10;

    // 隊列中數(shù)據(jù)數(shù)量
    private int count;

    private Object[] items;

    // 當(dāng)前隊列能存儲的最大容量
    private int maxSize;

    // head指向隊列頭部下標(biāo) , tail指向尾部下標(biāo)
    private int head = 0, tail = 0;

    public CircularQueue() {
        this(DEFAULT_SIZE);
    }

    public CircularQueue(int intSize) {
        items = new Object[intSize];
        this.maxSize = intSize;
    }

    @Override
    public boolean enqueue(T item) {
        // 隊滿
        if (count == maxSize) return false;
        // tail到達(dá)隊尾,
        if (tail == maxSize) tail = 0;
        // 存儲,隊尾下標(biāo)向后移動一位
        items[tail] = item;
        ++tail;
        ++count;
        return true;
    }

    @Override
    public T dequeue() {
        // 隊空,返回null
        if (isEmpty()) return null;
        // 取出數(shù)據(jù),隊頭下標(biāo)向后移動一位
        Object item = items[head];
        head = (head + 1) % maxSize;
        --count;
        return (T) item;
    }

    @Override
    public boolean isEmpty() {
        return count == 0;
    }

    @Override
    public int size() {
        return count;
    }

    @Override
    public void clear() {
        if (isEmpty()) return;
        for (int i = 0; i < size(); i++) {
            items[i] = null;
        }
        count = 0;
    }


    public void print() {
        for (int i = head, j = 0; j < count; i = (i + 1) % maxSize, j++) {
            System.out.print(items[i] + "\t");
        }
        System.out.println();
    }
}

測試:

        CircularQueue<String> queue = new CircularQueue<>(5);
        queue.enqueue("1");
        queue.enqueue("2");
        queue.enqueue("3");
        queue.enqueue("4");
        queue.enqueue("5");
        queue.enqueue("6");

        queue.print();

        System.out.println("出隊:" + queue.dequeue());
        System.out.println("出隊:" + queue.dequeue());

        queue.print();

        queue.enqueue("7");

        queue.print();

輸出:

1   2   3   4   5   
出隊:1
出隊:2
3   4   5   
3   4   5   7   

由于入隊enqueue()、出隊dequeue()分別對tail指針和head指針進(jìn)行了糾正,不再會出現(xiàn)數(shù)據(jù)搬移的情況,其時間復(fù)雜度都為:O(1).

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容