<數據結構系列3>隊列的實現與變形(循環隊列)
數據結構第三課了,今天我們再介紹一種很常見的線性表——隊列
就像它的名字,隊列這種數據結構就如同生活中的排隊一樣,隊首出隊,隊尾進隊。以下一段是百度百科中對隊列的解釋:
隊列是一種特殊的線性表,特殊之處在於它只允許在表的前端(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>隊列的實現與變形(循環隊列)