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.
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.
| i | a[i] | Pop while top ≤ a[i] | ans[i] | Push | Stack (top on the right) |
|---|---|---|---|---|---|
| 2 | 2 | empty | −1 | 2 | [2] |
| 1 | 1 | 2 ≰ 1, keep | 2 | 1 | [2, 1] |
| 0 | 2 | pop 1 (1 ≤ 2), pop 2 (2 ≤ 2) | −1 | 2 | [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.
| i | a[i] | While top value < a[i] | ans writes | Push i | Stack of indices (values) |
|---|---|---|---|---|---|
| 0 | 2 | empty | — | push 0 | [0] (2) |
| 1 | 1 | a[0]=2 ≮ 1 | — | push 1 | [0, 1] (2, 1) |
| 2 | 2 | pop 1 because 1 < 2; a[0]=2 ≮ 2 | ans[1] = 2 | push 2 | [0, 2] (2, 2) |
| 3 | 4 | pop 2 (2 < 4); pop 0 (2 < 4) | ans[2] = 4, ans[0] = 4 | push 3 | [3] (4) |
| 4 | 3 | a[3]=4 ≮ 3 | — | push 4 | [3, 4] (4, 3) |
| end | — | leftover 3 and 4 | ans[3] and ans[4] stay −1 | — | [3, 4] |
Answer: [4, 2, 4, -1, -1].
- 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.
| Problem | Pattern | Links |
|---|---|---|
| Next greater element | Monotonic decreasing stack — ask variant (one array / two arrays / circular) | Java in ch.05 · cheat card · clue · R4 coding |
| Daily temperatures | Same stack; store distance not value | cheat card |
| Asteroid collision | Stack; opposite signs fight | cheat card |
| Remove k digits | Monotonic increasing stack of digits | Java in ch.05 · cheat card · clue |
| Maximal rectangle | Histogram heights + monotonic stack of indices | Java 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.
| Op | st pairs [val, min] | top | getMin |
|---|---|---|---|
| push 3 | [3, 3] | 3 | 3 |
| push 5 | [3, 3], [5, 3] | 5 | 3 |
| push 2 | [3, 3], [5, 3], [2, 2] | 2 | 2 |
| pop | [3, 3], [5, 3] | 5 | 3 |
Answer after pop: top = 5, min = 3.
Example 2 — every step
Same ops, aux stack, getMin after every op.
| Op | st | mins | getMin 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.
2L * x - min) is the follow-up if they ban the aux structure.
- Aux stack that only pushes when
x < min, skipping equals — pop of a duplicate min is wrong. java.util.Stack(legacy). UseArrayDeque.- Calling getMin / pop on empty without a contract. Ask.
| Problem | Pattern | Links |
|---|---|---|
| Min stack | Pair or aux; one-stack encoding if they ban aux | Java in ch.05 · cheat card |
| Stack get middle | DLL or two stacks — not getMin | Java in ch.05 · cheat card |
| Basic calculator / OOD | Operand + 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.
| i | a[i] | Deque idx (vals) | Window ready? | ans |
|---|---|---|---|---|
| 0 | 1 | [0] (1) | no | — |
| 1 | 3 | poll last 0 (1 ≤ 3); [1] (3) | yes, max 3 | [3] |
| 2 | −1 | 1 > 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.
| i | a[i] | Drop front if idx ≤ i−3 | Drop back while val ≤ a[i] | Deque after offer | ans[i−2] |
|---|---|---|---|---|---|
| 0 | 1 | — | — | [0] (1) | — |
| 1 | 3 | — | poll 0 (1 ≤ 3) | [1] (3) | — |
| 2 | −1 | — | −1 < 3, keep | [1, 2] (3, −1) | 3 |
| 3 | −3 | 1 ≤ 0? no | −3 < −1, keep | [1, 2, 3] (3, −1, −3) | 3 |
| 4 | 5 | 1 ≤ 1 → pollFirst 1 | poll 3 (−3≤5), poll 2 (−1≤5) | [4] (5) | 5 |
Answer: [3, 3, 5].
- 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.
| Problem | Pattern | Links |
|---|---|---|
| Sliding window maximum | Decreasing deque of indices | Java 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.
| Op | map | Pointers (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 ⇄ T | 1 |
| 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.
| Op | H.next | chain | T.prev | n1.prev / next | n2.prev / next | n3.prev / next |
|---|---|---|---|---|---|---|
| start | T | H ⇄ T | H | — | — | — |
| put(1,1) | n1 | H ⇄ n1 ⇄ T | n1 | H / T | — | — |
| put(2,2) | n2 | H ⇄ n2 ⇄ n1 ⇄ T | n1 | n2 / T | H / n1 | — |
| get(1) | n1 | H ⇄ n1 ⇄ n2 ⇄ T | n2 | H / n2 | n1 / T | — |
| put(3,3) evict n2 | n3 | H ⇄ n3 ⇄ n1 ⇄ T | n1 | n3 / T | unlinked; map dropped 2 | H / 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.
- Forgetting to
map.removethe 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.
| Problem | Pattern | Links |
|---|---|---|
| LRU cache | HashMap + dummy-headed DLL; not Login Tracker | Java in ch.05 · cheat card · clue |
| LFU cache | Freq buckets + LRU inside the min-freq list | cheat 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.