Chapter 12 · Stacks and queues

Monotonic order, mins, and recency

A stack remembers waiters in order. A deque remembers a window max. LRU is a map plus a doubly linked recency list. Job 10454435 still none.

← Linked Lists · Hashing →

Next greater — monotonic stackSimple: right-to-left values. Interview: left-to-right indices.
Min stackPair on one deque, or an aux deque of mins.
Sliding window maxDecreasing deque of indices; front is the window max.
LRU cacheHashMap + doubly linked list. Dummy head and tail in the interview block.
Deque, not Stack Live Code: ArrayDeque. Three-page visit sequences are a HashMap of triples — that card lives in chapter 13, not here. Job 10454435 still none.

Next greater element — monotonic stack

Recognize it from — for every index, the next strictly larger value to the right (or −1). Daily temperatures is the same stack with distances. Asteroid collision and remove-k-digits are monotonic stacks with a different pop rule. Maximal rectangle’s histogram pass is also a monotonic stack of indices.

What it actually does — you keep a stack of candidates that have not yet seen a greater value. A new number pops everyone it is greater than (those people just found their answer) and then it waits on the stack. Each index is pushed once and popped once, so the nested while is still O(n).

Right-to-left with a stack of values is the easy picture: the stack top is the nearest larger value already seen to the right. Left-to-right with a stack of indices is the Live Code shape: the stack holds people still waiting, and when a greater value arrives you write ans[index] = that value.

Simple — read this first

int[] nextGreaterRtl(int[] a) {
    int n = a.length;
    int[] ans = new int[n];
    Deque<Integer> st = new ArrayDeque<>(); // values, decreasing toward the top
    for (int i = n - 1; i >= 0; i--) {      // walk from the right so the stack is "what is to my right"
        while (!st.isEmpty() && st.peek() <= a[i]) st.pop(); // not strictly greater — discard
        ans[i] = st.isEmpty() ? -1 : st.peek();              // top is the nearest greater to the right
        st.push(a[i]);                                       // we become a candidate for the left
    }
    return ans;
}

Interview — type this

int[] nextGreater(int[] a) {
    int n = a.length;
    int[] ans = new int[n];
    Arrays.fill(ans, -1);                   // leftover indices never find a greater
    Deque<Integer> st = new ArrayDeque<>(); // indices; a[st] is decreasing toward the top
    for (int i = 0; i < n; i++) {           // left to right: i is a candidate "next greater" for waiters
        while (!st.isEmpty() && a[st.peek()] < a[i]) {
            ans[st.pop()] = a[i];           // popped index just found its next greater
        }
        st.push(i);                         // i still waits for someone greater than a[i]
    }
    return ans;                             // anything still on the stack keeps -1
}
Example 1 — simple

Input [2, 1, 2]. Right-to-left stack of values. Next greater of the first 2 is not the last 2 — equal is not greater.

index 0 1 2 value 2 1 2
RTL values — [2, 1, 2]
ia[i]Pop while top ≤ a[i]ans[i]PushStack (top on the right)
22empty−12[2]
112 ≰ 1, keep21[2, 1]
02pop 1 (1 ≤ 2), pop 2 (2 ≤ 2)−12[2]

Answer: [-1, 2, -1].

Example 2 — every step

Input [2, 1, 2, 4, 3]. Interview code: left-to-right stack of indices. Every push and pop.

index 0 1 2 3 4 value 2 1 2 4 3 want 4 2 4 -1 -1
LTR indices — every push / pop on [2, 1, 2, 4, 3]
ia[i]While top value < a[i]ans writesPush iStack of indices (values)
02empty—push 0[0] (2)
11a[0]=2 ≮ 1—push 1[0, 1] (2, 1)
22pop 1 because 1 < 2; a[0]=2 ≮ 2ans[1] = 2push 2[0, 2] (2, 2)
34pop 2 (2 < 4); pop 0 (2 < 4)ans[2] = 4, ans[0] = 4push 3[3] (4)
43a[3]=4 ≮ 3—push 4[3, 4] (4, 3)
end—leftover 3 and 4ans[3] and ans[4] stay −1—[3, 4]

Answer: [4, 2, 4, -1, -1].

Time O(n) Space O(n) stack + answer
Why a monotonic stack Nested scan to the right is O(n²). A decreasing stack remembers “everyone still waiting” in decreasing order, so a new greater value can satisfy a whole prefix of waiters in amortized O(1). Heap loses left-to-right order. Sorting loses positions.
Common mistakes
  • Using <= vs < wrong: next greater must pop equals; next greater or equal is a different prompt.
  • Stack of values when you later need the index (daily temperatures = j − i).
  • Forgetting to fill −1. Interview block fills first so leftovers are correct.
ProblemPatternLinks
Next greater elementMonotonic decreasing stack — ask variant (one array / two arrays / circular)Java in ch.05 · cheat card · clue · R4 coding
Daily temperaturesSame stack; store distance not valuecheat card
Asteroid collisionStack; opposite signs fightcheat card
Remove k digitsMonotonic increasing stack of digitsJava in ch.05 · cheat card · clue
Maximal rectangleHistogram heights + monotonic stack of indicesJava in ch.05 · cheat card · clue

Min stack — pair or aux

Recognize it from — push, pop, top, and getMin all O(1). “One stack only” is a tighter constraint (encode the min). Get-middle of a stack is a different structure (DLL or two stacks).

What it actually does — a normal stack does not know the minimum without a scan. You remember the min as of each height of the stack. Pair version: each entry is (value, min of the prefix including that value). Aux version: a second deque that always has the same size (or that only pushes when the min changes). Pop both in lockstep. Duplicates of the min must still be recorded — if you skip pushing a duplicate min, a pop of the last copy loses the min.

Simple — read this first

class MinStackPair {
    Deque<int[]> st = new ArrayDeque<>(); // [value, minSoFar]
    void push(int x) {
        int m = st.isEmpty() ? x : Math.min(x, st.peek()[1]);
        st.push(new int[]{x, m});         // min rides along with this height
    }
    void pop() { st.pop(); }
    int top() { return st.peek()[0]; }
    int getMin() { return st.peek()[1]; } // O(1) — no scan
}

Interview — type this

class MinStack {
    Deque<Integer> st = new ArrayDeque<>();
    Deque<Integer> mins = new ArrayDeque<>(); // mins.peek() = getMin of current st
    void push(int x) {
        st.push(x);
        mins.push(mins.isEmpty() ? x : Math.min(x, mins.peek())); // duplicate mins still pushed
    }
    void pop() { st.pop(); mins.pop(); }
    int top() { return st.peek(); }
    int getMin() { return mins.peek(); }
}
Example 1 — simple

Ops: push 3, push 5, push 2, pop. Pair deque.

push 3, 5, 2, pop
Opst pairs [val, min]topgetMin
push 3[3, 3]33
push 5[3, 3], [5, 3]53
push 2[3, 3], [5, 3], [2, 2]22
pop[3, 3], [5, 3]53

Answer after pop: top = 5, min = 3.

Example 2 — every step

Same ops, aux stack, getMin after every op.

getMin after each op — aux deque
OpstminsgetMin now
push 3[3][3]3
push 5[3, 5][3, 3]3
push 2[3, 5, 2][3, 3, 2]2
pop[3, 5][3, 3]3

Answer: getMin sequence 3, 3, 2, 3.

Each op O(1) Space O(n)
Why an extra min per height Scanning on getMin is O(n). The min of a stack only changes at the top: it is either the new value or the previous min. Storing that next to each height (or in an aux deque of the same size) makes getMin a peek. One-stack encoding (2L * x - min) is the follow-up if they ban the aux structure.
Common mistakes
  • Aux stack that only pushes when x < min, skipping equals — pop of a duplicate min is wrong.
  • java.util.Stack (legacy). Use ArrayDeque.
  • Calling getMin / pop on empty without a contract. Ask.
ProblemPatternLinks
Min stackPair or aux; one-stack encoding if they ban auxJava in ch.05 · cheat card
Stack get middleDLL or two stacks — not getMinJava in ch.05 · cheat card
Basic calculator / OODOperand + operator stacks (or recursive descent)Java in ch.05 · cheat card

Sliding window maximum — deque

Recognize it from — array a, window size k, max of every a[i .. i+k-1]. Warehouse / “max of k days” story wrap is still this. A heap per window is slower; the deque is O(n).

What it actually does — a deque of indices, values decreasing from front to back. The front is the max of the current window. Before you insert i: drop indices that slid out (≤ i−k) from the front; drop indices whose values are ≤ a[i] from the back (they can never be max while i is in the window). Then offer i. Once i ≥ k−1, record a[dq.peekFirst()].

Simple — read this first

int[] maxWindowBrute(int[] a, int k) {
    int n = a.length;
    int[] ans = new int[n - k + 1];
    for (int i = 0; i + k <= n; i++) {   // each window start
        int m = a[i];
        for (int j = i + 1; j < i + k; j++) m = Math.max(m, a[j]);
        ans[i] = m;
    }
    return ans; // O(n k) — fine to type first, then upgrade
}

Interview — type this

int[] maxSlidingWindow(int[] a, int k) {
    int n = a.length;
    int[] ans = new int[n - k + 1];
    Deque<Integer> dq = new ArrayDeque<>(); // indices; a[idx] decreasing front → back
    for (int i = 0; i < n; i++) {
        if (!dq.isEmpty() && dq.peekFirst() <= i - k) dq.pollFirst(); // slid out of the window
        while (!dq.isEmpty() && a[dq.peekLast()] <= a[i]) dq.pollLast(); // useless as max
        dq.offerLast(i);
        if (i >= k - 1) ans[i - k + 1] = a[dq.peekFirst()]; // front is the window max
    }
    return ans;
}
Example 1 — simple

a = [1, 3, −1], k = 2. Two windows: [1,3] max 3; [3,−1] max 3.

k = 2 on [1, 3, −1]
ia[i]Deque idx (vals)Window ready?ans
01[0] (1)no—
13poll last 0 (1 ≤ 3); [1] (3)yes, max 3[3]
2−11 > 2−2 so keep; −1 < 3; [1,2] (3,−1)yes, max 3[3, 3]

Answer: [3, 3].

Example 2 — every step

a = [1, 3, −1, −3, 5], k = 3. Every pollFirst / pollLast.

windows of 3: [1,3,-1]=3 [3,-1,-3]=3 [-1,-3,5]=5
Every deque op — [1, 3, −1, −3, 5], k = 3
ia[i]Drop front if idx ≤ i−3Drop back while val ≤ a[i]Deque after offerans[i−2]
01——[0] (1)—
13—poll 0 (1 ≤ 3)[1] (3)—
2−1—−1 < 3, keep[1, 2] (3, −1)3
3−31 ≤ 0? no−3 < −1, keep[1, 2, 3] (3, −1, −3)3
451 ≤ 1 → pollFirst 1poll 3 (−3≤5), poll 2 (−1≤5)[4] (5)5

Answer: [3, 3, 5].

Time O(n) Space O(k) deque
Why a deque of indices Values alone cannot tell you when an element left the window. Indices do. Decreasing order means the max is always the front, and a new larger value can wipe the back in amortized O(1). A heap of the window is O(n log k) with lazy deletes — mention it, then type the deque.
Common mistakes
  • Storing values in the deque, then not knowing which index expired.
  • Using < instead of <= on the back: equal older values can never beat a newer equal, and they expire later — pop them.
  • Writing ans before i ≥ k−1.
ProblemPatternLinks
Sliding window maximumDecreasing deque of indicesJava in ch.05 · cheat card

LRU cache — HashMap + doubly linked list

Recognize it from — capacity k, get/put O(1), evict the least recently used key. Get counts as a use. This is not Login Tracker (unverified). LFU is a different eviction (least frequent, LRU only as a tie-break).

What it actually does — the map finds the node in O(1). The doubly linked list is the recency order: next to the dummy head is most recently used; next to the dummy tail is the victim. Get or put-update unlinks the node and inserts it after the head. Put-new inserts after the head; if size > cap, unlink tail.prev and remove it from the map.

Simple — read this first

class LRUSimple {
    static class Node {
        int k, v;
        Node prev, next;
        Node(int k, int v) { this.k = k; this.v = v; }
    }
    int cap;
    Map<Integer, Node> map = new HashMap<>();
    Node head, tail; // head = MRU, tail = LRU; null edges instead of dummies
    LRUSimple(int cap) { this.cap = cap; }
    void moveToFront(Node n) {         // sketch: unlink, then n becomes head
        if (n == head) return;
        if (n.prev != null) n.prev.next = n.next;
        if (n.next != null) n.next.prev = n.prev;
        if (n == tail) tail = n.prev;
        n.prev = null;
        n.next = head;
        if (head != null) head.prev = n;
        head = n;
        if (tail == null) tail = n;
    }
    int get(int k) {
        Node n = map.get(k);
        if (n == null) return -1;
        moveToFront(n);
        return n.v;
    }
    void put(int k, int v) {
        Node n = map.get(k);
        if (n != null) { n.v = v; moveToFront(n); return; }
        n = new Node(k, v);
        map.put(k, n);
        moveToFront(n);
        if (map.size() > cap) {        // evict tail — interview dummies skip these nulls
            map.remove(tail.k);
            Node gone = tail;
            tail = gone.prev;
            if (tail != null) tail.next = null;
            else head = null;
        }
    }
}

Interview — type this

class LRUCache {
    static class Node {
        int k, v;
        Node prev, next;
        Node(int k, int v) { this.k = k; this.v = v; }
    }
    int cap;
    Map<Integer, Node> map = new HashMap<>();
    Node head = new Node(0, 0), tail = new Node(0, 0); // dummies: never evict these
    LRUCache(int cap) {
        this.cap = cap;
        head.next = tail;
        tail.prev = head;
    }
    void unlink(Node n) {
        n.prev.next = n.next;
        n.next.prev = n.prev;
    }
    void insertAfterHead(Node n) {     // new MRU
        n.next = head.next;
        n.prev = head;
        head.next.prev = n;
        head.next = n;
    }
    int get(int key) {
        Node n = map.get(key);
        if (n == null) return -1;
        unlink(n);
        insertAfterHead(n);
        return n.v;
    }
    void put(int key, int val) {
        Node n = map.get(key);
        if (n != null) { n.v = val; unlink(n); insertAfterHead(n); return; }
        Node x = new Node(key, val);
        map.put(key, x);
        insertAfterHead(x);
        if (map.size() > cap) {
            Node lru = tail.prev;      // real LRU sits just before dummy tail
            unlink(lru);
            map.remove(lru.k);
        }
    }
}
Example 1 — simple

Capacity 1. Dummy head H, dummy tail T.

cap = 1
OpmapPointers (H = MRU side, T = LRU side)get result
start{}H ⇄ T—
put(1,1){1: n1}H ⇄ n1 ⇄ T   n1.prev=H, n1.next=T—
get(1){1: n1}unlink + insert: still H ⇄ n1 ⇄ T1
put(2,2){2: n2}evict n1 (tail.prev); H ⇄ n2 ⇄ T—
get(1){2: n2}unchanged−1

Answer: after put 2, key 1 is gone. get(1) = −1, get(2) = 2.

Example 2 — every step

Capacity 2. Sequence: put(1,1), put(2,2), get(1), put(3,3) — evict 2. Every pointer.

H = dummy head (MRU lives at H.next) T = dummy tail (LRU lives at T.prev)
cap = 2 — put 1, put 2, get 1, put 3 (evict 2)
OpH.nextchainT.prevn1.prev / nextn2.prev / nextn3.prev / next
startTH ⇄ TH———
put(1,1)n1H ⇄ n1 ⇄ Tn1H / T——
put(2,2)n2H ⇄ n2 ⇄ n1 ⇄ Tn1n2 / TH / n1—
get(1)n1H ⇄ n1 ⇄ n2 ⇄ Tn2H / n2n1 / T—
put(3,3) evict n2n3H ⇄ n3 ⇄ n1 ⇄ Tn1n3 / Tunlinked; map dropped 2H / n1

Answer: after put(3,3), map is {1, 3}. get(2) would be −1. LRU was n2 because get(1) moved n1 to the front.

get / put O(1) Space O(capacity)
Why HashMap + DLL Map gives O(1) lookup. DLL gives O(1) move-to-front and O(1) evict-from-back. ArrayList of pairs is O(n) to find or to shift. TreeMap by timestamp is log n and the wrong tool. Dummies mean unlink never special-cases “this node is the head.”
Common mistakes
  • Forgetting to map.remove the evicted key. Ghost entries, capacity lies.
  • Updating a value with put but not moving the node to MRU.
  • Treating this as Login Tracker or as LFU. Say the eviction rule out loud.
ProblemPatternLinks
LRU cacheHashMap + dummy-headed DLL; not Login TrackerJava in ch.05 · cheat card · clue
LFU cacheFreq buckets + LRU inside the min-freq listcheat card · clue

See also on this topic

Most-frequent three-page sequence is consecutive triples in a HashMap — chapter 13, not a stack. Job 10454435 still none.

← Linked Lists · Hashing →