說到隊列其實很簡單,第一反應肯定就是排隊買奶茶。排隊的一隊人就可以看成是一個隊列結構,先來的人排在前面先點餐然後拿到自己要的東西就先走了。走了以後,後面的人一個一個的向前移動。後來的人總是排在後面的。(槓精的不要說可能後來的點的餐會先好,然後比先來的先走。我們按常規來好嗎?)
隊列
所以隊列與棧結構是反正來的,隊列說:先到先得啊;而棧說:先來的沒肉次,晚點來。(兩個死對頭打一架,看誰能贏。咳咳,扯遠了,迴歸正題!)那麼順序表中隊列也是拿數組實現的,那麼它也有一個總接口,話不多說,上圖和代碼:
package 隊列;
public interface Queue<E> {
/**
* 獲取隊列的有效長度
* */
public int getSize();
/**
* 判斷當前隊列是否爲空
* */
public boolean isEmpty();
/**
* 清空隊列
* */
public void clear();
/**
* 入隊一個新元素e
* */
public void enqueue(E e);
/**
* 出隊一個元素e
* */
public E dequeue();
/**
* 獲取隊首元素(不刪除)
* */
public E getFront();
/**
* 獲取隊尾元素(不刪除)
* */
public E getRear();
}
在JavaAPI中,隊列Queue是在util包下的接口,他有一個實現子類,是用數組實現的叫ArrayDeque,我們在這裏實現這個子類,就叫ArrayQueue吧,其實也就是那ArrayList實現的。下面是它的類圖和代碼實現:
package 隊列;
import 線性表.ArrayList;
public class ArrayQueue<E> implements Queue<E> {
private ArrayList<E> list; //該隊列拿線性表實現
public ArrayQueue() { //創建一個默認的容量大小的隊列
list=new ArrayList<E>();
}
public ArrayQueue(int capacity){ //創建一個指定容量大小的隊列
list=new ArrayList<E>(capacity);
}
@Override
public int getSize() { //獲取有效元素的個數
return list.getSize(); //調用list中的方法
}
@Override
public boolean isEmpty() { //判空
return list.isEmpty();
}
@Override
public void clear() { //清空隊列
list.clear();
}
@Override
public void enqueue(E e) { //入隊一個元素
list.addLast(e); //入隊在隊尾插入元素
}
@Override
public E dequeue() { //出隊一個元素
return list.removeFirst(); //出隊刪除的是頭元素,list內部的removeFirst方法,將隊頭刪除之後,再把後面的元素都往前都移動了一個元素的單位
}
@Override
public E getFront() { //獲取隊頭元素
return list.getFirst(); //指的是數組的頭元素
}
@Override
public E getRear() { //獲取隊尾元素
return list.getLast(); //指的是有效元素的最後一個元素
}
@Override
public String toString() {
StringBuilder sb=new StringBuilder();
sb.append("ArrayQueue: size="+getSize()+",capacity="+list.getCapacity()+"\n");
if(isEmpty()){
sb.append("[]");
}else{
sb.append('[');
for(int i=0;i<getSize();i++){
sb.append(list.get(i));
if(i==getSize()-1){
sb.append(']');
}else{
sb.append(',');
}
}
}
return sb.toString();
}
@Override
public boolean equals(Object obj) {
if(obj==null){
return false;
}
if(obj==this){
return true;
}
if(obj instanceof ArrayQueue){
ArrayQueue queue=(ArrayQueue) obj;
return list.equals(queue.list); //在這裏,既然我們的隊列是拿list實現的,依然可以調用內部的方法進行比較。
}
return false;
}
}
說到棧和隊列兩個是死對頭的話,那麼棧都有雙端棧了,隊列怎麼能認輸呢!因此就出來了一個循環隊列,它比雙端棧更麻煩,要論輸贏的話,循環隊列比雙端棧更勝一籌喔。現在我們來重點說說循環隊列的結構。
循環隊列
當時用普通的隊列的時候我們發現,入隊的時候只在隊尾進行插入元素進行了,它的時間複雜度爲O(1),是比較方便的;但是在出隊的時候就沒那麼簡單方便了,隊首出去一個元素,後面的元素相對的就要往前移動。那麼算下來時間複雜度就是O(n)。爲了優化這一步,我們採取雙指針法,將一個指針設置成頭指針,表示隊頭的位置;另一個代表隊尾,表示尾部的位置。那麼每入隊一個元素,尾指針向後移動一位;每出隊一個元素,頭指針也向後移動一位,如下圖:
這樣入隊和出隊的時間複雜度都爲O(1)了,但是當達到一個特殊位置的時候,如下圖:
當達到隊尾了,Rear指針就不能繼續向後移動了,如果此時選擇擴容的話,前面會有一部分空間浪費。How to do?——循環瞭解一下。如何循環?當移到末尾的時候再將尾指針移回來,相當於一個循環的隊列:
這樣空間就不會浪費了,那麼如何判滿呢?我們發現當Rear指針移動到Front指針位置時,隊列就滿了,也是(Rear+1)%data.length==Front時,循環隊列滿了;那麼何時爲空?也就是Front移動到Rear位置時就空了(如下圖所示),那也是(Rear+1)%data.length==Front,
我去,判滿判空條件一樣?How to do?真是一波未平,一波又起啊!爲了解決這樣一個問題,我們毅然決定,浪費一個空間,將Rear指針指向元素即將進棧的位置,預留一個位置,這樣當尾指針要移動到下一個位置等於頭指針此時的位置時,也就是(Rear+1)%data.length==Front時,元素不能再進了,表示滿。當出隊Front移動到尾指針的位置,Rear==Front時,表示棧空。如下圖所示:
好了好了,該說的終於說完了,現在我們開始寫代碼:
package 隊列;
public class ArrayQueueLoop<E> implements Queue<E>{
private E[] data; //存儲數據的容器
private int front; //頭指針
private int rear; //尾指針
private int size; //元素有效個數
private static int DEFAULT_SIZE=10; //默認容量的大小
public ArrayQueueLoop() { //定義一個默認大小容量的隊列
this(DEFAULT_SIZE);
}
public ArrayQueueLoop(int capacity){ //定義一個指定大小容量的隊列
data=(E[]) new Object[capacity+1];
front=0; //頭指針在數組頭部
rear=0; //尾指針也在數組頭部
size=0; //有效元素個數爲零
}
@Override
public int getSize() { //獲取有效元素的個數
return size;
}
@Override
public boolean isEmpty() { //判空指的是頭尾指針在一起且有效元素個數爲1
return front==rear&&size==0;
}
@Override
public void clear() { //清空
size=0; //有效元素個數爲零
front=0; //頭尾指針都在數組頭
rear=0;
}
@Override
public void enqueue(E e) { //入隊
if((rear+1)%data.length==front){ //入隊判滿
//【擴容】
resize(data.length*2-1); //滿了擴容
}
data[rear]=e; //將元素入隊
rear=(rear+1)%data.length; //尾指針位置
size++;
}
private void resize(int newLen) { //擴/縮容
E[] newData=(E[]) new Object[newLen]; //新建數組
int index=0; //表示新數組角標
//遍歷元素從頭指針開始,到尾指針前結束,尾指針指向的位置有循環
for(int i=front;i!=rear;i=(i+1)%data.length){
newData[index++]=data[i];
}
front=0; //擴容後肯定有充足空間存儲,所以將頭指針放在新數組的頭部
rear=index; //尾指針位置在賦值後的角標位置
data=newData; //別忘了替換舊數組喔
}
@Override
public E dequeue() { //出隊
if(isEmpty()){ //出隊判空
throw new NullPointerException("隊列爲空!");
}
E e=data[front]; //先把這個小崽子提出來
front=(front+1)%data.length; //尾指針也會循環喔
size--; //有效元素個數減少一
if(size<=data.length/4&&data.length>DEFAULT_SIZE){
resize(data.length/2+1); //縮的太多空間浪費要考慮
}
return e; //最後將小崽子顯示出去
}
@Override
public E getFront() { //獲取頭元素
return data[front];
}
@Override
public E getRear() { //獲取尾元素
return data[(data.length+rear-1)%data.length];
}
@Override
public String toString() {
StringBuilder sb=new StringBuilder();
sb.append("ArrayQueueLoop: size="+getSize()+",capacity="+(data.length-1)+"\n");
if(isEmpty()){
sb.append("[]");
}else{
sb.append('[');
for(int i=front;i!=rear;i=(i+1)%data.length){
sb.append(data[i]);
if((i+1)%data.length==rear){
sb.append(']');
}else{
sb.append(',');
}
}
}
return sb.toString();
}
}
好了,終於把循環隊列這個骨頭啃得碎碎的了,困死我了,睡覺了!