GUIDE · Lists
Stacks & Queues
LIFO (stack) for nesting / "most recent unresolved"; FIFO (queue) for order / BFS.
At a glance
- WHAT
- LIFO (stack) for nesting / "most recent unresolved"; FIFO (queue) for order / BFS.
- TELL
- "balanced / matching", "nested", "evaluate expression", "undo", "next greater / smaller", "span", "histogram", "first-in-first-out", "level by level".
- SAY
- stackqueuedequemonotonic stack (increasing / decreasing)amortized O(1)circular buffer / ring buffertwo-stack queue
- BIG-O
- O(1) push/pop; monotonic stack is O(n) total because each index is pushed/popped once.
- NOTE
- JS Array.shift() is O(n). For a real queue use a head index (section 12) or a linked list.
The idea#
Picture a stack of cafeteria trays next to a lunch line. You take the tray on top. That is last in, first out. In the lunch line, the person who arrived first gets served first. That is first in, first out.
A stack is the tool for nesting and for the most recent thing you have not finished. A queue is the tool for order and for walking level by level. Push and pop are O(1). A monotonic stack is O(n) for the whole array, because each index is pushed once and popped once. In JavaScript, Array.shift() is O(n), so a real queue advances a head index or uses a ring, and it does not pull from the front of a raw array.
The tray stack remembers openers, operators, and folder names until the matching closer shows up. If the top tray is the wrong shape, the nesting is broken. A min stack keeps a second stack of running minimums, so "what is the smallest so far" is a peek, still O(1).
A monotonic stack is fussier. It holds indices whose values stay in sorted order, decreasing if you want the next greater item. When a taller value arrives, every shorter index on top just found its answer, and you pop them. Leftovers have no next greater. Flip the comparison for next smaller. Walk from the right if you want the previous one.
A queue can be two stacks. Push into the inbox. Pop from the outbox. When the outbox is empty, pour the inbox over, which reverses it, so the oldest item lands on top. Each item moves at most twice, so a run of operations is amortized O(1) each.
Why it matters#
Reach for a stack when the problem says balanced, matching, nested, undo, or evaluate. Parentheses, file paths, and reverse Polish notation are the same move: push the thing you cannot finish yet, and finish it when its partner arrives. Reach for a queue when the problem says first in first out, or level by level. Breadth-first search is a queue. The thing you discover first is the thing you expand first, which is why an unweighted shortest path falls out of it.
The phrase "next greater," "span," "daily temperatures," or "largest rectangle" means a monotonic stack. A nested loop that looks backward from every index is O(n²). The stack throws that inner scan away, because each index joins and leaves once.
You already keep this kind of pile. The back button is a stack of pages. A printer line is a queue. The bug people ship is calling shift() inside a hot loop and calling it O(1). It walks the whole array every time. The sheet's plain queue and its ring buffer both keep the ends O(1).
Two stacks make a queue in amortized O(1). One queue makes a stack if each push rotates the older items to the back, and that push is O(n).
A small example#
function nextGreater(nums) { const answer = new Array(nums.length).fill(-1); const stack = []; for (let i = 0; i < nums.length; i++) { while (stack.length > 0 && nums[stack[stack.length - 1]] < nums[i]) { answer[stack.pop()] = nums[i]; } stack.push(i); } return answer;} const greater = nextGreater([2, 1, 2, 4, 3]);greater is [4, 2, 4, -1, -1]. The stack held indices, not copies of the values. Each index was pushed once.
What the code below is doing#
Section 1 checks parentheses. Push openers. On a closer, the top must be the match. Empty at the end means balanced. O(n) time and space. Section 2 is the min stack: a parallel stack of running minimums, so push, pop, top, and getMin are all O(1). Section 3 evaluates reverse Polish notation. Numbers get pushed. An operator pops b, then a, and pushes a operated with b.
Section 4 decodes strings like nested repeats. One stack holds counts, one holds the string so far. On ] you pop and repeat. Section 5 simplifies a Unix path. Split on /. .. pops. . and empty pieces are skipped. Section 6 builds a queue from two stacks, inbox and outbox, amortized O(1). Section 7 builds a stack from one queue: after push, rotate size - 1 times so the new item sits at the front. Push is O(n), pop is O(1).
Then the monotonic template: stack of indices, values decreasing, pop while the new value is greater, record the answer as a value or as a distance, push the index. Section 8 writes the next greater value, and a second lap over the same array is how you handle a circular list. Section 9 writes the distance until a warmer day. Section 10 is the largest rectangle in a histogram. The stack stays increasing. On a pop, the width is i - stackTop - 1, and a sentinel 0 at the end flushes the stack. Section 11 removes k digits to leave the smallest number: pop while the top is bigger than the new digit and you still have removals left, then strip leading zeros.
Section 12 is a circular queue, a fixed array with head and tail moving mod the capacity. Every operation is O(1), and the space is the capacity. Section 13 is the everyday JavaScript queue: an array plus a head index. Dequeue advances the head. Compact occasionally if the dead prefix gets long. Amortized O(1), and shift stays out of it.
When you talk about it, say this: a stack is the most recent unfinished thing, a queue is first in first out, both ends are O(1), and a monotonic stack is O(n) because each index is pushed and popped once.
Everything below is the demonstration. Same idea, written as code you can run.
'use strict';const assert = require('node:assert/strict');Valid parentheses#
push openers; on closer, top must match
function isValid(s) { const pair = { ')': '(', ']': '[', '}': '{' }, st = []; for (const c of s) { if (c in pair) { if (st.pop() !== pair[c]) return false; } else st.push(c); } return st.length === 0;}Verified by 4 asserts
assert.equal(isValid('()[]{}'), true);assert.equal(isValid('([)]'), false);assert.equal(isValid(''), true);assert.equal(isValid('('), false);Min stack#
parallel stack of running minimums
class MinStack { #s = []; #min = []; push(x) { this.#s.push(x); this.#min.push(this.#min.length ? Math.min(x, this.#min[this.#min.length - 1]) : x); } pop() { this.#min.pop(); return this.#s.pop(); } top() { return this.#s[this.#s.length - 1]; } getMin() { return this.#min[this.#min.length - 1]; }}const ms = new MinStack();ms.push(-2); ms.push(0); ms.push(-3);ms.pop();Verified by 3 asserts
assert.equal(ms.getMin(), -3);assert.equal(ms.top(), 0);assert.equal(ms.getMin(), -2);Evaluate reverse polish notation#
push numbers; operator pops two (b then a)
function evalRPN(tokens) { const st = []; const ops = { '+': (a, b) => a + b, '-': (a, b) => a - b, '*': (a, b) => a * b, '/': (a, b) => Math.trunc(a / b) }; for (const t of tokens) { if (t in ops) { const b = st.pop(), a = st.pop(); st.push(ops[t](a, b)); } else st.push(Number(t)); } return st[0];}Verified by 3 asserts
assert.equal(evalRPN(['2', '1', '+', '3', '*']), 9);assert.equal(evalRPN(['4', '13', '5', '/', '+']), 6);assert.equal(evalRPN(['-7']), -7);Decode string#
stacks for count and partial string; on ']' pop and repeat
function decodeString(s) { const counts = [], strs = []; let cur = '', k = 0; for (const c of s) { if (c >= '0' && c <= '9') k = k * 10 + Number(c); else if (c === '[') { counts.push(k); strs.push(cur); cur = ''; k = 0; } else if (c === ']') cur = strs.pop() + cur.repeat(counts.pop()); else cur += c; } return cur;}Verified by 3 asserts
assert.equal(decodeString('3[a]2[bc]'), 'aaabcbc');assert.equal(decodeString('3[a2[c]]'), 'accaccacc');assert.equal(decodeString('abc'), 'abc');Simplify path (Unix)#
split on '/', '..' pops, '.'/'' skipped
function simplifyPath(path) { const st = []; for (const part of path.split('/')) { if (part === '..') st.pop(); else if (part && part !== '.') st.push(part); } return '/' + st.join('/');}Verified by 3 asserts
assert.equal(simplifyPath('/a/./b/../../c/'), '/c');assert.equal(simplifyPath('/../'), '/');assert.equal(simplifyPath('/home//foo/'), '/home/foo');Queue via two stacks#
inbox push; outbox pop; refill outbox by reversing inbox when empty
class QueueViaStacks { #in = []; #out = []; push(x) { this.#in.push(x); } #shift() { if (this.#out.length === 0) while (this.#in.length) this.#out.push(this.#in.pop()); } pop() { this.#shift(); return this.#out.pop(); } peek() { this.#shift(); return this.#out[this.#out.length - 1]; } get empty() { return this.#in.length + this.#out.length === 0; }}const q2 = new QueueViaStacks();q2.push(1); q2.push(2);q2.push(3);Verified by 3 asserts
assert.equal(q2.peek(), 1);assert.equal(q2.pop(), 1);assert.deepEqual([q2.pop(), q2.pop(), q2.empty], [2, 3, true]);Stack via one queue#
after push, rotate the queue size-1 times so the new item is at the front
class StackViaQueue { #q = []; push(x) { this.#q.push(x); for (let i = 1; i < this.#q.length; i++) this.#q.push(this.#q.shift()); } pop() { return this.#q.shift(); } top() { return this.#q[0]; }}const sq = new StackViaQueue();sq.push(1); sq.push(2); sq.push(3);Verified by 2 asserts
assert.equal(sq.top(), 3);assert.deepEqual([sq.pop(), sq.pop(), sq.pop()], [3, 2, 1]);MONOTONIC STACK TEMPLATE (next greater to the right; stack holds INDICES with decreasing values)#
// for i in 0..n-1:// while stack not empty and a[stack.top] < a[i]: // a[i] is the "next greater" for stack.top// j = stack.pop(); answer[j] = i (or a[i], or i - j)// stack.push(i)// leftover indices have no next greater.// Flip '<' to '>' for next smaller; iterate from the right for "previous" instead of "next".Next greater element (circular optional)#
template, output values
function nextGreater(a) { const out = new Array(a.length).fill(-1), st = []; for (let i = 0; i < a.length; i++) { while (st.length && a[st[st.length - 1]] < a[i]) out[st.pop()] = a[i]; st.push(i); } return out;}Verified by 3 asserts
assert.deepEqual(nextGreater([2, 1, 2, 4, 3]), [4, 2, 4, -1, -1]);assert.deepEqual(nextGreater([3, 2, 1]), [-1, -1, -1]);assert.deepEqual(nextGreater([]), []);Daily temperatures#
template, output distance i - j
function dailyTemperatures(t) { const out = new Array(t.length).fill(0), st = []; for (let i = 0; i < t.length; i++) { while (st.length && t[st[st.length - 1]] < t[i]) { const j = st.pop(); out[j] = i - j; } st.push(i); } return out;}Verified by 2 asserts
assert.deepEqual(dailyTemperatures([73, 74, 75, 71, 69, 72, 76, 73]), [1, 1, 4, 2, 1, 1, 0, 0]);assert.deepEqual(dailyTemperatures([30, 30]), [0, 0]);Largest rectangle in histogram#
increasing stack; on pop, width = i - stack.top - 1; sentinel 0 at end
function largestRectangleArea(h) { const st = []; let best = 0; for (let i = 0; i <= h.length; i++) { const cur = i === h.length ? 0 : h[i]; // sentinel flushes the stack while (st.length && h[st[st.length - 1]] >= cur) { const height = h[st.pop()]; const width = st.length ? i - st[st.length - 1] - 1 : i; best = Math.max(best, height * width); } st.push(i); } return best;}Verified by 3 asserts
assert.equal(largestRectangleArea([2, 1, 5, 6, 2, 3]), 10);assert.equal(largestRectangleArea([2, 4]), 4);assert.equal(largestRectangleArea([]), 0);Remove k digits (smallest result)#
increasing stack; pop while top > digit and k > 0; trim; strip leading zeros
function removeKdigits(num, k) { const st = []; for (const d of num) { while (k > 0 && st.length && st[st.length - 1] > d) { st.pop(); k--; } st.push(d); } st.length -= k; // still have removals left: drop from the end return st.join('').replace(/^0+/, '') || '0';}Verified by 3 asserts
assert.equal(removeKdigits('1432219', 3), '1219');assert.equal(removeKdigits('10200', 1), '200');assert.equal(removeKdigits('10', 2), '0');Circular queue (ring buffer)#
fixed array; head/tail indices mod capacity
class CircularQueue { #a; #head = 0; #size = 0; constructor(k) { this.#a = new Array(k); } enqueue(x) { if (this.isFull()) return false; this.#a[(this.#head + this.#size) % this.#a.length] = x; this.#size++; return true; } dequeue() { if (this.isEmpty()) return false; this.#head = (this.#head + 1) % this.#a.length; this.#size--; return true; } front() { return this.isEmpty() ? -1 : this.#a[this.#head]; } rear() { return this.isEmpty() ? -1 : this.#a[(this.#head + this.#size - 1) % this.#a.length]; } isEmpty() { return this.#size === 0; } isFull() { return this.#size === this.#a.length; }}const cq = new CircularQueue(3);Verified by 4 asserts
assert.deepEqual([cq.enqueue(1), cq.enqueue(2), cq.enqueue(3), cq.enqueue(4)], [true, true, true, false]);assert.equal(cq.rear(), 3);assert.equal(cq.dequeue(), true);assert.deepEqual([cq.enqueue(4), cq.front(), cq.rear()], [true, 2, 4]); // wrapped aroundO(1) queue in JS (head-index trick)#
never shift(); advance head; optionally compact
// SAY: "Array.shift is O(n) because V8 re-indexes; I keep a head pointer instead."class Queue { #a = []; #h = 0; push(x) { this.#a.push(x); } shift() { const x = this.#a[this.#h++]; if (this.#h > 1024 && this.#h * 2 > this.#a.length) { this.#a = this.#a.slice(this.#h); this.#h = 0; } // compact return x; } get length() { return this.#a.length - this.#h; }}const q = new Queue();for (let i = 0; i < 5; i++) q.push(i);// Deque with array: push/pop O(1) at tail; unshift/shift O(n) at head. For a true O(1) deque use a doubly linked list// or two arrays (front stack + back stack). For "sliding window max" a plain array + head index is enough.Verified by 1 assert
assert.deepEqual([q.shift(), q.shift(), q.length], [0, 1, 3]);See also