Merge Two Sorted Lists
Given two sorted singly linked lists
L₁andL₂, return a single sorted linked list containing all the nodes of both — by rewiringnextpointers in place, no new node allocations. The algorithm is the two-pointer merge: walk both lists in lockstep, repeatedly choose the smaller front node, append it to the output, advance the chosen list. Same logic as Merge Sort’s merge step (CLRS §2.3.1) but applied to linked storage rather than arrays — and crucially O(1) auxiliary space because we splice existing nodes rather than copy values. The classic LC 21 question; also the building block for K-Way Merge on linked lists, and a frequent vehicle for testing the sentinel/dummy head pattern that simplifies almost every linked-list problem with a non-obvious head node.
1. Intuition — The Two-Stack Card Sort
Imagine you have two stacks of playing cards on the table, each already sorted ascending from top to bottom. You want to combine them into a single sorted stack.
Look at the top card of each stack. Whichever is smaller, place face-up on a new “output” pile. Look at the new tops of both stacks. Whichever is smaller, place on the output pile. Repeat. When one stack runs out, dump the rest of the other on top of the output pile (it’s already sorted, so no further work is needed).
This is exactly the merge step of Merge Sort — and it’s also exactly what we do for linked lists, except instead of physically moving cards onto a new pile, we rewire pointers so the existing nodes form a single chain. No allocation, no copying, just next-pointer reassignments.
The output pile starts off as a sentinel (a “dummy head” with no payload) so we have a stable reference even before any real node is chosen. After the merge, the real head is the sentinel’s next.
2. Tiny Worked Example
Merge L₁: 1 → 4 → 5 and L₂: 1 → 3 → 4.
Set up a sentinel and a tail pointer that always points to the last node of the output so far.
Initial state
L1: [1] → [4] → [5] → null
L2: [1] → [3] → [4] → null
sentinel: [DUMMY | next=null]
tail: → DUMMY
Iteration 1
Compare L1.value (1) and L2.value (1). Tie — convention is to take from L1 (we’ll discuss the stability angle in §6).
Choose L1's first node ([1]):
tail.next := L1 → sentinel.next = [1 from L1]
tail := L1 (tail now points at [1 from L1])
L1 := L1.next (L1 now points at [4])
Output (so far): DUMMY → [1₁] → ?
↑
L1=[4] L2=[1₂]
I’m using subscript ₁/₂ to disambiguate the two 1s.
Iteration 2
Compare L1.value (4) and L2.value (1). L2 is smaller, so take from L2.
tail.next := L2 → [1₁].next = [1₂]
tail := L2
L2 := L2.next
Output: DUMMY → [1₁] → [1₂] → ?
↑
L1=[4] L2=[3]
Iteration 3
Compare L1.value (4) and L2.value (3). L2 smaller.
tail.next := L2 → [1₂].next = [3]
tail := L2 = [3]
L2 := L2.next = [4₂]
Output: DUMMY → [1₁] → [1₂] → [3] → ?
↑
L1=[4₁] L2=[4₂]
Iteration 4
Compare L1.value (4₁) and L2.value (4₂). Tie — take from L1 by convention.
tail.next := L1 → [3].next = [4₁]
tail := [4₁]
L1 := [5]
Output: DUMMY → [1₁] → [1₂] → [3] → [4₁] → ?
↑
L1=[5] L2=[4₂]
Iteration 5
Compare L1.value (5) and L2.value (4₂). L2 smaller.
tail.next := L2 → [4₁].next = [4₂]
tail := [4₂]
L2 := L2.next = null
Output: DUMMY → [1₁] → [1₂] → [3] → [4₁] → [4₂] → ?
↑
L1=[5] L2=null
Cleanup — One List Exhausted
L2 == null, so we attach all of L1 (which is already sorted) to the tail:
tail.next := L1 → [4₂].next = [5]
Output: DUMMY → [1₁] → [1₂] → [3] → [4₁] → [4₂] → [5] → null
Return
return sentinel.next — i.e., the node [1₁]. The sentinel itself is not part of the result; it served only to give us a uniform “tail” reference during the merge.
Six pointer reassignments (one per real node), no allocations, O(n+m) total time, O(1) auxiliary space.
3. Pseudocode
merge(L1, L2):
sentinel := new Node(value=null)
tail := sentinel
while L1 != null and L2 != null:
if L1.value <= L2.value: # <= preserves stability (L1 wins ties)
tail.next := L1
L1 := L1.next
else:
tail.next := L2
L2 := L2.next
tail := tail.next
# One of L1, L2 is null; the other is the sorted suffix to splice on.
if L1 != null:
tail.next := L1
else:
tail.next := L2
return sentinel.next
The sentinel pattern is doing two jobs: (a) providing a starting point for tail before any real node is appended, and (b) giving us a single reference to return sentinel.next regardless of whether L1 or L2 contributed the first node.
4. Python Implementation — Iterative
from typing import Optional
class Node:
"""Singly-linked list node — same shape as in [[Singly Linked List]] and friends."""
def __init__(self, value, next: Optional["Node"] = None):
self.value = value
self.next = next
def merge_two_sorted(l1: Optional[Node], l2: Optional[Node]) -> Optional[Node]:
"""LC 21. Merge two sorted singly linked lists, in place, into one sorted list.
O(n + m) time, O(1) auxiliary space."""
sentinel = Node(value=None) # dummy head — discard at the end
tail = sentinel
while l1 is not None and l2 is not None:
if l1.value <= l2.value: # <= for stability — L1 wins ties
tail.next = l1
l1 = l1.next
else:
tail.next = l2
l2 = l2.next
tail = tail.next
# Splice on the remaining tail of whichever list still has nodes.
tail.next = l1 if l1 is not None else l2
return sentinel.nextA few implementation choices worth flagging:
- Sentinel. Without it, the first iteration would need a special branch (“is the output empty?”) to set the result-head; instead, every iteration uniformly does
tail.next = chosen; tail = tail.next. The sentinel is allocated once at the top and discarded at the end. <=not<. This makes the merge stable with respect to lists where equal-value nodes fromL1should come before equal-value nodes fromL2. This matters most when this routine is called from Merge Sort on linked lists, where stability is one of merge sort’s selling points.- Final splice. When one list is exhausted, the other is non-empty and already sorted — splice the entire remainder in O(1) by setting
tail.next = remaining_list. No need to copy node-by-node.
5. Recursive Variant
A more concise version that mirrors the recursive merge in CLRS:
def merge_two_sorted_recursive(l1: Optional[Node], l2: Optional[Node]) -> Optional[Node]:
"""O(n + m) time, O(n + m) call-stack space."""
if l1 is None:
return l2
if l2 is None:
return l1
if l1.value <= l2.value:
l1.next = merge_two_sorted_recursive(l1.next, l2)
return l1
else:
l2.next = merge_two_sorted_recursive(l1, l2.next)
return l2Each recursive call peels off one front node and recurses on the rest. The base case is “one input is empty — return the other.” The recursion depth is n + m (one frame per node), so the stack space is O(n + m) — the same asymptotic class as the iterative O(1) solution, but with a much larger constant and a real stack-overflow risk for long lists. Lead with the iterative version in interviews unless explicitly asked for recursion.
6. Stability — Why <= Matters
A stable merge preserves the relative order of equal-keyed elements. If L1 contributes a (4, "Alice") and L2 contributes a (4, "Bob") (both with key 4), a stable merge always emits Alice before Bob — because Alice came from L1, the “earlier” input.
The <= comparison ensures this: when l1.value == l2.value, the condition l1.value <= l2.value is true, so we take from L1. If we used <, the equal case would fall to the else branch and take from L2 first — unstable.
This matters when the merge step is used inside Merge Sort on linked lists. Merge sort’s stability is one of its main advantages over Quicksort; that stability is delivered by exactly this <= choice in the merge subroutine.
7. Complexity
| Metric | Iterative | Recursive |
|---|---|---|
| Time | O(n + m) | O(n + m) |
| Auxiliary space | O(1) | O(n + m) (call stack) |
| Allocations | 1 (sentinel) | n + m (one frame per node) |
| Stable? | Yes (with <=) | Yes (with <=) |
Why O(n+m) time: every iteration either advances L1 or advances L2, never both, never neither. Each list is walked at most once. Total iterations ≤ n + m. The final splice is O(1). Hence O(n+m).
Why O(1) auxiliary space (iterative): we allocate exactly one sentinel node (and free it at the end — actually, we just stop holding a reference; in GC’d languages the sentinel is reclaimed). Beyond that, only a constant number of pointer variables (tail, l1, l2). The merged list reuses the input nodes.
Why this is optimal: any algorithm that produces all n + m nodes in sorted order must touch each at least once (Ω(n+m) lower bound). The iterative merge achieves this with a constant per-node overhead (one comparison, one assignment, one pointer advance). Asymptotically tight.
8. Use Cases
8.1 Building Block for Linked-List Merge Sort
Merge Sort applied to a linked list:
- Find the middle of the list using slow/fast pointers (see Singly Linked List §4.4).
- Split into two halves at the middle.
- Recursively sort each half.
- Merge the two sorted halves using this routine.
Because each of these steps is O(1) auxiliary (or O(log n) for the recursion stack), linked-list merge sort runs in O(n log n) time and O(log n) space — strictly better than copying to an array and back.
This is one of the few places linked lists win in practice over arrays: array merge sort needs O(n) auxiliary space for the merge buffer, while linked-list merge sort needs only the recursion stack. For sorting linked lists as linked lists, merge sort is the canonical choice.
8.2 K-Way Merge
K-Way Merge generalizes from 2 to k sorted inputs. The two natural approaches:
- Sequential pairwise merge. Merge L1 and L2; merge the result with L3; merge with L4; …
O(k · n)total work wherenis the average list length, because the running merged list grows and is re-walked each time. - Min-heap-based merge. Place the head of each of
klists into a min-heap; repeatedly pop the smallest, advance that list, push the new front.O(N log k)total, whereN = total node count. Strictly better than sequential pairwise for largek. - Tournament / divide-and-conquer. Recursively merge pairs of lists, halving
keach round.O(N log k)like the heap approach, but often with better constants.
The 2-list merge from this note is the leaf operation in approaches (a) and (c). Approach (b) doesn’t use the 2-list merge directly — it uses a heap of single-node fronts. See K-Way Merge for full discussion.
8.3 Database Sort-Merge Join
When two relations are already sorted (or sorted via an index) on the join key, the sort-merge join algorithm is essentially a two-list merge that emits matched pairs. The same O(n + m) linear-walk pattern, with comparison logic adapted to “match” rather than “select smaller.” This is a foundational join algorithm in every relational database; the linked-list version is the conceptual prototype.
8.4 Streaming Merge of Sorted Logs
Distributed systems frequently produce sorted-by-timestamp log streams from multiple machines. Merging them on a central collector for analysis is exactly this routine, generalized to k inputs. Frameworks like Apache Kafka’s compactor and the Linux sort -m flag both implement variants of merge.
9. Pitfalls
9.1 Forgetting the Sentinel
Without a sentinel, you need a special branch for the first iteration: “is the output empty? if so, set head; otherwise append to tail.” This adds an if to every loop iteration (or requires hoisting it out via a check before the loop). The sentinel eliminates this; the cost is one extra Node allocation. Always use the sentinel.
9.2 Forgetting the Final Splice
After the main loop, exactly one of l1 and l2 is non-null (the one with longer remaining suffix). You must attach this remainder to tail.next. Forgetting this step truncates the merged list at the length of the shorter input. A subtle bug that passes simple test cases (where both lists have the same length) and fails on asymmetric inputs.
9.3 Wrong Comparison Direction
if l1.value <= l2.value — choose L1. Some implementations write this backward (if l2.value < l1.value — choose L2), and confuse stability or off-by-one. Pick a direction and stick with it.
9.4 Using < Instead of <= and Losing Stability
Functionally either works (the merged list is sorted), but stability is broken with <. If the caller relies on stability (as merge sort does), this is a silent correctness bug — the output is sorted but elements with equal keys are reordered.
9.5 Allocating New Nodes Instead of Splicing
A common LeetCode-novice mistake:
# Anti-pattern: O(n+m) auxiliary space.
result = []
while l1 and l2:
if l1.value <= l2.value:
result.append(l1.value); l1 = l1.next
else:
result.append(l2.value); l2 = l2.next
# ... build a new list from `result` ...This allocates n + m new nodes (or holds n + m values in a Python list before reconstructing), wasting O(n+m) space. The whole point of merge-on-linked-lists is to reuse the input nodes by rewiring pointers. Splice, don’t copy.
9.6 Returning the Sentinel Instead of sentinel.next
return sentinel returns a node whose value is None (or 0, or whatever your sentinel uses) followed by the actual sorted list — i.e., the result has a junk head. return sentinel.next skips the sentinel. Easy mistake; LeetCode tests catch it instantly because the first emitted value is wrong.
9.7 Mutating the Inputs When You Shouldn’t
This routine mutates l1 and l2 in place — the input lists no longer exist as separate lists after the merge. If the contract requires preserving the inputs, you’d have to copy each node before splicing — adding O(n+m) time and space. Read the problem statement; LC 21 explicitly allows in-place mutation, but production code may not.
9.8 Recursive Stack Overflow for Long Lists
The recursive variant uses one stack frame per node. Python’s default recursion limit is 1000; lists longer than that crash. Either raise the limit (sys.setrecursionlimit(10**7)), use an explicit stack, or — better — use the iterative version.
9.9 Cyclic Inputs
If either input has a cycle (e.g., L1’s tail points back into L1), the loop never terminates because neither list reaches null. Detect cycles first via Linked List Cycle Detection if cycle inputs are possible.
10. Diagram — Pointer State at Three Snapshots
SNAPSHOT 0 (start)
l1 → [1] → [4] → [5] → null
l2 → [1] → [3] → [4] → null
sentinel → DUMMY (next=null)
tail → DUMMY
SNAPSHOT 2 (after appending [1] from L1, then [1] from L2)
l1 → [4] → [5] → null
l2 → [3] → [4] → null
sentinel → DUMMY → [1₁] → [1₂]
tail ──────────^
SNAPSHOT FINAL (after main loop and final splice)
l1 → null (consumed)
l2 → null (consumed)
sentinel → DUMMY → [1₁] → [1₂] → [3] → [4₁] → [4₂] → [5] → null
tail ──────^
return sentinel.next → [1₁]
flowchart LR S0[sentinel<br/>DUMMY] --> A0[1 from L1] --> B0[1 from L2] --> C0[3] --> D0[4 from L1] --> E0[4 from L2] --> F0[5] --> Z0[null]
What this diagram shows. Three snapshots in time. At Snapshot 0 the output is just the sentinel; both inputs are intact. At Snapshot 2, two nodes have been appended (the two 1s), one from each input, and the inputs have advanced. At the final snapshot, both inputs have been entirely consumed and spliced into a single chain reachable from sentinel.next. The mermaid diagram below the snapshots shows the final structure as a single sorted chain. Notice that no new value-carrying nodes were allocated — the same physical nodes that were originally in L1 and L2 now form the merged chain, just with their next pointers rewritten.
11. Common Interview Problems
| Problem | LeetCode | Pattern |
|---|---|---|
| Merge Two Sorted Lists | LC 21 | This note (the canonical question) |
| Merge K Sorted Lists | LC 23 | K-Way Merge — heap-based or divide-and-conquer pairwise |
| Sort List (sort a linked list) | LC 148 | Merge sort with this routine as the merge step |
| Add Two Numbers | LC 2 | Pairwise sum with carry — same two-pointer skeleton |
| Intersection of Two Linked Lists | LC 160 | Two-pointer walk with a length-equalizing trick |
| Merge Intervals | LC 56 | Same idea on arrays of intervals (not linked lists) |
LC 21 is on virtually every “linked list 101” interview list. LC 23 (K-way merge) is the senior-level extension. Sort List (LC 148) ties the two together — it asks you to sort a linked list, the natural answer is merge sort, and the merge step is exactly LC 21 — a delightful example of how foundational this routine is.
12. Open Questions
- Is there any in-place merging algorithm that beats O(n+m) time on linked lists? No — every node must be visited (Ω(n+m) lower bound) and pointers rewritten. The constants are already nearly optimal: one comparison, one assignment, one pointer advance per node.
- When does it make sense to merge two linked lists by copying values into an array, sorting, and rebuilding? Almost never — array merge is O(n+m) too but with worse constants on linked-storage inputs.
13. See Also
- Singly Linked List — the structure being merged
- Linked List Reversal — the other canonical pointer-rewire routine
- Linked List Cycle Detection — companion linked-list technique
- Merge Sort — uses this as the merge step (its base case for linked lists)
- K-Way Merge — generalizes from 2 to k sorted inputs
- Binary Heap — used in K-way merge’s min-heap approach
- Two Pointers — the technique family
- Big-O Notation
- SWE Interview Preparation MOC