面試題精選:循環隊列

近期在面試找工作的小夥伴們很多啊,我周圍就有好幾個認識的朋友在找工作,於是我突發奇想在CSDN開了一個面試題精選的專欄,主要會關注一些算法題、設計題,次要會補充一些java面試相關的題(比較本博主是java出身)。其實在此之前已經寫過一些相關的文章了,已經整理到專欄裏的,後續會持續更新,希望對大家有所幫助,有興趣的旁友可以關注下。

今天分享的面試題是循環隊列,我對這道題記憶深刻,因爲我在14年參加來校招面試的時候,二面面試官就問了這道題,當時我沒有完全答上來(不過面試官居然給我過了),後來我當面試官的時候也拿這道題考過別人,也沒遇到能徹底答出來的。不巧,前兩天又在看別人文章的時候遇到了循環隊列,索性就來自己寫一下。

有時候雖然面試官表面上是考一道很簡單的題,但背後卻想着暗度陳倉、憑空……(對不起放錯了)。比如這道循環隊列,面試官一上來可能不會直接問循環隊列,而是讓你實現一個有界隊列,如果你沒一下子想到最優的方案,不要慌還有機會,一名合格的面試官會嘗試引導你的思考,比如讓你做做複雜度分析,分析下優缺點,最後編碼實現下。就拿這題來說,首先考察了基本的數據結構隊列,再考察算法複雜度分析的能力,還考察下你歸納總結的能力、表達能力,最後考察下編碼能力。

有界隊列:指存儲有限個元素的隊列,當隊列滿之後就不能往其中添加了。與之相對的有無界隊列,無界隊列可以無限的往其中添加元素,在實際使用中往往要注意避免OOM。

回到面試題上,給你一個有界隊列的接口定義,你來實現一個有界隊列,接口如下:

public interface BoundedQueue {
    boolean add(int e);
    int peek();
    int poll();
    boolean isEmpty();
    boolean isFull();
}

我面過的一個候選人,當時他給出下面的實現方案。

public class MyBoundedQueue implements BoundedQueue {
    private int tail = 0;
    private int[] arr;

    public MyBoundedQueue(int cap) {
        this.arr = new int[cap];
    }
    @Override
    public boolean add(int e) {
        if (isFull()) {
            return false;
        }
        arr[tail++] = e;
        return true;
    }

    @Override
    public int peek() {
        if (isEmpty()) {
            return -1;
        }
        return arr[0];
    }

    @Override
    public int poll() {
        if (isEmpty()) {
            return -1;
        }
        int res = arr[0];
        for (int i = 0; i < tail-1; i++) {
            arr[i] = arr[i+1];
        }
        tail--;
        return res;
    }

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

    @Override
    public boolean isFull() {
        return tail == arr.length;
    }
}

看起來思路很清晰,是實現了有界隊列的接口。他的思路是有個指針tail一直指向隊尾第一個空位置,如果有插入就插到tail的位置,tail並往後移動。然後根據tail的具體位置來判斷隊列是滿是空,入隊如下圖示。

在這裏插入圖片描述

我們來重點看下出隊,他每次出隊都是出第0位的元素,爲了準確性必須保證arr[0]始終是隊頭,所以在每次出隊的時候都將後面的所有元素往前移動一位,導致出隊的時間複雜度變成了O(n),如下圖所示。
在這裏插入圖片描述
有沒有辦法做到不用把後面的元素往前移動呢,其實有的,我們只需要用一個head指針指向隊列的第一個可用元素前的空位即可,如下圖。
在這裏插入圖片描述
如果隊列裏的元素不動,那我們就得讓隊頭指針(head)動起來,每刪除poll()一次,隊頭指針就往後移動一次,因爲只需要移動指針,所以時間複雜度是O(1)。但是我們又有了新的問題,隊頭指針前面的那些空位置我們如何再利用起來?

這裏我們就引出了循環隊列,你把上圖想象成一根軟管,可以掰彎然後首尾相連,如下圖。
在這裏插入圖片描述
當然這裏我們使用數組實現了,只能做到邏輯上首尾相連,插入時如果插到了最後一格就直接跳回到第一格子。但如果你用鏈表實現的話,就很容易實現首尾相連了。

接下來就是難點了,上面的都比較容易理解,但好多人就是不知道如何判定隊列是空是滿(我當年面試的時候也是這種情況)。你有沒有發現上文中我頭指針和尾指針都是指向空的,不是說我喜歡這樣,而是爲了方便判斷空和滿。 注意看上文中的圖,我只在頭指針和尾指針之間的空格處存東西,所以當隊列是空的時候,循環隊列按順時針方向,頭指針在前尾指針在後,且二者相鄰,如下圖。
在這裏插入圖片描述
如果隊列滿,首尾指針是指向同一個位置,如下圖。
在這裏插入圖片描述
所有的問題我們都通過圖示的方式展示清楚了,下面我給出我用數組實現的代碼。

public class CircularBoundedQueue implements BoundedQueue {
    private int head = 0;
    private int tail = 1;
    private int[] arr;

    public CircularBoundedQueue(int cap) {
        this.arr = new int[cap+1]; 
    }
    @Override
    public boolean add(int e) {
        if (isFull()) {
            return false;
        }
        arr[tail] = e;
        tail = next(tail);
        return true;
    }

    @Override
    public int peek() {
        if (isEmpty()) {
            return -1;
        }
        return arr[head+1];
    }

    @Override
    public int poll() {
        if (isEmpty()) {
            return -1;
        }
        return arr[head = next(head)];
    }

    @Override
    public boolean isEmpty() {
        return tail == next(head);
    }

    @Override
    public boolean isFull() {
        return head == tail;
    }
    
    private int next(int cur) {
        return (cur+1)%arr.length;
    }
}

用單鏈表實現的代碼如下:

public class CircularBoundedQueue2 implements BoundedQueue {
    private class Node {
        int value;
        Node next;
    }
    
    private Node head;
    private Node tail;

    public CircularBoundedQueue2(int cap) {
        Node p = new Node();
        head = p;
        for (int i = 0; i < cap; i++) {
            Node newNode = new Node();
            p.next = newNode;
            p = p.next;
        }
        p.next = head; // 鏈表最末尾節點next指向頭部 
        tail = head.next;
    }
    @Override
    public boolean add(int e) {
        if (isFull()) {
            return false;
        }
        tail.value = e;
        tail = tail.next;
        return true;
    }

    @Override
    public int peek() {
        if (isEmpty()) {
            return -1;
        }
        return head.next.value;
    }

    @Override
    public int poll() {
        if (isEmpty()) {
            return -1;
        }
        head = head.next;
        return head.value;
    }

    @Override
    public boolean isEmpty() {
        return tail == head.next;
    }

    @Override
    public boolean isFull() {
        return head == tail;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章