1. 程式人生 > ><數據結構系列3>隊列的實現與變形(循環隊列)

<數據結構系列3>隊列的實現與變形(循環隊列)

技術 integer value 存儲 append ext info 對比 dequeue

數據結構第三課了,今天我們再介紹一種很常見的線性表——隊列

就像它的名字,隊列這種數據結構就如同生活中的排隊一樣,隊首出隊,隊尾進隊。以下一段是百度百科中對隊列的解釋:

隊列是一種特殊的線性表,特殊之處在於它只允許在表的前端(front)進行刪除操作,而在表的後端(rear)進行插入操作,和棧一樣,隊列是一種操作受限制的線性表。進行插入操作的端稱為隊尾,進行刪除操作的端稱為隊頭。隊列中沒有元素時,稱為空隊列。

隊列的數據元素又稱為隊列元素。在隊列中插入一個隊列元素稱為入隊,從隊列中刪除一個隊列元素稱為出隊。因為隊列只允許在一端插入,在另一端刪除,所以只有最早進入隊列的元素才能最先從隊列中刪除,故隊列又稱為先進先出(FIFO—first in first out)線性表。

技術分享圖片

隊列主要分為順序隊列以及循環隊列,在這我兩種隊列都會實現一下, 並在最後進行一下性能上的測試比較。

ok,讓我們開始今天的學習吧。

首先,還是先創建一個隊列的接口:

public interface Queue<E> {
    int getSize();    //得到隊列元素個數
    boolean isEmpty();    //判斷隊列是否為空
    void enqueue(E e);    //入隊
    E dequeue();    //出隊
    E getFront();    //查看隊首元素
}

由於在<數據結構系列1>(https://www.cnblogs.com/LASloner/p/10721407.html)中我們已經實現了ArrayList,所以今天我們依舊就用Array來作為Queue的數據存儲方式吧:

public class ArrayQueue<E> implements Queue<E> {
    //這裏用的是自己實現的Array,當然讀者可以直接用Java自帶的ArrayList,效果相同
    private Array<E> array=new 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() { return array.get(0); }
@Override
public String toString() { StringBuilder res=new StringBuilder(); res.append("Queue: "); res.append("front ["); for(int i = 0 ; i < array.getSize() ; i ++){ res.append(array.get(i)); if(i != array.getSize() - 1) res.append(", "); } res.append("] rear"); return res.toString(); } }

這並不是一件難事吧,那再讓我們測試一下吧:

    public static void main(String[] args) {
     ArrayQueue<Integer> queue = new ArrayQueue<>();
        for(int i = 0 ; i < 10 ; i ++){
            queue.enqueue(i);
            System.out.println("入隊:"+queue);
            if(i % 3 == 2){
                queue.dequeue();
                System.out.println("出隊:"+queue);
            }
        }
    }

輸出如下:

入隊:Queue: front [0] rear
入隊:Queue: front [0, 1] rear
入隊:Queue: front [0, 1, 2] rear
出隊:Queue: front [1, 2] rear
入隊:Queue: front [1, 2, 3] rear
入隊:Queue: front [1, 2, 3, 4] rear
入隊:Queue: front [1, 2, 3, 4, 5] rear
出隊:Queue: front [2, 3, 4, 5] rear
入隊:Queue: front [2, 3, 4, 5, 6] rear
入隊:Queue: front [2, 3, 4, 5, 6, 7] rear
入隊:Queue: front [2, 3, 4, 5, 6, 7, 8] rear
出隊:Queue: front [3, 4, 5, 6, 7, 8] rear
入隊:Queue: front [3, 4, 5, 6, 7, 8, 9] rear

這樣就算是實現了順序隊列了,但是呢,由於順序隊列存儲數據的方式使用了之前封裝好的ArrayList,並不能很好的體現隊列的這種數據結構,都沒使用到front和rear,接下來,我們再實現一下循環隊列,以此更好的了解隊列。

那之前先看看什麽是循環隊列:

在實際使用隊列時,為了使隊列空間能重復使用,往往對隊列的使用方法稍加改進:無論插入或刪除,一旦rear指針增1或front指針增1 時超出了所分配的隊列空間,就讓它指向這片連續空間的起始位置。自己真從MaxSize-1增1變到0,可用取模運算rear%MaxSize和front%MaxSize來實現。這實際上是把隊列空間想象成一個環形空間,環形空間中的存儲單元循環使用,用這種方法管理的隊列也就稱為循環隊列。除了一些簡單應用之外,真正實用的隊列是循環隊列。
在循環隊列中,當隊列為空時,有front=rear,而當所有隊列空間全占滿時,也有front=rear。為了區別這兩種情況,規定循環隊列最多只能有MaxSize-1個隊列元素,當循環隊列中只剩下一個空存儲單元時,隊列就已經滿了。因此,隊列判空的條件時front=rear,而隊列判滿的條件時front=(rear+1)%MaxSize。 技術分享圖片

依舊需要實現Queue接口:

public class LoopQueue<E> implements Queue<E>{
    private E[] data;
    private int front;//隊首
    private int rear;//隊尾
    private int size;
    
    //有參構造
    public LoopQueue(int capacity) {
        data =(E[])new Object[capacity+1];//浪費一個空間用作判斷 front==(rear+1)%(capacity+1)時為滿
        front=0;
        rear=0;
        size=0;
    }
    //無參構造
    public LoopQueue() {
        this(10);
    }
    
    //返回隊列中元素的個數
    @Override
    public int getSize() {
        return size;
    }

    //判斷隊列是否為空
    @Override
    public boolean isEmpty() {
        return front==rear;
    }

    //返回隊列容量
    public int getCapacity(){
        return data.length - 1;
    }
    
    //入隊
    @Override
    public void enqueue(E e) {
        if((rear+1)%data.length==front)//當隊列滿了之後擴容
            resize(getCapacity()*2);
        data[rear]=e;
        rear=(rear+1)%data.length;//這裏的data.length就代表Maxsize
        size++;
    }

    //出隊
    @Override
    public E dequeue() {
        if(isEmpty())
            throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
        E res=data[front];
        data[front]=null;
        front=(front+1)%data.length;
        size--;
        if(size==getCapacity()/4&&getCapacity()/2!=0)//動態縮容
            resize(getCapacity()/2);
        return res;
    }
    
    //獲取隊首元素
    @Override
    public E getFront() {
        if(isEmpty())
            throw new IllegalArgumentException("Queue is empty.");
        return data[front];
    }
    
    //動態擴容
    private void resize(int newCapacity) {
        E[] newData=(E[])new Object[newCapacity+1];
        for(int i=0;i<size;i++) {
            newData[i]=data[(front+i)%data.length];
        }
        data=newData;
        front=0;
        rear=size;
    }
    
    
    @Override
    public String toString() {
        StringBuilder res = new StringBuilder();
        res.append(String.format("Queue: size = %d , capacity = %d\t", size, getCapacity()));
        res.append("front [");
        for(int i = front ; i != rear ; i = (i + 1) % data.length){
            res.append(data[i]);
            if((i + 1) % data.length != rear)
                res.append(", ");
        }
        res.append("] rear");
        return res.toString();
    }
    
}

再寫一個Main方法測試一下:

public static void main(String[] args) {
        LoopQueue<Integer> queue = new LoopQueue<>();
        for(int i = 0 ; i < 10 ; i ++){
            queue.enqueue(i);
            System.out.println("入隊:"+queue);

            if(i % 3 == 2){
                queue.dequeue();
                System.out.println("出隊:"+queue);
            }
        }
    }

輸出:

入隊:Queue: size = 1 , capacity = 10    front [0] rear
入隊:Queue: size = 2 , capacity = 10    front [0, 1] rear
入隊:Queue: size = 3 , capacity = 10    front [0, 1, 2] rear
出隊:Queue: size = 2 , capacity = 5    front [1, 2] rear
入隊:Queue: size = 3 , capacity = 5    front [1, 2, 3] rear
入隊:Queue: size = 4 , capacity = 5    front [1, 2, 3, 4] rear
入隊:Queue: size = 5 , capacity = 5    front [1, 2, 3, 4, 5] rear
出隊:Queue: size = 4 , capacity = 5    front [2, 3, 4, 5] rear
入隊:Queue: size = 5 , capacity = 5    front [2, 3, 4, 5, 6] rear
入隊:Queue: size = 6 , capacity = 10    front [2, 3, 4, 5, 6, 7] rear
入隊:Queue: size = 7 , capacity = 10    front [2, 3, 4, 5, 6, 7, 8] rear
出隊:Queue: size = 6 , capacity = 10    front [3, 4, 5, 6, 7, 8] rear
入隊:Queue: size = 7 , capacity = 10    front [3, 4, 5, 6, 7, 8, 9] rear

那現在,循環隊列相關的代碼就全部完成了,需要註意的是,我們實現的循環隊列是可以動態擴容的,實際上我實現的所有的線性表都是動態擴容的,希望讀者註意。

既然實現了兩種隊列,就自然的做一下比較吧,首先做一下性能比較:

測試函數如下:

public class PerformanceTest {
    private static double queueTest(Queue<Integer> q,int opCount) {
        long startTime = System.nanoTime();//當前毫秒值

        Random random = new Random();
        for(int i = 0 ; i < opCount ; i ++)
            q.enqueue(random.nextInt(Integer.MAX_VALUE));
        for(int i = 0 ; i < opCount ; i ++)
            q.dequeue();

        long endTime = System.nanoTime();

        return (endTime - startTime) / 1000000000.0;
    }
    
    public static void main(String[] args) {
        int opCount = 100000;

        ArrayQueue<Integer> arrayQueue = new ArrayQueue<>();
        double time1 = queueTest(arrayQueue, opCount);
        System.out.println("ArrayQueue, time: " + time1 + " s");

        LoopQueue<Integer> loopQueue = new LoopQueue<>();
        double time2 = queueTest(loopQueue, opCount);
        System.out.println("LoopQueue, time: " + time2 + " s");    
    }
}

輸出為:

  ArrayQueue, time: 6.924613333 s
  LoopQueue, time: 0.020527111 s

再看時間復雜度的比較:

ArrayQueue<E>
void enqueue(E)    O(1)
E dequeue()           O(n)
E front()                O(1)
int getSize()        O(1)
boolean isEmpty     O(1)

LoopQueue<E>
void enqueue(E)    O(1)
E dequeue()           O(1)
E front()                O(1)
int getSize()        O(1)
boolean isEmpty     O(1)

經過對比不難看出,循環隊列的效率要高於順序隊列,但是,也不得不說,循環隊列在實現上要復雜一些。

哈哈,真是難得,竟然一天內更了兩篇博文,繼續加油!!

對於學習,四個字概括:至死方休

<數據結構系列3>隊列的實現與變形(循環隊列)