簡介
佇列Queue是一個非常常見的資料結構,所謂佇列就是先進先出的序列結構。
想象一下我們日常的排隊買票,只能向隊尾插入資料,然後從隊頭取資料。在大型專案中常用的訊息中介軟體就是一個佇列的非常好的實現。
佇列的實現
一個佇列需要一個enQueue入佇列操作和一個DeQueue操作,當然還可以有一些輔助操作,比如isEmpty判斷佇列是否為空,isFull判斷佇列是否滿員等等。
為了實現在佇列頭和佇列尾進行方便的操作,我們需要儲存隊首和隊尾的標記。
先看一下動畫,直觀的感受一下佇列是怎麼入隊和出隊的。
先看入隊:
再看出隊:
可以看到入隊是從隊尾入,而出隊是從隊首出。
佇列的陣列實現
和棧一樣,佇列也有很多種實現方式,最基本的可以使用陣列或者連結串列來實現。
先考慮一下使用陣列來儲存資料的情況。
我們用head表示隊首的index,使用rear表示隊尾的index。
當隊尾不斷插入,隊首不斷取資料的情況下,很有可能出現下面的情況:
上面圖中,head的index已經是2了,rear已經到了陣列的最後面,再往陣列裡面插資料應該怎麼插入呢?
如果再往rear後面插入資料,head前面的兩個空間就浪費了。這時候需要我們使用迴圈陣列。
迴圈陣列怎麼實現呢?只需要把陣列的最後一個節點和陣列的最前面的一個節點連線即可。
有同學又要問了。陣列怎麼變成迴圈陣列呢?陣列又不能像連結串列那樣前後連線。
不急,我們先考慮一個餘數的概念,假如我們知道了陣列的capacity,當要想陣列插入資料的時候,我們還是照常的將rear+1,但是最後除以陣列的capacity, 隊尾變到了隊首,也就間接的實現了迴圈陣列。
看下java程式碼是怎麼實現的:
public class ArrayQueue {
//儲存資料的陣列
private int[] array;
//head索引
private int head;
//real索引
private int rear;
//陣列容量
private int capacity;
public ArrayQueue (int capacity){
this.capacity=capacity;
this.head=-1;
this.rear =-1;
this.array= new int[capacity];
}
public boolean isEmpty(){
return head == -1;
}
public boolean isFull(){
return (rear +1)%capacity==head;
}
public int getQueueSize(){
if(head == -1){
return 0;
}
return (rear +1-head+capacity)%capacity;
}
//從尾部入佇列
public void enQueue(int data){
if(isFull()){
System.out.println("Queue is full");
}else{
//從尾部插入
rear = (rear +1)%capacity;
array[rear]= data;
//如果插入之前佇列為空,將head指向real
if(head == -1 ){
head = rear;
}
}
}
//從頭部取資料
public int deQueue(){
int data;
if(isEmpty()){
System.out.println("Queue is empty");
return -1;
}else{
data= array[head];
//如果只有一個元素,則重置head和real
if(head == rear){
head= -1;
rear = -1;
}else{
head = (head+1)%capacity;
}
return data;
}
}
}
大家注意我們的enQueue和deQueue中使用的方法:
rear = (rear +1)%capacity
head = (head+1)%capacity
這兩個就是迴圈陣列的實現。
佇列的動態陣列實現
上面的實現其實有一個問題,陣列的大小是寫死的,不能夠動態擴容。我們再實現一個能夠動態擴容的動態陣列實現。
//因為是迴圈陣列,這裡不能做簡單的陣列拷貝
private void extendQueue(){
int newCapacity= capacity*2;
int[] newArray= new int[newCapacity];
//先全部拷貝
System.arraycopy(array,0,newArray,0,array.length);
//如果real<head,表示已經進行迴圈了,需要將0-head之間的資料置空,並將資料拷貝到新陣列的相應位置
if(rear< head){
for(int i=0; i< head; i++){
//重置0-head的資料
newArray[i]= -1;
//拷貝到新的位置
newArray[i+capacity]=array[i];
}
//重置real的位置
rear= rear+capacity;
//重置capacity和array
capacity=newCapacity;
array=newArray;
}
}
需要注意的是,在進行陣列擴充套件的時候,我們不能簡單的進行拷貝,因為是迴圈陣列,可能出現rear在head後面的情況。這個時候我們需要對陣列進行特殊處理。
其他部分是和普通陣列實現基本一樣的。
佇列的連結串列實現
除了使用陣列,我們還可以使用連結串列來實現佇列,只需要在頭部刪除和尾部新增即可。
看下java程式碼實現:
public class LinkedListQueue {
//head節點
private Node headNode;
//rear節點
private Node rearNode;
class Node {
int data;
Node next;
//Node的建構函式
Node(int d) {
data = d;
}
}
public boolean isEmpty(){
return headNode==null;
}
public void enQueue(int data){
Node newNode= new Node(data);
//將rearNode的next指向新插入的節點
if(rearNode !=null){
rearNode.next=newNode;
}
rearNode=newNode;
if(headNode == null){
headNode=newNode;
}
}
public int deQueue(){
int data;
if(isEmpty()){
System.out.println("Queue is empty");
return -1;
}else{
data=headNode.data;
headNode=headNode.next;
}
return data;
}
}
佇列的時間複雜度
上面的3種實現的enQueue和deQueue方法,基本上都可以立馬定位到要入佇列或者出佇列的位置,所以他們的時間複雜度是O(1)。
本文的程式碼地址:
本文已收錄於 http://www.flydean.com/12-algorithm-queue/
最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!
歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!