資料結構——佇列Queue(陣列佇列和迴圈佇列)
什麼是佇列?
佇列是一種線性的資料結構【線性資料結構:陣列、棧、佇列】
相比陣列,佇列對應的資料操作是陣列的子集。
只能從一端(隊尾)新增元素,只能從另一端(隊首)取出元素。
陣列佇列
程式碼實現
Array陣列類
package cn.itcats.queue; public class Array<E> { private E[] data; private int size; public Array(int capacity) { data =(E[]) new Object[capacity]; size = 0; } public Array() { //空引數構造預設的capacity為10 this(10); } //獲取陣列元素中元素個數 public int getSize() { return this.size; } //獲得陣列容量 public int getCapacity() { return data.length; } //判斷陣列是否為空 public boolean isEmpty() { return size == 0; } //向陣列尾部新增元素 public void addLast(E e) { add(size, e); } //向陣列頭部新增元素 public void addFirst(E e) { add(0, e); } //向陣列中index索引處插入某個元素 public void add(int index, E e) { //檢查陣列中是否能容納新的元素 if (size == data.length) System.out.println("陣列需要擴容"); resize(data.length * 2); if (index < 0 || index > size) throw new IllegalArgumentException("index非法"); //移動元素 for (int i = size - 1; i >= index; i--) { //後一個索引賦上前一個索引的元素,即每一個元素都向後挪了一個位置 data[i + 1] = data[i]; } data[index] = e; size++; } //獲取陣列中的值 E get(int index) { if (index < 0 || index >= size) throw new IllegalArgumentException("index非法"); return data[index]; } //獲取陣列中的值 E getLast() { return get(size - 1); } //獲取陣列中的值 E getFirst() { return get(0); } //更新陣列的值 E update(int index, E e) { E oldValue = get(index); data[index] = e; return oldValue; } //陣列中是否含有某元素,有返回true,無返回false public boolean contains(E e) { for (int i = 0; i < size; i++) { if (data[i].equals(e)) return true; } return false; } //查詢陣列中的某個元素,找到返回索引,找不到返回-1 public int find(E e) { for (int i = 0; i < size; i++) { if (data[i].equals(e)) { return i; } } return -1; } //刪除陣列中某個元素 public E remove(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("index非法"); } E ret = data[index]; for (int i = index + 1; i < size; i++) { data[i - 1] = data[i]; } size--; data[size] = null; if(size == data.length / 4 && data.length / 2 != 0 ) resize(data.length /2); return ret; } public E removeFirst() { return remove(0); } public E removeLast() { return remove(size - 1); } //刪除指定元素,如果有則刪除,刪除成功返回true,刪除失敗返回false public boolean removeElement(E e) { int index = find(e); if(index != -1){ remove(index); return true; } return false; } //實現動態陣列,動態擴容 size==data.lenngth 擴容2倍 和 縮容 size == data.length / 2 private void resize(int newCapacity){ //建立一個新的陣列 E[] newData = (E[]) new Object[newCapacity]; //把原來的元素遷移到新的陣列中 for(int i = 0 ; i < size ; i++){ newData[i] = data[i]; } data = newData; } //列印陣列中的元素 @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(String.format("Array: size = %d , capacity = %d\n", size, data.length)); //顯示[1,2,3,4,5] sb.append("["); for (int i = 0; i < size; i++) { sb.append(data[i]); if (i != size - 1) { sb.append(", "); } } sb.append("]"); return sb.toString(); } }
Queue介面
package cn.itcats.queue;
public interface Queue<E> {
//入隊操作
void enqueue(E e);
//出隊操作
E dequeue();
//獲取隊首元素
E getFront();
//獲取佇列中元素個數
int getSize();
//判斷佇列是否為空
boolean isEmpty();
}
基於陣列實現的Queue
package cn.itcats.queue; 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 void enqueue(E e) { array.addLast(e); } @Override public E dequeue() { return array.removeFirst(); } @Override public E getFront() { return array.getFirst(); } @Override public int getSize(){ return array.getSize(); } @Override public boolean isEmpty() { return array.isEmpty(); } public int getCapacity(){ return array.getCapacity(); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(String.format("Queue: ")); sb.append("front ["); for (int i = 0; i < array.getSize(); i++) { sb.append(array.get(i)); if (i != array.getSize() - 1) { sb.append(", "); } } sb.append("] tail"); return sb.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("i:"+i +" "+queue); if(i % 3 == 2){ queue.dequeue(); System.out.println("i:"+i +" "+queue); } } } }
使用陣列實現佇列的複雜度分析:
可以看到,因為每次出隊都呼叫removeFirst()方法,時間複雜度O(n),那麼有沒有什麼方法可以實現入隊和出隊時間複雜度都是O(1)呢?答案是有的,下面我們就來說說迴圈佇列
迴圈佇列
由於陣列佇列的出隊時間複雜度都為O(n),原因是每次出隊時都需要移動元素進行賦值操作,那麼迴圈佇列便產生了。
迴圈佇列內部維護了兩個指標,front和tail。
1、在初始化時,front和tail都指向索引為0的位置,所以當front==tail時,佇列為空。
2、每入隊一個元素,tail++,每出隊一個元素,front++。
3、到tail++到末尾時,若開頭還有可利用的空間,則7之後的索引其實是0。 index = (7+1) % capacitySize == 0
3、如下圖所示,如果在索引1處放置一個元素,tail++,此時tail==front,但此時佇列並不為空,這並不是我們想看見的,所以陣列中需要浪費一個索引空間。當(tail+1) % capacity ==front時,佇列為滿,所以索引1不放元素。有些人可能疑問為什麼不是tail+1 == front呢?當tail==7時,再加入一個元素,靠的就是取模運算把下一個tail放在索引0位置的,當tail為0,front為7時,佇列也是滿的,而7+1 != front,不能判斷佇列是滿的,所以需要(tail+1) % capacity ==front,判斷佇列為滿。
程式碼實現:
package cn.itcats.queue;
/**
* 迴圈佇列 基於陣列
* @param <E>
*/
public class LoopQueue<E> implements Queue<E> {
private E[] data;
private int front , tail;
private int size;
public LoopQueue(int capacity){
//因為迴圈佇列我們有意識的浪費一個空間,所以長度+1
data = (E[]) new Object[capacity + 1];
front = 0;
tail = 0;
size = 0;
}
public LoopQueue(){
this(10);
}
//入隊操作
@Override
public void enqueue(E e) {
//入隊之前判斷佇列是否為滿
if(front == (tail+1) % data.length){
//佇列滿了,擴容
resize(getCapacity() * 2);
}
data[tail] = e;
tail = (tail + 1) % data.length;
size++;
}
//出隊操作
@Override
public E dequeue() {
//佇列為空
if(isEmpty())
throw new IllegalArgumentException("佇列為空,無法出隊");
//佇列不為空,出隊元素
E e = data[front];
//出隊後把元素置空
data[front] = null;
front = (front + 1) % data.length;
size -- ;
//若出隊到一定數量時,動態縮小容量
if(size == getCapacity() / 4 && getCapacity() / 2 != 0 ){
resize(getCapacity() / 2);
}
return e;
}
//擴容過程
private void resize(int newCapacity){
//構建一個新的陣列 長度為原來可用空間的兩倍
E[] newData = (E[]) new Object[newCapacity + 1];
//遷移陣列
for(int i = 0 ; i < size ; i++){
//把原來front位置的元素依次遷移到新陣列0開始的位置,存在偏移量front
newData[i] = data[(i+front) % data.length];
}
//改變引用
data = newData;
//front和tail歸位
front = 0 ;
tail = size;
}
//檢視隊首元素,但不出隊
@Override
public E getFront() {
//佇列為空
if(isEmpty())
throw new IllegalArgumentException("佇列為空,無法檢視隊首元素");
return data[front];
}
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
return front == tail;
}
public int getCapacity(){
return data.length - 1;
}
//列印陣列中的元素
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(String.format("LoopQueue: size = %d , capacity = %d\n", size, getCapacity()));
//顯示[1,2,3,4,5]
sb.append("front [");
for (int i = front; i != tail; i = (i+1) % data.length) {
sb.append(data[i]);
//如果當前索引不是最後一個元素
if ( (i+1) % data.length != tail) {
sb.append(", ");
}
}
sb.append("] tail");
return sb.toString();
}
//測試方法
public static void main(String[] args) {
LoopQueue<Integer> queue = new LoopQueue<>();
for(int i = 0 ; i < 10 ;i ++){
queue.enqueue(i);
System.out.println("i:"+i +" "+queue);
if(i % 3 == 2){
queue.dequeue();
System.out.println("i:"+i +" "+queue);
}
}
}
}
陣列佇列與迴圈佇列的比較
1、迴圈佇列相比陣列佇列底層都是基於陣列,但迴圈佇列編碼更復雜一些,它引入了front和tail指標。
2、陣列隊列出隊時間複雜度為O(n),而迴圈隊列出隊時間複雜度為O(1)。【均攤複雜度】,因為可能觸發縮容操作。
為此,我們可以對二者進行效能測試。
package cn.itcats.queue;
import java.util.Random;
public class SpeedTest {
//模擬執行count次入隊和出隊所需時間,單位: s 秒
public static double speedTest(Queue<Integer> queue, int count){
long startTime = System.nanoTime();
Random random = new Random();
for(int i = 0 ; i < count ; i++)
queue.enqueue(random.nextInt(Integer.MAX_VALUE));
for(int i = 0 ; i < count ; i++)
queue.dequeue();
long endTime = System.nanoTime();
return (endTime - startTime) /1000000000.0 ;
}
public static void main(String[] args) {
//進行入隊和出隊的次數
int count = 2000000;
//使用陣列佇列ArrayQueue
Queue arrayQueue = new ArrayQueue();
System.out.println("ArrayQueue陣列佇列執行的時間: "+speedTest(arrayQueue,count)+" s");
//使用迴圈佇列LoopQueue
Queue loopQueue = new LoopQueue();
System.out.println("LoopQueue迴圈佇列執行的時間: " + speedTest(loopQueue,count)+" s");
}
}
發現loopQueue要比arrayQueue快上百倍。