Skip to the chapter
←DSAGGroundworkΣDSA
DSA/Beginner/B12
12 / 48 written chapters
/

Levels and plan
  1. B1Complexity analysis
  2. B2JavaScript toolkit
  3. B3Arrays & strings
  4. B4Prefix sums
  5. B5Hashing
  6. B6Two pointers
  7. B7Sliding window
  8. B8Binary search
  9. B9Basic recursion
  10. B10Sorting algorithms
  11. B11Stacks & queues
  12. B12Linked lists
  1. I1Trees
  2. I2BST operations
  3. I3Tree problems in depth
  4. I4Heaps & priority queues
  5. I5Graphs: representation
  6. I6BFS on grids
  7. I7Graph problems
  8. I8Topological patterns
  9. I9Backtracking
  10. I10DP: 1D
  11. I11DP: 2D
  12. I12DP state machines
  13. I13Greedy algorithms
  14. I14Intervals
  15. I15Bit manipulation
  16. I16Matrix problems
  17. I17Tries
  18. I18Monotonic stack & queue
  19. I19Math for interviews
  20. I20Design & simulation
  21. I21Divide and conquer
  22. I22Randomization
  1. A1Advanced DP
  2. A2Union-Find
  3. A3Advanced graph algorithms
  4. A4Minimum Spanning Tree
  5. A5Advanced graphs
  6. A6Segment & Fenwick trees
  7. A7Sparse tables
  8. A8String algorithms
  9. A9Design problems
  10. A10Advanced backtracking
  11. A11Games & probability DP
  12. A12Ordered structures
  13. A13Sweep line & geometry
  14. A14Interview strategy

ΣB12 · Part 1 · Beginner

Linked lists

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

41 min read2 diagrams22 sections29 exercises
12 of 48

In this chapter

  • What you're giving up, and what you get for it
  • Doubly linked lists — pay more memory, get backward traversal free
  • The node and the walk
  • Reversal — the pattern that trips people up live
  • Fast/slow pointers — find the middle in one pass

and 17 more

about 41 min
22On this page
  1. What you're giving up, and what you get for it
  2. Doubly linked lists — pay more memory, get backward traversal free
  3. The node and the walk
  4. Reversal — the pattern that trips people up live
  5. Fast/slow pointers — find the middle in one pass
  6. Cycle detection — Floyd's algorithm
  7. Finding WHERE the cycle starts — Floyd's phase two
  8. Floyd on an array — finding the duplicate number
  9. Merging two sorted lists — the dummy node in action
  10. Merge sort on a list, and merging K sorted lists
  11. Nth from the end — the gap technique
  12. Two lists that meet — intersection, and adding two numbers
  13. The dummy-node trick — kills a whole class of edge-case bugs
  14. Reversing part of a list — a sublist, pairs and groups of k
  15. Rearranging without allocating — odd-even, partition and rotate
  16. Composing the primitives — palindrome and reorder list
  17. Copying a list that has random pointers
  18. LRU cache — a hash map plus a doubly linked list with sentinels
  19. Checking pointer code against a simple reference
  20. The pointer-surgery checklist
  21. Recognizing it in an unseen problem
  22. Recognising the harder list problems

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
}

Dry run: reverseList(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.

Floyd on an array — finding the duplicate number

Here is the problem. An array nums holds n + 1 integers, every one of them between 1 and n. By the pigeonhole principle at least one value repeats. Return a repeated value, without modifying the array and with O(1) extra space. Sorting would modify the array, a Set would cost O(n) space, and counting values into a second array costs O(n) as well. The constraint that rules all three out is the hint that something cleverer is going on.

The trick is to see a linked list that was never drawn. Treat index i as a node and nums[i] as its next pointer. Every value is between 1 and n, so every pointer lands on a real index, and no pointer ever leads back to index 0 (zero is never a value). Every node has exactly one outgoing pointer and none of them is null, so a walk that starts at index 0 can never end. With only n + 1 nodes it must eventually revisit one, which means it enters a cycle.

Now ask who points at the node where the cycle begins. One pointer comes from inside the cycle, closing the loop. Another comes from the tail, the path that led in from index 0 — and that tail is never empty, because index 0 itself is not on the cycle (nothing points to it). Two different indices holding the same value is exactly a duplicate. So the entry node of the cycle is a repeated value, and finding it is the two-phase Floyd algorithm you already wrote, with nums[i] standing in for node.next.

function findDuplicate(nums) {
  let slow = 0;
  let fast = 0;
  do {
    slow = nums[slow];
    fast = nums[nums[fast]];
  } while (slow !== fast); // meet somewhere inside the cycle

  slow = 0; // phase two: same speed, one from the start, one from the meeting point
  while (slow !== fast) {
    slow = nums[slow];
    fast = nums[fast];
  }
  return slow; // the cycle's entry is the value that two indices point at
}

Dry run: findDuplicate([1, 3, 4, 2, 2])

The pointers are 0 → 1, 1 → 3, 2 → 4, 3 → 2 and 4 → 2. Walking from index 0 gives 0, 1, 3, 2, 4, 2, 4, …: a tail of 0, 1, 3 leading into the cycle 2 ⇄ 4.

phasestepslowfastwhat happened
1113slow moves once, fast twice
1234fast is already inside the cycle
1324slow reaches the cycle's entry
1444they meet; slow !== fast is now false
2reset04slow goes back to index 0, fast stays at the meeting point
2112both move one step
2234both move one step
2322they meet at the cycle's entry — return 2

The answer 2 appears at indices 3 and 4, the two pointers that arrive at node 2. Each phase takes at most about as many steps as there are nodes, and the tail plus the cycle never exceed n + 1 nodes, so the time is O(n) and the space is O(1).

⚠ This only works under the problem's exact constraints The argument needs every value to be in 1..n. If zero were allowed, index 0 could sit on the cycle and the entry node would no longer be a duplicate; a value larger than n would read outside the array. Also note what you are promised: a repeated value, not every repeated value. With several repeated values it returns whichever one is the cycle's entry. The loop must be a do … while: both pointers start on the same index, so a plain while (slow !== fast) would stop before moving at all.

There is a different route with the same space cost: binary search on the value, counting how many numbers are at most mid. It runs in O(n log n) time, so the Floyd version is the faster of the two. The practice list includes ex-find-duplicate-number for this one.

Say it like this → "I'll treat index i as a node whose next pointer is nums[i]. Because every value is in range, the walk from index 0 has to enter a cycle, and the cycle's entry is a node with two incoming pointers — a repeated value — so I can find it with Floyd's two phases in O(n) time and O(1) space."

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.

Merge sort on a list, and merging K sorted lists

Merge sort is the sorting algorithm that fits a linked list, and it is easy to see why. Quicksort and heapsort jump around by index, which a list cannot do. Merge sort only needs to split a sequence and to merge two sorted ones, and both are sequential walks. The merge you already wrote relinks nodes in place, so there is no auxiliary array at all: the only memory the sort needs is its recursion stack.

function sortList(head) {
  if (head === null || head.next === null) return head;

  let slow = head;
  let fast = head.next; // starting fast one step ahead makes slow stop at the END of the first half
  while (fast !== null && fast.next !== null) {
    slow = slow.next;
    fast = fast.next.next;
  }
  const second = slow.next;
  slow.next = null; // cut the list in two

  return mergeTwoLists(sortList(head), sortList(second));
}

The one decision that decides whether this works is where slow stops. The findMiddle function from earlier starts both pointers at head and returns the second middle of an even-length list. That is the wrong node here, because we need the last node of the first half so that slow.next = null cuts the list cleanly in two. Starting fast one step ahead, at head.next, shifts the stopping point one node earlier.

⚠ The wrong middle recurses forever on two nodes With the findMiddle version, a two-node list stops slow on the second node. Cutting after it leaves a first half of both nodes and a second half of none, so sortList(head) is called again on exactly the same two nodes and the recursion never gets smaller. Every list that reaches length two hits this. Always test the 2-node case first.

Dry run: sortList(4 → 2 → 1 → 3)

calllistslow stops atsplitreturns
14 → 2 → 1 → 324 → 2 | 1 → 3merge of the two sorted halves
24 → 244 | 22 → 4
31 → 311 | 31 → 3
—merge(2 → 4, 1 → 3)——1 → 2 → 3 → 4

Complexity. Every level of the recursion walks and merges each node a constant number of times, and halving the list gives about log₂ n levels, so the time is O(n log n). The extra space is the recursion stack, O(log n), because the merges relink existing nodes and allocate none. A bottom-up version (merge runs of 1, then 2, then 4 …) reaches O(1) extra space, but the recursive one is what to write first.

Merging K sorted lists

Given k sorted lists with N nodes in total, the obvious plan is to merge them into one result, one list at a time. That is quietly expensive: the accumulated result gets longer with every merge and is walked again each time. With k equal lists, merge number i handles about i lists' worth of nodes, so the total is roughly N · (k + 1) / 2, which is O(N·k).

The fix is to merge in pairs, in rounds: lists 0 and 1, lists 2 and 3, and so on, which halves the number of lists while doubling their length. After log₂ k rounds one list is left, and every round handles each node at most once, so the total is O(N log k).

function mergeKLists(lists) {
  if (lists.length === 0) return null;
  let round = lists;
  while (round.length > 1) {
    const next = [];
    for (let i = 0; i < round.length; i += 2) {
      next.push(i + 1 < round.length ? mergeTwoLists(round[i], round[i + 1]) : round[i]);
    }
    round = next; // half as many lists, each twice as long
  }
  return round[0];
}

Counting how many nodes the merge calls handle, for lists of 100 nodes each:

k (lists)N (nodes)one at a timein pairs
44001,000800
88003,6002,400
161,60013,6006,400
323,20052,80016,000

The pairwise column is exactly N · log₂ k (3,200 × 5 = 16,000), and the one-at-a-time column grows with k itself. A min-heap that always holds the current front node of each list gets the same O(N log k) and is the usual alternative; it is written out in the heaps chapter. Empty entries in lists need no special code, because mergeTwoLists already handles a null input, and an empty lists array returns null from the first line.

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.

Two lists that meet — intersection, and adding two numbers

Intersection of two lists

Two singly linked lists may merge: from some node onward they are the very same nodes, not copies, and the tail is shared. Return that first shared node, or null if there is none. The nested-loop answer checks every pair and costs O(m·n). Putting one list's nodes into a Set and scanning the other costs O(m) space. The constant-space answer needs one idea: make both pointers walk the same total distance.

Say list A has x nodes of its own followed by c shared ones, and list B has y of its own followed by the same c. Walk a pointer down A and, when it falls off the end, send it to the head of B. Walk another down B and then onto A. The first pointer covers x + c nodes, then y of B's own, and arrives at the shared part after the same number of steps as the second pointer, which covers y + c and then x. The unequal prefixes have been swapped, so the difference in length cancels out and both pointers land on the first shared node together.

function getIntersectionNode(headA, headB) {
  let a = headA;
  let b = headB;
  while (a !== b) {
    a = a === null ? headB : a.next;
    b = b === null ? headA : b.next;
  }
  return a; // the shared node, or null when the lists never meet
}

Dry run: A = 4 → 1 → 8 → 4 → 5 and B = 5 → 6 → 1 → 8 → 4 → 5

The lists share their last three nodes, 8 → 4 → 5. Call A's own nodes a1, a2 (values 4, 1), B's own nodes b1, b2, b3 (values 5, 6, 1), and the shared ones s1, s2, s3.

stepa is onb is on
0a1b1
1a2b2
2s1b3
3s2s1
4s3s2
5nulls3
6b1 (switched)null
7b2a1 (switched)
8b3a2
9s1s1

Look at step 8: b3 and a2 both hold the value 1, but they are different nodes, so the pointers keep walking. That is why the comparison is a !== b on the nodes and never on .val. If the lists share nothing, both pointers fall off the end of their second list on the same step, a === b === null, and the loop exits returning null without a special case. Each pointer walks at most m + n + 1 steps, so the time is O(m + n) and the space is O(1). Both lists must be free of cycles, or the loop never ends.

Adding two numbers stored as lists

Each list holds one number, one digit per node, with the ones digit first: 2 → 4 → 3 is 342. That order is a gift, because the head is exactly the digit that column addition starts with. Walk both lists together, add the two digits and the carry, write sum % 10 into a new node and carry Math.floor(sum / 10) to the next column. A dummy node and a tail pointer build the result front to back.

function addTwoNumbers(l1, l2) {
  const dummy = new ListNode(0);
  let tail = dummy;
  let carry = 0;
  while (l1 !== null || l2 !== null || carry !== 0) {
    const sum = (l1 !== null ? l1.val : 0) + (l2 !== null ? l2.val : 0) + carry;
    tail.next = new ListNode(sum % 10);
    carry = Math.floor(sum / 10);
    tail = tail.next;
    l1 = l1 !== null ? l1.next : null;
    l2 = l2 !== null ? l2.next : null;
  }
  return dummy.next;
}
columnl1 digitl2 digitcarry insumnode writtencarry out
1250770
24601001
3341880

342 + 465 gives 7 → 0 → 8, which is 807. The loop condition has three parts for a reason: the lists have different lengths, and a final carry still needs its own node. Adding 9 → 9 and 1 gives 0 → 0 → 1, the 1 coming from the carry alone. The time is O(max(m, n)), and the only memory beyond the answer itself is a few variables.

⚠ Do not turn the lists into numbers It is tempting to read both lists into integers, add, and rebuild a list. But the lists may be hundreds of digits long, and a JavaScript number is only exact up to Number.MAX_SAFE_INTEGER, which is 9007199254740991 — sixteen digits. Digit-by-digit addition has no such limit. If the digits come most significant first instead, reverse both lists (or push the digits on two stacks), add as above, and reverse the answer.

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."

Reversing part of a list — a sublist, pairs and groups of k

reverseList reverses everything. Two common variants reverse only part of a list, and both are built from the same pointer dance: reverse positions left to right, and reverse every consecutive group of k nodes. Both need a dummy node, because the first node of the list can be the one that moves.

A sublist, by head insertion

One way is to cut the segment out, call reverseList on it and sew the three pieces back together, which means juggling four pointers. A cleaner way is to reverse in place by head insertion. Let before be the node just ahead of position left, and start the node at position left. Repeatedly take the node right after start and move it to the front of the segment, right after before. Each move grows the reversed prefix by one node, and start slowly drifts to the back without ever being touched. After right − left moves the segment is reversed, and start.next already points at the first node after the segment, so nothing needs reconnecting.

function reverseBetween(head, left, right) {
  const dummy = new ListNode(0, head);
  let before = dummy;
  for (let i = 1; i < left; i++) before = before.next; // node just before position left

  const start = before.next; // first node of the segment; it ends up as the segment's tail
  for (let i = 0; i < right - left; i++) {
    const moving = start.next; // the node to pull to the front of the segment
    start.next = moving.next; // unhook it
    moving.next = before.next; // hang it in front of the current segment head
    before.next = moving; // and make it the new segment head
  }
  return dummy.next;
}

Dry run: reverseBetween(1 → 2 → 3 → 4 → 5, left = 2, right = 4)

movebeforestartmovinglist after the move
setup12—1 → 2 → 3 → 4 → 5
11231 → 3 → 2 → 4 → 5
21241 → 4 → 3 → 2 → 5
⚠ The three assignments are not interchangeable moving.next = before.next must read before.next, the current head of the segment, not start. Writing moving.next = start happens to work on the first move, when the segment head is still start, and silently breaks on the second. The loop runs right − left times, not right − left + 1, because the first node of the segment never moves. With left === right the loop does nothing, and the answer is correct.

The time is O(n): up to left steps to find before, then right − left moves of a few assignments each. The space is O(1).

Pairs, which are groups of two

Swapping every two adjacent nodes is the smallest case. For a pair prev → a → b → next the goal is prev → b → a → next. Order matters: first a.next = b.next so that a already points at the rest, then b.next = a, then prev.next = b. Last, prev = a, since a is now the tail of the swapped pair.

function swapPairs(head) {
  const dummy = new ListNode(0, head);
  let prev = dummy;
  while (prev.next !== null && prev.next.next !== null) {
    const a = prev.next;
    const b = a.next;
    a.next = b.next; // a skips over b
    b.next = a; // b points back at a
    prev.next = b; // the previous pair's tail now leads to b
    prev = a; // a is the tail of the swapped pair
  }
  return dummy.next;
}

The loop condition needs both prev.next and prev.next.next to exist, so an odd node left over at the end stays where it is. On 1 → 2 → 3 → 4 → 5: the first pair gives 2 → 1 → 3 → 4 → 5, the second gives 2 → 1 → 4 → 3 → 5, and then only 5 remains, so the loop stops.

Reversing in groups of k

Now generalise: reverse each consecutive block of k nodes, and leave the final block alone if it has fewer than k nodes. The plan for each block has four steps. Walk k nodes from groupPrev to find the block's last node, kth; if the walk falls off the list, stop. Remember groupNext, the node after the block. Reverse the block with the usual loop, but start prev at groupNext instead of null, so the block's old first node, once flipped, points straight at the rest of the list. Finally hang the reversed block off the previous one: groupPrev.next = kth.

function reverseKGroup(head, k) {
  const dummy = new ListNode(0, head);
  let groupPrev = dummy; // the node just before the current group

  while (true) {
    let kth = groupPrev;
    for (let i = 0; i < k && kth !== null; i++) kth = kth.next;
    if (kth === null) break; // fewer than k nodes left: leave them alone

    const groupNext = kth.next; // first node after the group
    let prev = groupNext; // the group's first node must end up pointing here
    let curr = groupPrev.next;
    while (curr !== groupNext) {
      const next = curr.next;
      curr.next = prev;
      prev = curr;
      curr = next;
    }

    const newTail = groupPrev.next; // the old first node is now the group's tail
    groupPrev.next = kth; // the old kth node is now the group's head
    groupPrev = newTail;
  }
  return dummy.next;
}

Dry run: reverseKGroup(1 → 2 → 3 → 4 → 5 → 6 → 7, k = 3)

blockgroupPrevkthgroupNextlist after the block
1dummy343 → 2 → 1 → 4 → 5 → 6 → 7
21673 → 2 → 1 → 6 → 5 → 4 → 7
34none—only the 7 is left, fewer than 3, so stop

Every node is walked once while looking for kth and once while it is reversed, so the time is O(n) and the space is O(1). A recursive version that reverses a block and recurses on the rest is shorter to write but uses O(n / k) stack. The usual bugs are reversing the short final block anyway (check kth before reversing), and forgetting to move groupPrev to the block's old first node, which is now its tail.

Rearranging without allocating — odd-even, partition and rotate

This family reorders the nodes of a list by relinking them: no new nodes, O(1) extra space. Two techniques cover almost all of it. The first is to build two chains and join them, each with its own dummy head. The second is to turn the list into a ring and cut it somewhere else.

Odd-even: group by position

Gather the nodes at positions 1, 3, 5, … first, followed by the nodes at positions 2, 4, 6, …, each group keeping its order. This is about positions, never about the values stored. Keep an odd pointer and an even pointer, and remember evenHead, because the even chain's start is otherwise lost. Each pass lets odd skip over an even node and then lets even skip over the new odd node.

function oddEvenList(head) {
  if (head === null) return null;
  let odd = head;
  let even = head.next;
  const evenHead = even; // remember where the even chain starts
  while (even !== null && even.next !== null) {
    odd.next = even.next; // odd skips over the even node
    odd = odd.next;
    even.next = odd.next; // even skips over the new odd node
    even = even.next;
  }
  odd.next = evenHead; // stitch the chains together
  return head;
}
passodd.next becomeseven.next becomesodd chaineven chain
setup——12
1341 → 32 → 4
25null1 → 3 → 52 → 4
endodd.next = evenHead1 → 3 → 5 → 2 → 4

The loop test is even !== null && even.next !== null: the second half is needed because the body reads even.next, and the first keeps a one-node list (where even starts as null) from crashing. The time is O(n), the space O(1).

Partition around a value

Move every node with a value below x in front of the others, but keep the original relative order within each side. Two dummy heads give you two chains: append each node to the small or the large chain as you pass it, then join them. Because you reuse the original nodes, one line is easy to forget and costly to forget.

function partition(head, x) {
  const smallHead = new ListNode(0);
  const largeHead = new ListNode(0);
  let small = smallHead;
  let large = largeHead;

  for (let node = head; node !== null; node = node.next) {
    if (node.val < x) {
      small.next = node;
      small = node;
    } else {
      large.next = node;
      large = node;
    }
  }
  large.next = null; // the last large node may still point at a small one
  small.next = largeHead.next;
  return smallHead.next;
}
⚠ The line that prevents a cycle: large.next = null The last node appended to the large chain still holds its original next, which can be a small node that now sits earlier in the answer. Run partition(1 → 4 → 3 → 2 → 5 → 2, 3) without that line and the 5 still points at the final 2, whose next is the 4, so the list loops forever. With the line the result is 1 → 2 → 2 → 4 → 3 → 5. The same stale-pointer mistake appears in any problem that reuses nodes: before you return, ask which node is the new last one and where its next points.

Removing every node with a given value is the same shape with one chain, and it was written earlier in the dummy-node section.

Rotate right by k: close the ring, cut elsewhere

Rotating 1 → 2 → 3 → 4 → 5 right by 2 gives 4 → 5 → 1 → 2 → 3. Picture the list closed into a ring: rotating does not reorder anything, it only chooses a different place to cut. The new tail is the node at index length − k − 1 and the new head is the node after it. So: walk once to find the length and the old tail, reduce k modulo the length, walk to the new tail, cut, and link the old tail to the old head.

function rotateRight(head, k) {
  if (head === null || head.next === null) return head;

  let length = 1;
  let tail = head;
  while (tail.next !== null) {
    tail = tail.next;
    length++;
  }

  k %= length; // rotating by the length changes nothing
  if (k === 0) return head;

  let newTail = head;
  for (let i = 0; i < length - k - 1; i++) newTail = newTail.next;
  const newHead = newTail.next;
  newTail.next = null; // cut the ring here
  tail.next = head; // close the old end back onto the old head
  return newHead;
}

For the example the length is 5 and k = 2, so the new tail is the node at index 2 (the 3), the new head is the 4, and the old tail 5 is linked back to the 1. Two details cost offers. k can exceed the length (rotating a 5-node list by 7 is rotating it by 2), so take k %= length. And when that leaves k === 0 you must return early: otherwise the "new tail" is the old tail, its next is null, and you return null. The time is O(n) with two walks, the space O(1).

Composing the primitives — palindrome and reorder list

Many "hard" list problems are not new ideas. They are two or three of the earlier pieces glued together: find the middle, reverse the second half, then walk both halves together. Palindrome walks them comparing; reorder walks them weaving. Learn the three pieces and these become assembly.

There is one catch, and it is an off-by-one that the earlier findMiddle invites. For a list like 1 → 2 → 2 → 1 that function returns the second middle, the third node. To split a list we want the end of the first half, the second node. Changing the loop test to fast.next !== null && fast.next.next !== null does it: for an even length slow stops on the last node of the first half, and for an odd length it stops on the exact middle, which stays in the first half.

function endOfFirstHalf(head) {
  let slow = head;
  let fast = head;
  while (fast.next !== null && fast.next.next !== null) {
    slow = slow.next;
    fast = fast.next.next;
  }
  return slow; // for an even length this is the LAST node of the first half
}

function isPalindrome(head) {
  if (head === null || head.next === null) return true;

  const firstEnd = endOfFirstHalf(head);
  const secondHead = reverseList(firstEnd.next);

  let a = head;
  let b = secondHead;
  let same = true;
  while (same && b !== null) {
    if (a.val !== b.val) same = false;
    a = a.next;
    b = b.next;
  }

  firstEnd.next = reverseList(secondHead); // put the list back the way we found it
  return same;
}

function reorderList(head) {
  if (head === null || head.next === null) return;

  const firstEnd = endOfFirstHalf(head);
  let second = reverseList(firstEnd.next);
  firstEnd.next = null; // cut, so the first half ends cleanly

  let first = head;
  while (second !== null) {
    const firstNext = first.next;
    const secondNext = second.next;
    first.next = second;
    second.next = firstNext;
    first = firstNext;
    second = secondNext;
  }
}

Dry run: isPalindrome(1 → 2 → 3 → 2 → 1)

stepwhat happensstate
1endOfFirstHalf stops on the 3first half 1 → 2 → 3, second half 2 → 1
2reverse the second halfsecond half is 1 → 2
3compare a = 1 with b = 1equal, advance both
4compare a = 2 with b = 2equal, advance both
5b is null, stopthe middle 3 was never compared, which is correct
6reverse the second half again and relink itlist is 1 → 2 → 3 → 2 → 1 once more, answer true

The loop runs on b because the reversed second half is never longer than the first. The last line puts the list back the way it arrived. Interviewers often ask whether you mutated the input, and a function that quietly leaves a list half-reversed is a bug in anything that calls it afterwards. The cost is three linear passes plus one more to restore: O(n) time and O(1) space, against the O(n) space of copying the values into an array.

Reorder list: L0 → Ln → L1 → Ln-1 → …

Rearrange 1 → 2 → 3 → 4 → 5 into 1 → 5 → 2 → 4 → 3: first node, last node, second node, second-to-last, and so on. The picture is a zipper. Cut the list after endOfFirstHalf, reverse the second half, and interleave the two halves one node at a time.

passfirstsecondwiringlist so far
setup15first half 1 → 2 → 3, second half reversed 5 → 4—
1151.next = 5, 5.next = 21 → 5 → 2 → 3
2242.next = 4, 4.next = 31 → 5 → 2 → 4 → 3
end3nullthe loop stops when second runs out1 → 5 → 2 → 4 → 3
⚠ Cut the first half, or the zipper closes into a ring The line firstEnd.next = null matters. Without it the node 3 keeps pointing at the 4, and after the interleave the 4 points back at the 3, so 3 → 4 → 3 → … goes round forever. When you reverse a half in place, the node that used to link into it still points at its old position until you cut it.

Copying a list that has random pointers

Each node now has two pointers: next, and random, which may point at any node in the list, or at null. Build a deep copy: a completely new set of nodes, with the same values and with next and random pointing at the corresponding new nodes. No node of the copy may point into the original.

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

What makes this harder than copying a plain list is a chicken-and-egg problem. When you create the copy of node 3 and want its random to point at the copy of node 7, that copy does not exist yet. The tempting shortcut, new Node(node.random.val), creates a stray node that merely holds an equal value; the copy's shape is wrong, and two original nodes that shared a random target would now point at two different new nodes.

Approach one: a map from original to copy

Split the work in two passes. The first pass creates every copy and records the correspondence in a Map keyed by the original node objects. The second pass wires next and random by looking up the copy of whichever node the original pointed at. By then every copy exists, so there is nothing left to wait for.

function copyRandomList(head) {
  if (head === null) return null;

  const copyOf = new Map(); // original node -> its copy
  for (let node = head; node !== null; node = node.next) {
    copyOf.set(node, new Node(node.val));
  }
  for (let node = head; node !== null; node = node.next) {
    const copy = copyOf.get(node);
    copy.next = node.next === null ? null : copyOf.get(node.next);
    copy.random = node.random === null ? null : copyOf.get(node.random);
  }
  return copyOf.get(head);
}

Approach two: weave the copies into the list

The map only answers one question: "where is the copy of this node?". If each copy sits immediately after its original in the same list, the answer needs no map: the copy of x is x.next. That gives three passes, with O(1) extra space beyond the new nodes themselves:

  1. Insert a copy after every original: A → B → C becomes A → A′ → B → B′ → C → C′.
  2. Set copy.random = original.random.next, the copy of the original's random target (or null).
  3. Unweave: give each original back its own next, and chain the copies together.
function copyRandomListInPlace(head) {
  if (head === null) return null;

  for (let node = head; node !== null; node = node.next.next) {
    node.next = new Node(node.val, node.next); // the copy sits right after its original
  }
  for (let node = head; node !== null; node = node.next.next) {
    node.next.random = node.random === null ? null : node.random.next; // random's copy is random's neighbour
  }

  const copyHead = head.next;
  for (let node = head; node !== null; node = node.next) {
    const copy = node.next;
    node.next = copy.next; // the original skips its copy
    copy.next = copy.next === null ? null : copy.next.next; // the copy skips the next original
  }
  return copyHead;
}
stagenodes (values 7, 13, 11; the random of 13 and of 11 is the node 7)
input7 → 13 → 11
after pass 17 → 7′ → 13 → 13′ → 11 → 11′
after pass 27′.random = null, 13′.random = 7.next = 7′, 11′.random = 7′
after pass 3original 7 → 13 → 11 untouched; copy 7′ → 13′ → 11′ with its own randoms
⚠ Order of the passes, and cleaning up after yourself Pass 2 relies on the copy sitting next to its original, so it must finish before pass 3 breaks that adjacency. Pass 3 must restore the original list: node.next = copy.next gives each original its old successor back. A copy that leaves the input half-woven is wrong even if the returned list looks right. The last copy's next must also become null, which the guard in the code handles.

Complexity. Both versions are O(n) time. The map version needs O(n) extra space for the map, the weave version O(1). To check the claim about time, count how often the code reads a next pointer, using a node class whose next is a getter that increments a counter. The counts for lists of increasing size:

nmap versionweave version
1,0003,99912,999
2,0007,99925,999
4,00015,99951,999

Doubling n doubles both counts: linear in each case, with the weave version doing about three times as many pointer reads in exchange for not needing a map. Keep both in your head. The map is easy to write correctly under pressure; the weave is the follow-up for "can you do it in constant space?".

Say it like this → "I'll copy in two passes with a map from original node to copy, so the second pass can always find the copy it needs. If I must avoid the map, I can weave each copy right after its original, so the list itself becomes the lookup table, then split the two lists apart."

LRU cache — a hash map plus a doubly linked list with sentinels

Design a cache of fixed capacity with two operations, get(key) and put(key, value), both in O(1). When a put would exceed the capacity, evict the least recently used key. A successful get and every put count as a use.

Each requirement kills one simple structure. A hash map finds a key in O(1) but knows nothing about recency. A list ordered by recency knows what to evict but needs O(n) to find a key. The combination works: a Map from key to list node, and a doubly linked list ordered by recency. The map locates a node in O(1); the list moves it to the front, or removes the last one, in O(1).

The list must be doubly linked. To move a node you first unlink it from the middle, which needs its predecessor, and a singly linked list can only find that by walking from the head, the O(n) cost the earlier doubly linked section warned about. With prev stored in the node, unlinking is two assignments.

Sentinel nodes remove the edge cases. Put two fixed dummy nodes in the list, a head and a tail, which are never removed and never hold data. Every real node then has a real node (or a sentinel) on both sides, so unlink and pushFront are the same few lines whether the list has zero, one or a hundred nodes, with no if (node.prev) checks. The most recent node is always head.next and the least recent is always tail.prev.

class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.nodes = new Map(); // key -> node
    this.head = { key: null, value: null, prev: null, next: null }; // sentinel: the most recent node sits after it
    this.tail = { key: null, value: null, prev: null, next: null }; // sentinel: the least recent node sits before it
    this.head.next = this.tail;
    this.tail.prev = this.head;
  }

  unlink(node) {
    node.prev.next = node.next;
    node.next.prev = node.prev;
  }

  pushFront(node) {
    node.prev = this.head;
    node.next = this.head.next;
    this.head.next.prev = node;
    this.head.next = node;
  }

  get(key) {
    const node = this.nodes.get(key);
    if (node === undefined) return -1;
    this.unlink(node);
    this.pushFront(node); // a read counts as a use
    return node.value;
  }

  put(key, value) {
    if (this.capacity === 0) return;
    const existing = this.nodes.get(key);
    if (existing !== undefined) {
      existing.value = value;
      this.unlink(existing);
      this.pushFront(existing);
      return;
    }
    if (this.nodes.size === this.capacity) {
      const oldest = this.tail.prev; // the least recently used node
      this.unlink(oldest);
      this.nodes.delete(oldest.key); // this is why a node stores its key
    }
    const node = { key, value, prev: null, next: null };
    this.nodes.set(key, node);
    this.pushFront(node);
  }
}

Three invariants keep the structure honest: the map's keys are exactly the keys of the real nodes in the list; the list runs from most recent (after head) to least recent (before tail); and the size never exceeds the capacity. Every operation preserves them.

Dry run: capacity 2

operationreturnslist, most recent firstnote
put(1, 10)—1new key
put(2, 20)—2, 1new key goes to the front
get(1)101, 2a read moves 1 to the front
put(3, 30)—3, 1full: evict tail.prev, which is 2
get(2)−13, 12 was evicted
put(1, 11)—1, 3existing key: update the value, move to the front
put(4, 40)—4, 1full: evict 3
get(3)−14, 1miss; a miss changes nothing
get(1)111, 4the updated value, now the most recent
get(4)404, 1read moves 4 to the front

Complexity. Every operation is a lookup in the map plus a fixed number of pointer assignments, so each is O(1) (the map's lookups, inserts and deletes are O(1) on average). The space is O(capacity): at most that many nodes and map entries exist at once.

⚠ The bugs that sink an LRU answer
  • Evicting the node from the list but not deleting its key from the map, so a later get finds a detached node. This is why each node stores its own key.
  • Forgetting that updating an existing key is also a use: it must move the node to the front, not just change its value.
  • Writing pushFront with the assignments in the wrong order. Read head.next into the new node's next before overwriting it.
  • Capacity 0: tail.prev is then the head sentinel, and "evicting" it would unlink a sentinel. The guard in put handles it.

A JavaScript Map remembers insertion order, so deleting a key and setting it again moves it to the back, and the first key is the oldest. That makes a short LRU possible with a Map alone, and it is exactly the reference used in the next section. In an interview that asks for the data structure, though, the list-plus-map version is the answer being looked for. The design-problems chapter has a practice exercise, ex-lru-cache, for this one.

Say it like this → "A hash map gives O(1) lookup, and a doubly linked list gives O(1) removal and reordering, so I keep both: the map from key to node, and the list in recency order with sentinels at both ends so there are no null checks. get and put each unlink a node and push it to the front, and eviction removes tail.prev and deletes its key from the map."

Checking pointer code against a simple reference

Pointer code fails quietly. It passes the example in the problem and then breaks on a shape nobody thought of: a self-pointing random, an empty list, a repeated key. The best defence is to write a slow, obviously correct reference next to the real solution and compare them on many small random inputs. Two kinds of check are worth knowing.

A structural check: is it really a deep copy?

For the copy-with-random problem, comparing values is not enough, because value-equal nodes that point at the wrong places look identical in a printout. Instead reduce each list to its shape: for each node, its value and the index of its random target. Two lists with equal shapes are wired the same way. Then separately check that the copy shares no node with the original.

function serializeShape(head) {
  const index = new Map(); // node -> its position in the list
  const nodes = [];
  for (let node = head; node !== null; node = node.next) {
    index.set(node, nodes.length);
    nodes.push(node);
  }
  return nodes.map((node) => [node.val, node.random === null ? null : index.get(node.random)]);
}

function isDeepCopy(original, copy) {
  const originals = new Set();
  for (let node = original; node !== null; node = node.next) originals.add(node);
  for (let node = copy; node !== null; node = node.next) {
    if (originals.has(node)) return false; // the copy borrowed one of the original's nodes
  }
  return JSON.stringify(serializeShape(original)) === JSON.stringify(serializeShape(copy));
}

Both helpers run in O(n) time with a Map or Set, so they can be applied to every random list you generate. Serialize the original before the copy and again after: if the two serializations differ, the copy damaged its input, which is exactly the bug the weave version risks.

A behavioural check: the list against a Map

For the LRU cache the reference is the Map-only version, where the order of the keys is the recency order. Replay the same random sequence of operations on both, and after every step compare the return value and the full recency order, read by walking the real list from head.next to tail.

class ReferenceLRU {
  constructor(capacity) {
    this.capacity = capacity;
    this.map = new Map(); // insertion order = recency order, oldest first
  }

  get(key) {
    if (!this.map.has(key)) return -1;
    const value = this.map.get(key);
    this.map.delete(key);
    this.map.set(key, value);
    return value;
  }

  put(key, value) {
    if (this.capacity === 0) return;
    this.map.delete(key);
    this.map.set(key, value);
    if (this.map.size > this.capacity) this.map.delete(this.map.keys().next().value);
  }
}

function checkAgainstReference(capacity, steps) {
  const cache = new LRUCache(capacity);
  const reference = new ReferenceLRU(capacity);
  for (const [op, key, value] of steps) {
    if (op === "put") {
      cache.put(key, value);
      reference.put(key, value);
    } else if (cache.get(key) !== reference.get(key)) {
      return false;
    }
    const order = [];
    for (let node = cache.head.next; node !== cache.tail; node = node.next) order.push(node.key);
    if (order.join() !== [...reference.map.keys()].reverse().join()) return false;
  }
  return true;
}

Feed it sequences such as the following, with a small key range so that hits, misses, updates and evictions all occur:

function randomSteps(count) {
  const steps = [];
  for (let i = 0; i < count; i++) {
    const key = Math.floor(Math.random() * 6);
    steps.push(Math.random() < 0.5 ? ["put", key, i] : ["get", key]);
  }
  return steps;
}

A bug in an unlink or in an eviction shows up within a few steps as a mismatch in order, long before it could show up as a wrong answer. A runaway pointer bug in a test is a hung tab, so when you print a list under test, stop after a limit, for example for (let n = head, i = 0; n !== null && i < 100; n = n.next, i++). Whatever the structure, the habit is the same: a slow reference, many tiny random inputs, and a comparison after every operation instead of once at the end.

The pointer-surgery checklist

Almost every list bug in an interview comes from one of the lines below. Run them as a checklist over any list code before you say it is done.

CheckThe failure it prevents
Did I save next before overwriting it?the rest of the list becomes unreachable
Can the head change? Is there a dummy node?a special case for the first node, and a wrong return value
Where does the new last node's next point?a stale pointer, which makes a cycle (partition, reorder)
Is the loop test ordered x !== null && x.next !== null?a TypeError reading .next of null
Am I comparing nodes or values?wrong answers on lists with repeated values (intersection, cycle)
Is the loop count right − left or + 1? Is k reduced modulo the length?off-by-one on the segment, a wrong rotation
Did I promise to leave the input unchanged?a half-reversed or half-woven list for the next caller
Did I try 0, 1 and 2 nodes?the three lengths where most of these functions break

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

Recognising the harder list problems

The basic signals above get you to "this is a list problem". These phrases tell you which of the longer techniques it is.

The prompt says…Reach for
"reverse between positions", "reverse in groups of k", "swap in pairs"dummy node, then head insertion or a bounded reverse loop
"palindrome", "reorder the list from both ends"middle, reverse the second half, then walk both halves
"first node where two lists meet"switch heads at null, compare nodes
"add two numbers" stored one digit per nodewalk both with a carry; keep the loop going while there is a carry
"odd and even positions", "split by a value", "rotate"two dummy chains, or close the ring and cut
"deep copy" with an extra pointeroriginal-to-copy map, or weave the copies in
"sort a list", "merge k lists"split with a one-step-ahead fast pointer and merge; merge in pairs or use a heap
"find the duplicate" in an array of valid indices, nothing to modifythe array is a list: Floyd's two phases
"design a cache" with O(1) eviction orderhash map plus doubly linked list with sentinels
Before you move on
  • Write reverseList from memory, and explain why next must be saved before curr.next is overwritten.
  • Trace reverseList on a new short list by hand, tracking prev, curr, and next each step.
  • Explain why the fast pointer moving 2 steps per 1 of the slow pointer guarantees a meeting inside a cycle.
  • Explain what problem the dummy-node trick solves, with a concrete edge case (removing the actual head) it sidesteps.
  • Given a new problem, recognize whether it needs fast/slow pointers ("middle," "cycle," "nth from the end") before writing any code.
  • Write reverseBetween by head insertion and explain why moving.next = before.next cannot be start.
  • Trace reverseKGroup with k = 3 on seven nodes, and say what prev = groupNext buys you.
  • Explain the line large.next = null in partition and what the list does without it.
  • Say why palindrome and reorder need the end of the first half, and which middle findMiddle returns on an even list.
  • Explain why switching heads at null makes the two intersection pointers arrive together, and why the comparison is on nodes.
  • Copy a list with random pointers twice, once with a map and once by weaving, and say which pass must not run early.
  • Build the LRU cache from a map and a doubly linked list with sentinels, and name the bug if the map entry is not deleted on eviction.
  • Describe how you would test any of these against a slow reference on small random inputs.

29Practice this chapter

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

  • 8 testsReverse Linked Listbeginner
  • 8 testsReverse Linked List IIintermediate
  • 8 testsMerge Two Sorted Listsbeginner
  • 8 testsLinked List Cyclebeginner
  • 8 testsLinked List Cycle IIintermediate
  • 8 testsFind the Duplicate Numberintermediate
  • 8 testsMiddle of the Linked Listbeginner
  • 8 testsRemove Nth Node From Endintermediate
  • 8 testsRemove Linked List Elementsbeginner
  • 7 testsDelete the Middle Node of a Linked Listbeginner
  • 8 testsPalindrome Linked Listbeginner
  • 8 testsIntersection of Two Linked Listsbeginner
  • 8 testsAdd Two Numbersintermediate
  • 8 testsAdd Two Numbers IIintermediate
  • 8 testsMerge K Sorted Listsadvanced
  • 8 testsCopy List with Random Pointerintermediate
  • 8 testsReorder Listintermediate
  • 8 testsSwap Nodes in Pairsintermediate
  • 8 testsReverse Nodes in k-Groupadvanced
  • 8 testsRotate Listintermediate
  • 8 testsPartition Listintermediate
  • 8 testsOdd Even Linked Listintermediate
  • 8 testsRemove Duplicates from Sorted Listbeginner
  • 8 testsRemove Duplicates from Sorted List IIintermediate
  • 8 testsDelete Node in a Linked Listbeginner
  • 8 testsSort Listintermediate
  • 8 testsFlatten a Multilevel Doubly Linked Listintermediate
  • 8 testsDesign Linked Listintermediate
  • 8 testsLRU Cacheadvanced
All 217 for this level →

✓Check yourself

5 questions, pass at 4. The check runs in your browser and the result stays on this device, so it is a self-check and not a certificate.

B12

Finished reading?

0 of 48 chapters read in this series

Mark as read

→Next stop

  1. B11Previous · B11Stacks & queues
  2. B12you are here
  3. Next · I1TreesA linked list that's allowed to branch — and the three orders you can walk it in.23 min read

01On this page

22 sections · 0%

  1. 01What you're giving up, and what you get for it
  2. 02Doubly linked lists — pay more memory, get backward traversal free
  3. 03The node and the walk
  4. 04Reversal — the pattern that trips people up live
  5. 05Fast/slow pointers — find the middle in one pass
  6. 06Cycle detection — Floyd's algorithm
  7. 07Finding WHERE the cycle starts — Floyd's phase two
  8. 08Floyd on an array — finding the duplicate number
  9. 09Merging two sorted lists — the dummy node in action
  10. 10Merge sort on a list, and merging K sorted lists
  11. 11Nth from the end — the gap technique
  12. 12Two lists that meet — intersection, and adding two numbers
  13. 13The dummy-node trick — kills a whole class of edge-case bugs
  14. 14Reversing part of a list — a sublist, pairs and groups of k
  15. 15Rearranging without allocating — odd-even, partition and rotate
  16. 16Composing the primitives — palindrome and reorder list
  17. 17Copying a list that has random pointers
  18. 18LRU cache — a hash map plus a doubly linked list with sentinels
  19. 19Checking pointer code against a simple reference
  20. 20The pointer-surgery checklist
  21. 21Recognizing it in an unseen problem
  22. 22Recognising the harder list problems
Mark as read

[] chapters · t top