HARD Lab · Linked List · 55

21. Merge Two Sorted Lists

Given two sorted linked lists, weave them into one sorted list by repeatedly plucking the smaller front node — a dummy head eliminates all edge cases and lets tail.next = … do all the work.

EasyLinked ListMergeDummy HeadTypeScript

PROBLEM What we're solving

You receive two singly-linked lists, each already sorted in non-decreasing order. Return the head of one merged sorted list built from the nodes of both. Example: 1→2→4 and 1→3→4 produce 1→1→2→3→4→4. Both lists may be empty; an empty list merged with any list returns that list unchanged.

KEY IDEA Dummy head makes the first node a non-event

Insight → Without a dummy head you need a special case to decide what head of the result is. With a dummy head const dummy = new ListNode(0), every attachment is identical: tail.next = chosen; tail = tail.next. The real head of the result is always dummy.next. This "sentinel" pattern appears throughout linked-list problems.

RECIPE Create dummy → compare heads → splice remainder

  • 0 · Create sentinel. dummy = new ListNode(0), tail = dummy. We never touch dummy.val.
  • 1 · Compare heads while both lists are non-null.Whichever head is smaller (or equal — pick either, ≤ is stable) gets attached: tail.next = winner; winner = winner.next; tail = tail.next.
  • 2 · Splice the leftover. Once one list empties, the other is already sorted — attach the whole remaining sublist with tail.next = list1 ?? list2. No need to walk it node by node.
  • 3 · Return dummy.next. Skip the sentinel. If both inputs were empty, dummy.next is null, which is correct.
Classic confusion → Forgetting to advance tailafter the attachment. The loop does two things per iteration: attach and advance. Write tail = tail.next! as the last line inside the loop — if you try to do it at the start of the next iteration, you lose track of where to attach next.

COST Complexity & alternatives

Collect + sort all values
O((m+n) log(m+n))
Ignores the sorted property; rebuilds nodes.
Iterative dummy-head merge
O(m + n)
O(1) extra space — just the dummy node and tail pointer.

Recursive alternative

The merge can also be written recursively: list1.val ≤ list2.val ? (list1.next = merge(list1.next, list2), list1) : …. Elegant but uses O(m + n) stack frames — fine for an interview, risky in production for very long lists.

Pattern transfer → The same dummy-head + tail approach scales directly to Merge k Sorted Lists (use a min-heap to pick the smallest head) and Sort List (merge-sort on a linked list uses this merge as its base). Any time you build a linked list incrementally, reach for a dummy head.

RUN IT Merge with a dummy head — always attach the smaller front node

step 0 / 7
STARTCreate a dummy head node (value 0). tailstarts at dummy. We'll always append to tail.next — this avoids special-casing the first node.
L1102142|L2103142|out
State
→ [0]
dummy
dummy
tail
1
i (L1 head)
1
j (L2 head)
list1 current headlist2 current headmerged / just attached
slowfast

TYPESCRIPT The solution, annotated

mergeTwoLists.ts
// Definition: class ListNode { val: number; next: ListNode | null; ... }

function mergeTwoLists(
  list1: ListNode | null,
  list2: ListNode | null,
): ListNode | null {
  const dummy = new ListNode(0);   // sentinel avoids special-casing the head
  let tail: ListNode = dummy;

  while (list1 !== null && list2 !== null) {
    if (list1.val <= list2.val) {
      tail.next = list1;
      list1 = list1.next;
    } else {
      tail.next = list2;
      list2 = list2.next;
    }
    tail = tail.next!;             // advance tail to the node just attached
  }

  // At most one list still has nodes — splice the remainder directly
  tail.next = list1 ?? list2;

  return dummy.next;               // skip the sentinel
}

Reading it block by block

Line 4 — create the sentinel. dummy is a throw-away node whose only purpose is to give tail somewhere to live before the first real node is attached. Its value is never read.
Lines 7–15 — compare-and-attach loop. While both lists have nodes, the smaller head gets wired to tail.next. We advance the source pointer (not tail) first, then advance tail last. This order keeps the rest of each source list accessible.
Line 18 — splice the remainder. Exactly one list (or neither) still has nodes. Attaching list1 ?? list2splices the entire remaining sublist in O(1) because it's already sorted and we just need to point at its head.
Line 20 — return dummy.next. The sentinel sits one step before the real head. Returning dummy.next skips it. If both inputs were null, this returns null automatically.
Complexity → O(m + n) time — every node from both lists is visited exactly once. O(1) extra space — only the dummy node and the tail pointer are allocated; nodes are re-linked, not copied.

INTERVIEWFollow-ups they'll ask

  • "Can you do it recursively?" Yes: base cases are either list being null; otherwise compare heads and recurse on the winner's next. O(m + n) stack depth is the trade-off.
  • "What if the lists aren't sorted?"This algorithm produces wrong output — it assumes sortedness. You'd need to sort first (O(n log n)) or use a different structure.
  • "How would you merge k sorted lists?" Replace the two-pointer comparison with a min-heap (priority queue) of size k. Each pop gives the globally smallest head; push the next node from that list. O(N log k) total.
  • "What if nodes can't be mutated?" Create new nodes and copy values in the same order — same O(m + n) loop, O(m + n) space.
  • "Why ≤ instead of < for the comparison?" Either works for correctness; is a common convention that preserves the relative order of equal-valued nodes from list1 (stable merge).

MNEMONIC The one-liner

"Dummy head waits at the gate — tail attaches each smaller plate."

TRIGGERS When you see ___ → reach for ___

merge two sorted linked listsdummy head + tail pointer
"build a linked list from scratch"dummy sentinel to avoid head special-case
one list exhausted before the othersplice remainder with tail.next = rest
merge k sorted listssame pattern + min-heap for the k-way comparison

SKELETON The reusable shape

skeleton.ts
function mergeTwoLists(
  list1: ListNode | null,
  list2: ListNode | null,
): ListNode | null {
  const dummy = new ListNode(0);
  let tail = dummy;
  while (list1 && list2) {
    if (list1.val <= list2.val) { tail.next = list1; list1 = list1.next; }
    else                        { tail.next = list2; list2 = list2.next; }
    tail = tail.next!;
  }
  tail.next = list1 ?? list2;
  return dummy.next;
}

FLASHCARDS Tap to flip

What is the dummy-head trick and why use it?
Allocate a sentinel node before the loop; tail starts there. Every attachment is uniform — no special case for the first node. Return dummy.next at the end.
tap to flip
1 / 7
Answer to reveal explanations. No penalty for retries.Score: 0 / 7
QUESTION 01
What is the time complexity of the iterative merge?
QUESTION 02
Why do we return dummy.next instead of dummy?
QUESTION 03
After the while-loop exits, one list may still have nodes. The correct way to attach them is:
QUESTION 04
What is the extra space used by the iterative solution (not counting input/output nodes)?
QUESTION 05
The recursive solution is correct but has a downside compared to the iterative one. What is it?
QUESTION 06
list1 = [1,2,4], list2 = [1,3,4]. What does the merge produce?
QUESTION 07
Where is the dummy-head pattern most directly reused?