簡介
隊列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/
最通俗的解讀,最深刻的干貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!
歡迎關注我的公眾號:「程序那些事」,懂技術,更懂你!