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%
B9

Linked lists

No index math, no shifting cost — the tradeoff array questions can't make.

What you're giving up, and what you get for it

5 | ● 3 | ● 9 | ● null head →
Each node only knows its own value and where the next one lives — no shared block of memory.
OperationArrayLinked list
Access by indexO(1)O(n) — must walk from head
Insert/delete at the frontO(n) — shifts everythingO(1) — just repoint
Insert/delete at a known nodeO(n)O(1)
Search by valueO(n)O(n)
Memory layoutcontiguousscattered, +overhead per node for the pointer

Doubly linked lists — pay more memory, get backward traversal free

prev|5|next prev|3|next prev|9|next every node points both ways — walk forward OR backward, both O(1) per step
The classic tradeoff: extra pointer per node (more memory) buys O(1) removal of a known node without needing its predecessor tracked separately, and O(1) backward walks.
class DoublyListNode {
  constructor(val, prev = null, next = null) {
    this.val = val;
    this.prev = prev;
    this.next = next;
  }
}

// removing a known node is O(1) — no need to walk to find its predecessor
function removeNode(node) {
  if (node.prev) node.prev.next = node.next;
  if (node.next) node.next.prev = node.prev;
}

In a singly linked list, deleting a known node still requires its predecessor (to repoint .next) — and finding the predecessor means walking from the head, O(n). A doubly linked list already has .prev sitting right there, which is exactly why real-world LRU caches (advanced tier) are almost always built on one: O(1) move-to-front and O(1) eviction of a known node.

The node and the walk

class ListNode {
  constructor(val, next = null) {
    this.val = val;
    this.next = next;
  }
}

function traverse(head) {
  let node = head;
  while (node !== null) {
    console.log(node.val);
    node = node.next; // the ENTIRE reason lists are O(n) to access by index
  }
}

Reversal — the pattern that trips people up live

function reverseList(head) {
  let prev = null;
  let curr = head;
  while (curr !== null) {
    const next = curr.next;  // save before you overwrite it
    curr.next = prev;        // flip the pointer
    prev = curr;              // advance both
    curr = next;
  }
  return prev; // prev is the new head
}

Watch the pointers move, step by step

List: 1 → 2 → 3 → null

stepprevcurrnext (saved)after curr.next = prev
startnull1—1 → 2 → 3 → null (unchanged)
1null121 → null    (2 → 3 → null, separate)
— advance —12—prev=1, curr=2
21232 → 1 → null    (3 → null, separate)
— advance —23—prev=2, curr=3
323null3 → 2 → 1 → null
— advance —3null—loop ends, return prev = 3

The list is genuinely broken into two disconnected pieces mid-flip at every step — that's expected, not a bug. It only becomes one connected list again at the very end, once every node's .next has been repointed backward.

⚠ The #1 linked-list bug: losing the rest of the list If you write curr.next = prev before saving curr.next into a temporary variable, you've just overwritten your only pointer to the rest of the list — everything after curr is now unreachable. Save next first, every time, no exceptions.

Fast/slow pointers — find the middle in one pass

function findMiddle(head) {
  let slow = head, fast = head;
  while (fast !== null && fast.next !== null) {
    slow = slow.next;      // moves 1 step
    fast = fast.next.next; // moves 2 steps
  }
  return slow; // when fast hits the end, slow is at the middle
}

This is the same fast/slow idea from the two-pointers chapter, adapted to a structure with no indices — you can't do arr[Math.floor(arr.length/2)] here, so the two-speed walk is how you find the middle without a second pass to count length first.

Cycle detection — Floyd's algorithm

function hasCycle(head) {
  let slow = head, fast = head;
  while (fast !== null && fast.next !== null) {
    slow = slow.next;
    fast = fast.next.next;
    if (slow === fast) return true; // they lapped each other
  }
  return false; // fast hit null — no cycle
}

If there's a cycle, the fast pointer (moving 2x speed) is guaranteed to eventually land on the same node as the slow pointer — think of two runners on a circular track at different speeds, the faster one always laps the slower one. If there's no cycle, fast simply reaches null first.

Finding WHERE the cycle starts — Floyd's phase two

Detecting a cycle only answers yes/no. The harder follow-up — "return the node where the cycle begins" — has a genuinely elegant second phase: once slow and fast meet, reset one pointer to the head and advance both remaining pointers one step at a time. Where they meet again is the cycle's start.

function detectCycleStart(head) {
  let slow = head, fast = head;
  while (fast !== null && fast.next !== null) {
    slow = slow.next;
    fast = fast.next.next;
    if (slow === fast) {
      let ptr = head;
      while (ptr !== slow) { // phase two — same speed now
        ptr = ptr.next;
        slow = slow.next;
      }
      return ptr; // the cycle's entry node
    }
  }
  return null; // no cycle
}

Why this works comes down to the math: if the distance from head to the cycle's start is a, and slow/fast meet b steps into the cycle, it can be shown that a equals the remaining distance around the cycle back to the start from the meeting point — which is exactly why walking both pointers at equal speed from those two starting points lands them on the same node. You don't need to re-derive this live; knowing the two-phase shape and being able to state the result is enough.

Merging two sorted lists — the dummy node in action

function mergeTwoLists(l1, l2) {
  const dummy = new ListNode(0);
  let tail = dummy;

  while (l1 !== null && l2 !== null) {
    if (l1.val <= l2.val) {
      tail.next = l1;
      l1 = l1.next;
    } else {
      tail.next = l2;
      l2 = l2.next;
    }
    tail = tail.next;
  }
  tail.next = l1 !== null ? l1 : l2; // attach whatever's left
  return dummy.next;
}
stepcomparetail.next =merged so far
1l1=1, l2=211
2l1=3, l2=221 → 2
3l1=3, l2=431 → 2 → 3
4l1=null, l2=4attach remaining l21 → 2 → 3 → 4

This is the exact merge() step from merge sort (the sorting chapter), just applied to linked lists instead of arrays — and it's O(1) space here instead of O(n), because nodes are relinked in place rather than copied into a new array.

Nth from the end — the gap technique

Without knowing the length up front, you can't index from the end directly. The fix: advance one pointer n steps first to create a fixed gap, then move both pointers together — when the lead pointer hits the end, the trailing pointer is exactly n from the end.

function removeNthFromEnd(head, n) {
  const dummy = new ListNode(0, head);
  let fast = dummy, slow = dummy;

  for (let i = 0; i < n; i++) fast = fast.next; // open the gap

  while (fast.next !== null) { // walk both, gap stays fixed
    fast = fast.next;
    slow = slow.next;
  }

  slow.next = slow.next.next; // slow is right before the target
  return dummy.next;
}

The dummy node earns its keep again here — without it, removing the actual head (when n equals the list's length) would need its own special case.

The dummy-node trick — kills a whole class of edge-case bugs

// remove all nodes with a given value
function removeElements(head, val) {
  const dummy = new ListNode(0, head); // fake node before the real head
  let curr = dummy;
  while (curr.next !== null) {
    if (curr.next.val === val) curr.next = curr.next.next;
    else curr = curr.next;
  }
  return dummy.next; // the real (possibly new) head
}

Without the dummy node, deleting the actual head requires special-case code (there's no "previous" node to repoint). With a dummy node in front, the head is never special — it's just dummy.next like any other node's neighbor. Reach for this trick anytime the head itself might need to change.

Say it like this → "I'll use a dummy node before the head so removing or inserting at the front doesn't need special-case code — it's just another node's .next update, same as anywhere else in the list."

Recognizing it in an unseen problem

  • The prompt gives you a ListNode / "linked list" input directly
  • "Reverse," "merge two sorted lists," "detect a cycle," "find the middle," "nth from the end"
  • You need O(1) insert/delete and don't need random access by index
  • Fast/slow pointers solve it if the ask involves "middle," "cycle," or "nth from the end"
  • Need O(1) removal of an arbitrary known node, or backward traversal — reach for doubly linked
  • A dummy node removes a special case anytime the head itself might change
Practice this layer

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

Reverse Linked List5 tests · beginnerMerge Two Sorted Lists5 tests · beginnerLinked List Cycle5 tests · beginnerLinked List Cycle II5 tests · intermediateMiddle of the Linked List5 tests · beginnerRemove Nth Node From End5 tests · intermediatePalindrome Linked List5 tests · advancedIntersection of Two Linked Lists5 tests · intermediateAdd Two Numbers5 tests · intermediateMerge K Sorted Lists5 tests · advancedCopy List with Random Pointer5 tests · advancedReorder List5 tests · intermediateSwap Nodes in Pairs5 tests · intermediateReverse Nodes in k-Group5 tests · advancedRotate List5 tests · intermediatePartition List5 tests · intermediateRemove Duplicates from Sorted List5 tests · beginnerDelete Node in a Linked List5 tests · beginnerSort List5 tests · advancedFlatten a Multilevel Doubly Linked List5 tests · advanced
←previousStacks & queues↑ CovernextBasic recursion→