玩轉資料結構(04)--佇列
佇列(Queue)
一、基本概念
佇列是一種線性資料結構;
相比陣列,佇列對應的操作也是陣列的子集;
只能從一端(隊尾)新增元素,只能從另一端(隊首)取出元素; 圖解:佇列就相當於排隊
由上圖知,佇列是先進先出的資料結構(First In First Out [FIFO]);
二、佇列的實現
陣列佇列:
佇列的基本操作:
Queue<E>
void enqueue(E) --- 【入隊】向佇列中新增元素 時間複雜度:O(1)
E dequeue() ---【出隊】從佇列中拿出隊首元素 時間複雜度:O(n)【隊首後面的所有的元素都要移動一下】
E getFornt() ---檢視隊首元素 時間複雜度:O(1)
int getSize() ---檢視佇列中總共有多少個元素 時間複雜度:O(1)
boolean isEmpty() ---判斷佇列是否為空 時間複雜度:O(1)
示例程式碼:
Array.java(複用上章陣列的程式碼)
public class Array<E> { private E[] data; private int size; // 建構函式,傳入陣列的容量capacity構造Array public Array(int capacity){ data = (E[])new Object[capacity]; size = 0; } // 無引數的建構函式,預設陣列的容量capacity=10 public Array(){ this(10); } // 獲取陣列的容量 public int getCapacity(){ return data.length; } // 獲取陣列中的元素個數 public int getSize(){ return size; } // 返回陣列是否為空 public boolean isEmpty(){ return size == 0; } // 在index索引的位置插入一個新元素e public void add(int index, E e){ if(index < 0 || index > size) throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size."); if(size == data.length) resize(2 * data.length); for(int i = size - 1; i >= index ; i --) data[i + 1] = data[i]; data[index] = e; size ++; } // 向所有元素後新增一個新元素 public void addLast(E e){ add(size, e); } // 在所有元素前新增一個新元素 public void addFirst(E e){ add(0, e); } // 獲取index索引位置的元素 public E get(int index){ if(index < 0 || index >= size) throw new IllegalArgumentException("Get failed. Index is illegal."); return data[index]; } public E getLast(){ return get(size - 1); } public E getFirst(){ return get(0); } // 修改index索引位置的元素為e public void set(int index, E e){ if(index < 0 || index >= size) throw new IllegalArgumentException("Set failed. Index is illegal."); data[index] = e; } // 查詢陣列中是否有元素e public boolean contains(E e){ for(int i = 0 ; i < size ; i ++){ if(data[i].equals(e)) return true; } return false; } // 查詢陣列中元素e所在的索引,如果不存在元素e,則返回-1 public int find(E e){ for(int i = 0 ; i < size ; i ++){ if(data[i].equals(e)) return i; } return -1; } // 從陣列中刪除index位置的元素, 返回刪除的元素 public E remove(int index){ if(index < 0 || index >= size) throw new IllegalArgumentException("Remove failed. Index is illegal."); E ret = data[index]; for(int i = index + 1 ; i < size ; i ++) data[i - 1] = data[i]; size --; data[size] = null; // loitering objects != memory leak 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); } // 從陣列中刪除元素e public void removeElement(E e){ int index = find(e); if(index != -1) remove(index); } @Override public String toString(){ StringBuilder res = new StringBuilder(); res.append(String.format("Array: size = %d , capacity = %d\n", size, data.length)); res.append('['); for(int i = 0 ; i < size ; i ++){ res.append(data[i]); if(i != size - 1) res.append(", "); } res.append(']'); return res.toString(); } // 將陣列空間的容量變成newCapacity大小 private void resize(int newCapacity){ E[] newData = (E[])new Object[newCapacity]; for(int i = 0 ; i < size ; i ++) newData[i] = data[i]; data = newData; } }
Queue.java
public interface Queue<E> {
int getSize(); //檢視佇列中總共有多少個元素
boolean isEmpty();//判斷佇列是否為空
void enqueue(E e);//【入隊】向佇列中新增元素
E dequeue(); //【出隊】從佇列中拿出棧頂元素
E getFront(); //檢視隊首元素
}
ArrayQueue.java
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(){ return array.getFirst(); //檢視隊首 } @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)); //將佇列的每個元素都放到 res 中 if(i != array.getSize() - 1) //如果 i 不是array 的最後一個元素 res.append(", "); } res.append("] tail"); //陣列右側是隊尾 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); //取出元素 } } } }
輸出:
2.陣列佇列的問題
刪除隊首元素(左側隊首)
a 移除佇列,後面的移動一個單位,size -1;得圖
但如果 a移除佇列後,後面的不移動,記錄目前的隊首位置為 front,隊尾為 tail,只要維護 front 的指向即可(front++),不需要所有的元素移動一個單位,即可得到迴圈佇列這種實現方式。
3.迴圈佇列
front == tail --- 佇列為空時【起始狀態如下圖所示】
佇列進入5個元素後【front不變,tail 右移即可(tail++)】
將 a 移除佇列,【tail不變,front 右移即可(front++)】其餘元素不必移動
繼續加入元素到佇列中,裝滿後面的空間,前面還有空著的空間,tail 就會移動到前面 0 的位置【環形結構】
(tail+1)%c == front ---佇列滿時 ,效果如圖【capacity 中有意識的浪費一個空間】
佇列的基本操作:
Queue<E>
void enqueue(E) --- 【入隊】向佇列中新增元素 時間複雜度:O(1)【均攤】
E dequeue() ---【出隊】從佇列中拿出隊首元素 時間複雜度:O(1)【均攤】
E getFornt() ---檢視隊首元素 時間複雜度:O(1)
int getSize() ---檢視佇列中總共有多少個元素 時間複雜度:O(1)
boolean isEmpty() ---判斷佇列是否為空 時間複雜度:O(1)
迴圈佇列的實現
示例程式碼:
Queue.java
public interface Queue<E> {
int getSize();
boolean isEmpty();
void enqueue(E e);
E dequeue();
E getFront();
}
LoopQueue.java
public class LoopQueue<E> implements Queue<E> {
private E[] data;
private int front, tail;
private int size; // 有興趣的同學,在完成這一章後,可以思考一下:
// LoopQueue中不宣告size,如何完成所有的邏輯?
// 這個問題可能會比大家想象的要難一點點:)
public LoopQueue(int capacity){ //定義陣列容積capacity
data = (E[])new Object[capacity + 1]; //迴圈陣列中有意識的浪費一個空間
front = 0;
tail = 0;
size = 0;
}
public LoopQueue(){ //無引數的建構函式
this(10);
}
public int getCapacity(){ //迴圈佇列中最多裝載的元素數量
return data.length - 1;
}
@Override
public boolean isEmpty(){
return front == tail;
}
@Override
public int getSize(){
return size;
}
@Override //(新增程式碼)
public void enqueue(E e){ //迴圈佇列入隊
if((tail + 1) % data.length == front) //判斷佇列是否“滿”
resize(getCapacity() * 2); //佇列擴容
data[tail] = e;
tail = (tail + 1) % data.length;
size ++;
}
@Override //(新增程式碼)
public E dequeue(){ //迴圈隊列出隊
if(isEmpty())
throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
E ret = data[front]; //出隊的元素是隊首元素
data[front] = null;
front = (front + 1) % data.length;
size --;
if(size == getCapacity() / 4 && getCapacity() / 2 != 0) //佇列容量要自動縮減
resize(getCapacity() / 2);
return ret;
}
@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[(i + front) % data.length]; //將data中的size個元素放到了newData中的[0,size-1]的位置
data = newData;
front = 0;
tail = size;
}
@Override//(新增程式碼)
public String toString(){ //列印輸出
StringBuilder res = new StringBuilder();
res.append(String.format("Queue: size = %d , capacity = %d\n", size, getCapacity()));
res.append("front ["); //佇列左側是隊首
for(int i = front ; i != tail ; i = (i + 1) % data.length){ //遍歷方式二
res.append(data[i]);
if((i + 1) % data.length != tail) //判斷當前索引不是最後一個元素
res.append(", ");
}
res.append("] tail"); //佇列右側是隊尾
return res.toString();
}
public static void main(String[] args){//(新增程式碼)
LoopQueue<Integer> queue = new LoopQueue<>(); //新增測試用例
for(int i = 0 ; i < 10 ; i ++){
queue.enqueue(i); //將0-9這10個數字存放到 queue 中
System.out.println(queue);
if(i % 3 == 2){
queue.dequeue(); //每隔三個數字執行出隊操作
System.out.println(queue);
}
}
}
}
輸出:
三、陣列佇列和迴圈佇列的比較(執行效率)
示例程式碼:Main.java
import java.util.Random;
public class Main {
// 測試使用q執行opCount個enqueueu和dequeue操作所需要的時間,單位:秒
private static double testQueue(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 = testQueue(arrayQueue, opCount);
System.out.println("ArrayQueue, time: " + time1 + " s");
LoopQueue<Integer> loopQueue = new LoopQueue<>(); //迴圈佇列輸出時間
double time2 = testQueue(loopQueue, opCount);
System.out.println("LoopQueue, time: " + time2 + " s");
}
}
輸出:
陣列佇列執行10萬個佇列入隊出隊所需的時間遠遠大於迴圈佇列所需的時間
主要的差距在出隊的過程中,陣列佇列 每一次出隊後面所有的元素都要向前挪動一個位置,時間複雜度為O(n),則對於testQueue來說是O(n2);迴圈佇列 則無需挪動位置,時間複雜度為O(n),對於testQueue來說是O(n).