隊列
聊起隊列,你一定會聯想到一個與隊列相似的數據結構:棧
。
爲了更好的理解什麼是隊列,我們將它和棧來比較一下:
隊列的特點是:先進先出
,如下圖,1先進,1就先出。
圖1:隊列的圖解
棧的特點是:先進後出
,如下圖,1先進,1卻最後出。
圖2:棧的圖解
爲了讓你更好的區分與理解隊列與棧,你只記住這個口訣:喫多了拉就是隊列,喫多了吐就是棧
。
哈哈哈,千萬不要告訴別人這是我告訴你的....
數組實現隊列,難在哪?
數組實現隊列,乍一看,很貼合的一種方式去實現隊列。仔細推敲一下你會發現並不簡單。
爲了更看清數組存在什麼問題,我先new一個size爲4的數組:
圖3:大小爲4的數組
下一步,向數組中入列4個元素,數組按照0、1、2、3的下標被填充:
圖4:隊列寫滿
繼續,消費兩個元素,我們按照先進先出的原則,消費掉下標爲0、1的元素,如圖:
圖5:從隊列消費2個元素
如果繼續入列,你會發現:最大的下標3已經被佔用,而數組依然存在空間。無法入列,造成空間浪費!
數組隊列如何解決困難?
思維活躍的程序員,不可能因爲遇到困難而畏懼!更不可能存在空間,而無法使用。就像飢腸轆轆,食物擺在眼前,卻不知道如何喫掉它!
方案一:擴
瞭解過ArrayList
、HashMap
的朋友肯定知道空間不夠,那就擴
。當發現空間不夠用,我們是否可以將空間擴成原來的2倍?
,像下圖這樣:
圖6:數組擴容
是辦法,但是不是最好的辦法。因爲下標爲0和1的空間,無法利用和回收,造成浪費!
,不妥!也不可取!有人說無所謂,錢不是問題,金主直接return~
方案二:移
既然存在空間無法利用,那發現數組空間滿了之後,是否可以將元素進行挪動
呢?如圖:
圖7:數組元素移動
是辦法,是比方案一更好的辦法,但依然不是最好的辦法。那這又爲什麼呢?想想,兩個數據的挪動,貌似沒有什麼成本,但是如果是1萬甚至更多元素挪動,會帶來什麼呢?對!更高的時間複雜度,即o(n)。
除了擴
和移
,真的沒有其他的辦法?有的,今天給大家介紹一個新玩法:循環隊列
。
循環數組隊列
說起循環,大家肯定腦海中呈現出頭尾相連的一個環,就是循環。是的!循環數組隊列就是這樣的結構,將你認知的連續的長長的數組,抽象成一個環
,就是下圖:
圖8:循環數組隊列
在圖5數組隊列的基礎上,繼續入列一個元素,如下圖:
圖9:循環數組隊列追加元素
入列後的數組,你試着把它想象成一個環狀,如果再繼續入列元素,會放到下標爲1的位置。
因此循環數組隊列很好地解決了之前兩種方案的缺點:
1.空間浪費無法充分利用
2.數據遷移導致的o(n)的時間複雜度
如何實現循環數組隊列
屬性定義&說明
實現循環數組隊列,需要定義幾個關鍵的屬性,以幫助我們更好地代碼實現:
-
capicity
隊列的容量大小 -
items
定義的數組,容納元素 -
head
出列的頭部數組下標,指向下次出列的位置 -
tail
入列的尾部數組下標,指向下次入列的位置 -
size
隊列入列的元素個數
代碼實現
<pre class="custom" data-tool="mdnice編輯器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`public class LoopQueue<T> {
// 容量
private int capicity;
// 隊列元素
private Object[] items;
// 頭部位置
private int head = 0;
// 尾部位置
private int tail = 0;
// 大小
private int size = 0;
public LoopQueue(int capicity) {
this.capicity = capicity;
this.items = new Object[capicity];
}
public boolean enqueue(T t) {
// 如果超出容量,直接返回入列失敗
if (size >= capicity) {
return false;
}
// 到尾部了,但是還沒有滿,tail重置
if (tail == capicity) {
tail = 0;
}
// 數組tail位置放置元素,並將tail自增
this.items[tail++] = t;
// 大小累加
size++;
return true;
}
public T dequeue() {
// 隊列無元素,直接返回空
if (size <= 0) {
return null;
}
// 到尾部了,但是還沒有滿,head重置
if (head == capicity) {
head = 0;
}
// size自減少
size--;
// 獲取元素並自增head
return (T) this.items[head++];
}
public static void main(String[] args) {
LoopQueue<Integer> queue = new LoopQueue<Integer>(6);
for (int i = 0; i < 7; i++) {
System.out.println(queue.enqueue(i));
}
for (int i = 0; i < 7; i++) {
System.out.println(queue.dequeue());
}
}
}` </pre>
上述代碼,已經增加了註釋,你可以直接拷貝試驗下。
main方法中可直接運行測試:定義容量爲6的數組隊列,入列7次後,出列7次
,運行結果如圖:
圖10:循環數組運行結果
-
位置1
因爲空間已滿,無法入列,返回false -
位置2
因爲隊列已空,出列的元素爲空
ArrayBlockingQueue解密
爲了研究下數組隊列在JDK中如何實現的,我特地看了下ArrayBlockingQueue
的源碼:
- put方法入口,當空間滿了,
阻塞
:
圖11:put方法
- 如果空間沒有滿,進入第2步,
enqueue方法
,實現邏輯也採用的循環數組隊列
:
圖12:enqueue方法
寫在最後
數組隊列用了較長的篇幅在討論,採用循環的思路很優雅地解決了空間利用問題。
不知道你有沒有發現一個問題,JDK中的ArrayBlockingQueue
的構造函數全部都需要初始化capacity
:
圖13:ArrayBlockingQueue構造函數
因爲使用數組需要初始化空間,所以需要初始化capacity。
留一道思考題:有沒有實現無限空間的隊列的方法呢?
不妨打開JDK尋找下答案~
我是公衆號【面試怪圈】的keaizhuzhu,關注我~