數據結構之——隊列與循環隊列

數據結構學習之——隊列與循環隊列java

  • 什麼是隊列(Queue)
  • 隊列基於動態數組的實現及時間複雜度分析
  • 優化隊列
  • 循環隊列(LoopQueue)

什麼是隊列(Queue)

隊列(Queue)同棧(stack)同樣也是一種運算收限的線性數據結構,參考:數據結構之——棧。棧即:LIFO(Last In First Out),隊列則是FIFO(First In First Out),也就是說隊列這種數據結構僅容許在一端(隊尾)添加元素,在另外一端(隊首)刪除元素,因此說隊列是一種先進先出的數據結構。能夠將隊列想象成銀行櫃檯的排隊機制同樣,在前面排隊的人能夠先辦理業務,在最後排隊的人等到前面全部的人辦理完畢後,才能夠進行業務的處理,如圖:
git

bank

隊列基於動態數組的實現及時間複雜度分析

隊列同ArrayStack的實現同樣,都須要基於動態數組做爲底層實現。
動態數組實現代碼,動態數組實現原理
在設計模式上,同ArrayStack同樣,設計的是Queue這樣一個接口,並建立ArrayQueue這樣一個類implements這個接口,Queue接口的方法與Stack棧的方法大致相同,只不過咱們將入棧push設計成enqueue(入隊),出棧pop設計爲dequeue(出隊)。接口代碼以下:github

public interface Queue<E>{
    void enqueue(E e);
    E dequeue();
    E getFront();
    int getSize();
    boolean is Empty();
}
複製代碼

ArrayQueue代碼以下:算法

public class ArrayQueue<E> implements Queue<E>{
    private Array<E> array;
    public ArrayQueue(int capacity){
        array = new Array<>(capacity);
    }
    public ArrayQueue(){
        array = new Array<>();
    }
    @Override
    public int getSize(){
        return array.getSize();
    }
    @Override
    public boolean isEmpty(){
        return array.isEmpty();
    }
    public int getCapacity(){
        return array.getCapacity();
    }
    @Override
    public void enqueue(E e){
        array.addLast(e);
    }
    @Override
    public E dequeue(){
        return array.removeFirst();
    }
    @Override
    public E getFront(){
        if(array.isEmpty)
            throw new IllegalArgumentException("ArrayQueue is Empty");
        return array.get(0)
    }
    
    @Override
    public String toString(){
        StringBuilder sb = new StringBuilder();
        sb.append("Queue:\n");
        sb.append("front:[");
        for(int i=0;i<getSize();i++){
            sb.append(array.get(i));
            if(i!=getSize()-1){
                sb.append(",");
            }else{
                sb.append("]tail");
            }
        }
        return sb.toString();
    }
}
複製代碼

時間複雜度分析以下:設計模式

E getFront();
 int getSize();
 boolean is Empty();
複製代碼

以上方法,時間複雜度爲:O(1)。數組

void enqueue(E e);
複製代碼

由於入隊操做至關於在動態數組的尾部添加元素,雖然有resize()這樣一個O(n)級別的算法,可是以均攤時間複雜度分析,enqueue操做仍然是一個O(1)級別的算法。bash

E dequeue();
複製代碼

dequeue()操做至關於動態數組的removeFirst()操做,在數組的頭部刪除一個元素,array[0] 後面的全部元素都須要向前挪動一個位置,因此dequeue出隊是一個O(n)級別的算法。
綜上分析,ArrayQueue仍是有些不完美的地方,ArrayStack全部的操做均爲O(1)級別的算法,可是基於動態數組實現的隊列ArrayQueue 在出隊操做dequeue上性能仍是略顯不足,LoopQueue(循環隊列)就優化了ArrayQueue出隊這樣一個問題。數據結構

優化ArrayQueue

咱們已經知道了,ArrayQueue美中不足的地方就在於dequeue這樣一個操做是O(n)級別的算法。出現這個問題的緣由其實是由於每次進行出隊操做時,動態數組都須要將array[0]後面全部的元素向前挪動一個單位,但實際上想想這個過程並不「划算」,由於,隊列元素的數量達到萬以上的級別時,僅僅刪除一個元素,咱們就須要將全部的元素進行一次大換血。和銀行櫃檯業務辦理的排隊不一樣,銀行櫃檯的一號辦理人辦理完畢,後面全部的人只須要上前一小步就能夠了,可是對於計算機來說,每個數據的調整都須要計算機親歷躬行。有什麼辦法能夠避免這種大規模的動輒呢?咱們可使用兩個變量去維護隊列的隊首和隊尾。
app

myQueue
假設變量爲front和tail。front維護隊首,變量tail維護隊尾,當front==tail時,隊列爲空,每增長一個元素,tail就像後移動一個單位,因此咱們須要多使用一個空間去維護 tail這樣一個變量,這樣咱們在設計模式上就須要開闢用戶指定的capacity加1的數組空間。對於用戶來說,capacity==n,但實際上真正的capacity也就是data.length==n+1,由於咱們須要多浪費一個空間去維護tail這個變量,可是浪費這樣一個空間相比於dequeue的大換血O(n)操做也是值得的。
示例:enqueue元素「D」

myQueue2

示例:dequeue

myQueue3

上述思路優化後的隊列MyQueue代碼以下:

public class MyQueue<E> implements Queue<E> {
    private int front;
    private int tail;
    private E[]data;
    public MyQueue(int capacity){
        data = (E[])new Object[capacity+1];
    }
    public MyQueue(){
        this(10);
    }
    @Override
    public int getSize(){
        return tail-front;
    }
    @Override
    public boolean isEmpty(){
        return front==tail;
    }
    @Override
    public E getFront(){
        return data[front];
    }
    private void resize(int newCapacity){
        E[]newData = (E[])new Object[newCapacity+1];
        for(int i=0;i<(tail-front);i++){
            newData[i] = data[front+i];
        }
        data = newData;
        tail = tail - front;
        front = 0;
    }
    @Override
    public void enqueue(E e){
        if(tail == data.length-1)
            resize((tail-front)*2);

        data[tail] = e;
        tail++;
    }

    @Override
    public E dequeue(){
        E ret = data[front];
        // Loitering Object
        data[front] = null;
        front++;
        if(((tail-front)==(data.length-1)/4) && (data.length-1)/2!=0)
            resize((data.length-1)/2);

        return ret;
    }

    @Override
    public String toString(){
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(String.format("MyQueue size=%d,capacity=%d\n",tail-front,data.length-1));
        stringBuilder.append("front:[");
        for(int i=front;i<tail;i++){
            stringBuilder.append(data[i]);
            if((i+1)!=tail){
                stringBuilder.append(",");
            }
        }
        stringBuilder.append("]tail");
        return stringBuilder.toString();
    }
}

複製代碼

MyQueue對ArrayQueue進行了優化操做,本來dequeue須要O(n)的時間複雜度,進行優化後,dequeue由O(n)的時間複雜度變爲了均攤O(1)的時間複雜度。這裏面須要注意的是:MyQueue的enqueue入隊操做規定了tail == data.length-1時進行「擴容」操做,這裏面擴容二字我使用了雙引號,由於有可能這個「擴容」其實是縮容。
dom

myQueue4

咱們規定了enqueue操做中,

if(tail == data.length-1)
    resize((tail-front)*2);
複製代碼

在上圖的示例中,若是reisze,咱們實際上就至關於進行了縮容的操做。咱們這樣的設計看起來解決了問題,但仍然不靈活,咱們但願在入隊時的resize只涉及到擴容,出隊時的resize只涉及縮容,咱們是否能對這樣的需求進行優化呢?

循環隊列(LoopQueue)

循環隊列的思想和ArrayQueue優化後的MyQueue大致相同,只不過循環隊列裏面加入了更加巧妙的循環機制。

myQueue4

上例中,咱們規定tail == data.length-1隊列爲滿進行resize,可是這一次咱們換一種思路。當繼續向當前隊列添加元素時,咱們這樣作:

loopQueue

變量tail從新回到了起點,這也就是循環隊列稱之爲「循環」的意義所在。那麼何時表示當前隊列已滿須要進行resize呢?

loopQueue3

loopQueue2

當front == (tail+1)%data.length,當這個條件成立時,也就說明了隊列爲滿,須要進行擴容操做了。循環隊列實現代碼以下:

public class LoopQueue<E> implements Queue<E> {
    private E[]data;
    private int front;
    private int tail;
    public LoopQueue(int capacity){
        data = (E[])new Object[capacity+1];
    }
    public LoopQueue(){
        this(10);
    }
    @Override
    public int getSize(){
        if(tail<front)
            return data.length-(front-tail);
        else{
            return tail-front;
        }
    }

    public int getCapacity(){
        return data.length-1;
    }
    @Override
    public boolean isEmpty(){
        return getSize()==0;
    }

    private void resize(int newCapacity){
        E[]newData = (E[])new Object[newCapacity+1];
        for(int i=0;i<getSize();i++){
            newData[i] = data[(i+front)%data.length];
        }
        data = newData;
        tail = getSize();
        front = 0;
    }
    @Override
    public void enqueue(E e){
        if(front==(tail+1)%data.length)
            resize(2*getSize());

        data[tail] = e;
        tail = (tail+1)%data.length;
    }

    @Override
    public E dequeue(){
        E ret = data[front];
        // Loitering Object
        data[front] = null;
        front = (front+1)%data.length;
        if(getSize() == getCapacity()/4 && getCapacity()/2!=0){
            resize(getCapacity()/2);
        }
        return ret;
    }

    @Override
    public E getFront(){
        if(getSize()==0)
            throw new IllegalArgumentException("LoopQueue is empty");

        return data[front];
    }

    @Override
    public String toString(){
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(String.format("LoopQueue size:%d,capacity:%d\n",getSize(),getCapacity()));
        stringBuilder.append("front:[");
        for(int i=front;i!=tail;i=(i+1)%data.length){
            stringBuilder.append(data[i]);
            if((i+1)%data.length!=tail)
                stringBuilder.append(",");
        }
        stringBuilder.append("]tail");
        return stringBuilder.toString();
    }
}

複製代碼

如今咱們對ArrayQueue,LoopQueue進行性能上的測試,

import java.util.Random;

public class Main {
    private static double testQueue(Queue<Integer>q,int testCount){
        long startTime = System.nanoTime();
        Random random = new Random();
        for(int i=0;i<testCount;i++)
            q.enqueue(random.nextInt(Integer.MAX_VALUE));

        for(int i=0;i<testCount;i++)
            q.dequeue();

        long endTime = System.nanoTime();
        return (endTime-startTime)/1000000000.0;
    }
    public static void main(String[]args){
        int testCount = 100000;
        ArrayQueue<Integer> arrayQueue = new ArrayQueue<>();
        LoopQueue<Integer> loopQueue = new LoopQueue<>();
        double loopQueueTime = testQueue(loopQueue,testCount);
        double arrayQueueTime = testQueue(arrayQueue,testCount);
        System.out.println("LoopQueue:"+loopQueueTime);
        System.out.println("ArrayQueue"+arrayQueueTime);
    }
}

複製代碼

在jdk1.8的環境下測試結果爲:

loopQueueTest
致使二者之間形成巨大差別的結果就是dequeue操做,ArrayQueue的dequeue操做爲O(n)級別的算法,而LoopQueue的dequeue操做 在均攤的時間複雜度上爲O(1)。
相關文章
相關標籤/搜索