cover

Deque 可以同時當 Stack 和 Queue 用——而且 Java 官方推薦用它取代那個過時的 Stack 類。

先講結論

Deque(Double-Ended Queue)兩端都可以插入和刪除,所有操作 O(1)。它是 Stack 和 Queue 的超集——前端操作就是 Queue,後端操作就是 Stack,兩端都用就是 Deque。最殺手級的應用是「滑動視窗最大值」。

兩端都能操作

  addFirst →  ┌───┬───┬───┬───┐  ← addLast
removeFirst ← │ A │ B │ C │ D │  → removeLast
               └───┴───┴───┴───┘
              front              rear

Stack 只能從一端操作,Queue 從兩端但一端進一端出。Deque 兩端都能進出,是最靈活的線性結構。

實作方式

雙向鏈表:每個節點有 prev 和 next,兩端操作都 O(1)。直觀但 cache 不友善。

環形陣列:跟 Queue 的環形陣列一樣,但 front 可以往前也可以往後。

addFirst → front = (front - 1 + capacity) % capacity
addLast  → tail = (tail + 1) % capacity

Java 的 ArrayDeque 就是環形陣列版,實務上幾乎總是首選。

滑動視窗最大值:Deque 的殺手應用

LeetCode 239。給一個陣列和視窗大小 k,求每個視窗位置的最大值。

暴力法每個視窗都掃一遍是 O(nk)。用一個單調遞減的 Deque 可以做到 O(n):

Deque<Integer> deque = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
    // 把超出視窗的踢掉(從前端)
    while (!deque.isEmpty() && deque.peekFirst() < i - k + 1)
        deque.pollFirst();
    // 比新元素小的都沒用了,踢掉(從後端)
    while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i])
        deque.pollLast();
    deque.offerLast(i);
    if (i >= k - 1) result[i-k+1] = nums[deque.peekFirst()];
}

Deque 裡存的是 index。前端永遠是當前視窗的最大值。新元素進來時,把所有比它小的從後端踢掉——因為它們不可能再成為任何未來視窗的最大值了。

這個技巧叫「單調隊列」(Monotonic Queue),在競賽和面試中出現頻率很高。

為什麼 Java 推薦 ArrayDeque 取代 Stack?

Java 的 Stack 類繼承自 Vector,每個方法都加了 synchronized——在單執行緒場景這是無謂的效能損失。ArrayDeque 沒有這個問題,而且底層是環形陣列,比 Vector 的動態陣列更高效。

// 不要這樣
Stack<Integer> stack = new Stack<>();
 
// 要這樣
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.pop();

Java 的 legacy API 有多少坑?Stack 只是冰山一角。


Deque 是資料結構界的瑞士刀——它什麼都能做,但最閃亮的那一刻是解滑動視窗問題。