Skip to the notes
JSGroundwork
JSGroundwork handwritten · web dev
✎Playground→⌘Problems↻Review🔥Progress

Chapters

34 chapters
⌕
Beginner10›
B1Complexity analysisB2Arrays & stringsB3HashingB4Two pointersB5Sliding windowB6Binary searchB7Sorting algorithmsB8Stacks & queuesB9Linked listsB10Basic recursion
Intermediate12›
I1TreesI2Tree problems in depthI3Heaps & priority queuesI4Graphs: representationI5Graph problemsI6BacktrackingI7DP: 1DI8DP: 2DI9Greedy algorithmsI10IntervalsI11Bit manipulationI12Matrix problems
Advanced12›
A1Advanced DPA2Union-FindA3Advanced graph algorithmsA4Minimum Spanning TreeA5TriesA6Segment & Fenwick treesA7String algorithmsA8Monotonic stack & queueA9Design problemsA10Advanced backtrackingA11Topological patternsA12Interview strategy
/ search[ ] chaptert top

DSA in JS levels

1Beginner2Intermediate3Advanced

Ready to read

JSJavaScript⑂Git◎Interview prepΣDSA in JSSDSystem Design
More topics15›
</>HTML{ }CSS⚛ReactNNext.jsNeNest.jsTSTypeScriptNoNode.js🐳DockerDBSQL & Databases✓Testing🔒Web Security☁Cloud & DevOps◈GraphQL◆Redis☸Kubernetes
100%
B8

Stacks & queues

Two rules for order, and half of interview problems secretly need one of them.

The only thing that actually differs between them

3 (top) 2 1 Stack — LIFO: push/pop the top 1 → out 1 2 3 ← 4 in Queue — FIFO: enqueue at back, dequeue from front
Same idea (add/remove one at a time) — opposite rule for which end you remove from.
Stack (LIFO)Queue (FIFO)
Addpush() — O(1)enqueue at the back — O(1)
Removepop() — O(1), removes most recentdequeue from front — O(1), removes oldest
Real-world modela stack of platesa checkout line
Classic useundo, call stack, backtracking, matching pairsBFS, task scheduling, rate limiting

Stacks in JS — just use an array

const stack = [];
stack.push(1);
stack.push(2);
stack.pop();      // 2 — removes from the END, O(1)
stack[stack.length - 1]; // peek without removing
⚠ Don't build a queue out of array.shift() shift() removes from the front, which means every remaining element shifts down — O(n) per dequeue, so an n-step queue simulation silently becomes O(n²). For a real queue, either push/pop from the array's end and treat index 0 as "front" with a separate pointer, or use two stacks (below).
// an O(1)-amortized queue using two stacks
class Queue {
  #inStack = [];
  #outStack = [];

  enqueue(x) { this.#inStack.push(x); }

  dequeue() {
    if (this.#outStack.length === 0) {
      while (this.#inStack.length) {
        this.#outStack.push(this.#inStack.pop());
      }
    }
    return this.#outStack.pop();
  }
}

Each element gets moved from inStack to outStack at most once ever — so across n operations the total work is O(n), even though a single dequeue can occasionally cost O(n). That's the "amortized O(1)" argument, same shape as array.push()'s resizing.

The pattern stacks solve: matching and undoing

// valid parentheses — the canonical stack interview question
function isValid(s) {
  const pairs = { ")": "(", "]": "[", "}": "{" };
  const stack = [];
  for (const c of s) {
    if (c === "(" || c === "[" || c === "{") {
      stack.push(c);
    } else {
      if (stack.pop() !== pairs[c]) return false;
    }
  }
  return stack.length === 0;
}

Watch the stack fill and drain, step by step

s = "{[()]}":

chartypeactionstack after
{openpush[ { ]
[openpush[ {, [ ]
(openpush[ {, [, ( ]
)closepop, expect "(" — got "(" ✓[ {, [ ]
]closepop, expect "[" — got "[" ✓[ { ]
}closepop, expect "{" — got "{" ✓[ ] — empty

Stack empty at the end → valid. If any close bracket ever popped the wrong open bracket, or the stack ran out of elements to pop, or the stack still had leftover opens at the end — any of those means invalid. All three failure modes are just as common in interview test cases as the happy path, so trace through them mentally too.

The tell: whenever a problem needs "the most recent unmatched thing" — an open bracket, an undo history, the calling function to return to — a stack is the structure that naturally tracks it, because LIFO is "most recent first."

Min-stack — the classic "track extra state per level" question

A regular stack can't answer "what's the minimum value currently in me" in better than O(n) — you'd have to scan everything. The fix: keep a second stack that tracks the running minimum at each level, so it shrinks in lockstep with the main stack.

class MinStack {
  #stack = [];
  #minStack = []; // minStack[i] = the min among stack[0..i]

  push(x) {
    this.#stack.push(x);
    const currentMin = this.#minStack.length
      ? Math.min(x, this.#minStack[this.#minStack.length - 1])
      : x;
    this.#minStack.push(currentMin);
  }

  pop() {
    this.#minStack.pop();
    return this.#stack.pop();
  }

  getMin() {
    return this.#minStack[this.#minStack.length - 1]; // O(1)
  }
}
operationstackminStackgetMin()
push(5)[5][5]5
push(2)[5, 2][5, 2]2
push(7)[5, 2, 7][5, 2, 2]2
pop()[5, 2][5, 2]2
pop()[5][5]5

minStack pops in lockstep with stack, so it never has stale data — the minimum "at this depth" is always exactly minStack's top. This "shadow stack that mirrors the main one, tracking one extra fact" idea generalizes to max, running sum, and similar per-level queries.

Evaluating expressions — the other classic stack application

Postfix (Reverse Polish) notation — "3 4 +" instead of "3 + 4" — needs no parentheses and no operator precedence rules, because a stack evaluates it directly: push numbers, and when you hit an operator, pop two operands, apply it, push the result back.

function evalRPN(tokens) {
  const stack = [];
  const ops = {
    "+": (a, b) => a + b,
    "-": (a, b) => a - b,
    "*": (a, b) => a * b,
    "/": (a, b) => Math.trunc(a / b),
  };
  for (const token of tokens) {
    if (token in ops) {
      const b = stack.pop();
      const a = stack.pop();
      stack.push(ops[token](a, b)); // order matters for - and /
    } else {
      stack.push(Number(token));
    }
  }
  return stack.pop();
}
// evalRPN(["3","4","+","2","*"]) → (3+4)*2 → 14

Deques — a queue that can push/pop from both ends

A deque (double-ended queue) supports O(1) add/remove at both the front and back. It's the structure behind the advanced "sliding window maximum" pattern (kept as a monotonic deque of candidate maximums) and behind efficient BFS variants that need to push to the front sometimes (0-1 BFS). In JS there's no built-in deque — people either accept an array's O(n) front operations at small scale, or reach for a small class backed by two stacks (same two-stack trick as the queue above, extended to push at both ends) or a circular buffer.

Circular queue — a fixed-size ring buffer

0 1 2 3 4 5 front rear rear+1 wraps back to slot 0 — no shifting, ever
Fixed-size array + two pointers that wrap with modulo — O(1) enqueue/dequeue with zero shifting and zero resizing.
class CircularQueue {
  #data; #front = 0; #size = 0;
  constructor(capacity) { this.#data = new Array(capacity); }

  enqueue(x) {
    if (this.#size === this.#data.length) throw new Error("full");
    const rear = (this.#front + this.#size) % this.#data.length;
    this.#data[rear] = x;
    this.#size++;
  }

  dequeue() {
    if (this.#size === 0) throw new Error("empty");
    const x = this.#data[this.#front];
    this.#front = (this.#front + 1) % this.#data.length;
    this.#size--;
    return x;
  }
}

This is what a production task queue or a ring buffer for streaming data actually looks like — fixed memory, no allocation churn, and the modulo (%) is what makes "wrap back to the start" free.

The pattern queues solve: process in the order things arrived

Queues are the backbone of breadth-first search (its own chapter later): visit the closest things first, which requires processing in the exact order they were discovered — FIFO, not LIFO.

// level-order traversal shape — the queue IS the "current frontier"
function bfsShape(start, getNeighbors) {
  const queue = [start];
  const visited = new Set([start]);
  while (queue.length) {
    const node = queue.shift(); // front — fine at small scale; use a real
                                    queue/deque for large inputs, per the warning above
    for (const next of getNeighbors(node)) {
      if (!visited.has(next)) {
        visited.add(next);
        queue.push(next);
      }
    }
  }
}
Say it like this → "I need to always process the most recently opened thing first, so a stack's LIFO order matches the problem directly — I don't need to search for it, the last element pushed is always the right one to check."

Recognizing which one you need

  • Stack: matching pairs, undo/redo, "closest unmatched," depth-first exploration, evaluating expressions
  • Queue: breadth-first exploration, "process in arrival order," task scheduling
  • Deque: need to add/remove at both ends — sliding-window maximum, 0-1 BFS
  • Circular queue: fixed-capacity buffering — rate limiters, streaming windows, producer/consumer queues
  • Min-stack (or max-stack): "track the running min/max as things get pushed/popped"
  • If a problem says "next greater/smaller element," that's usually a monotonic stack — covered in the advanced tier
Practice this layer

Opens in the editor — write it, run it, and check it against real tests.

Min Stack4 tests · beginnerImplement Queue using Stacks4 tests · beginnerImplement Stack using Queues4 tests · beginnerAsteroid Collision5 tests · intermediateEvaluate Reverse Polish Notation5 tests · beginnerBasic Calculator5 tests · advancedBasic Calculator II5 tests · intermediateValid Parentheses5 tests · beginnerDecode String5 tests · intermediate
←previousSorting algorithms↑ CovernextLinked lists→