Merge Two Sorted Lists
Compare the heads of two sorted lists, pick the smaller one, advance that list's pointer — repeat until one list is exhausted, then attach the remainder. This simple O(n + m) merge is the heart of merge sort and the building block for merging K sorted lists with a heap.
Table of contents
- Before we start
- Picture this first (no code yet)
- The actual problem
- First, the slow way (so you feel the pain)
- The turning point
- The one idea to remember
- The code, line by line
- Watch it happen, frame by frame
- Recursive version (elegant, O(n+m) stack space)
- Extension: Merge K Sorted Lists (LC #23)
- Where to spot this pattern
- Common traps
- Check yourself
- Practice problems
Before we start
This is one of the most fundamental operations in computer science — it is the "merge" in merge sort. Master it on linked lists and you will understand merge sort deeply enough to implement it on arrays, on lists, and eventually extend it to K lists using a heap. By the end you will be able to:
- Write the two-list merge from memory with a dummy head node.
- Trace the pointer dance step by step.
- Extend the idea to Merge K Sorted Lists using a min-heap.
Picture this first (no code yet)
A real-life story
Two airport check-in queues, both already sorted by ticket number. A single agent is merging them into one line. She looks at the first person in each queue. Whoever has the lower ticket number goes next. She waves that person forward, and the next person in that queue steps up. She glances again — lower number goes next. She repeats until one queue empties entirely, then she waves the whole remaining queue through at once.
She never looks more than one person deep into each queue. She never re-examines anyone already waved through. Total work: exactly n + m comparisons for queues of sizes n and m.
The actual problem
Merge Two Sorted Lists (LC #21):
Given the heads of two sorted linked lists
l1andl2, merge them into one sorted list and return its head.
l1: 1 → 2 → 4
l2: 1 → 3 → 4
Output: 1 → 1 → 2 → 3 → 4 → 4
First, the slow way (so you feel the pain)
Collect all values from both lists into an array, sort the array, build a new linked list. O((n+m) log(n+m)) for the sort, O(n+m) extra space for the array. The comparison-based merge is O(n+m) time, O(1) extra space (just a few pointer variables) — no sorting, no extra array.
The turning point
Pause & think
You are at the point where l1 starts with 2 and l2 starts with 3. You pick 2 and advance l1 to 4. Now l1 starts with 4 and l2 starts with 3. You pick 3. Now l1 starts with 4 and l2 is empty. What do you do? Do you need to copy the remaining l1 nodes one by one?
Answer
No — you just attach the rest of l1 directly. Since both lists were sorted, every remaining node in l1 is ≥ everything already placed in the merged list. You point the tail of the merged list to l1 (or whichever list is non-empty). One pointer assignment finishes the rest of the merge — O(1) at the end, not O(n).
The one idea to remember
The entire pattern in one sentence
Use a dummy head node to eliminate edge-case handling; keep a tail pointer at the last merged node; compare l1.val and l2.val, link the smaller one to tail, advance that list and tail; when one list empties, attach the other entirely.
The code, line by line
def mergeTwoLists(l1, l2):
dummy = ListNode(0) # dummy eliminates "is the result list empty?" checks
tail = dummy # tail always points to the last node of the merged list
while l1 and l2: # while both lists have remaining nodes
if l1.val <= l2.val:
tail.next = l1 # link smaller node
l1 = l1.next # advance l1
else:
tail.next = l2
l2 = l2.next
tail = tail.next # advance tail to the newly linked node
# at least one list is now empty — attach the other
tail.next = l1 if l1 else l2
return dummy.next # skip the dummy node
Line by line:
dummy = ListNode(0)— a sentinel that letstail.next = ...work even for the very first node, with no special "is result empty?" check.tail = dummy— tail starts at dummy and advances with every node added.while l1 and l2:— keep comparing while both lists are non-empty.tail.next = l1— we are not creating a new node; we are rewiringl1's existing node into the merged list.tail = tail.next— must advance tail after each link, ortail.nextwould always overwrite the same connection.tail.next = l1 if l1 else l2— one line handles the remainder;l1andl2are already sorted, so the remaining nodes are all valid and in order.return dummy.next— the dummy node itself isn't part of the answer.
Watch it happen, frame by frame
l1: 1 → 2 → 4, l2: 1 → 3 → 4
dummy → ? tail = dummy
Iter 1: l1.val=1 == l2.val=1 → pick l1 (<=).
tail.next = node1(l1). l1 → node2. tail → node1.
merged so far: dummy → 1
Iter 2: l1.val=2, l2.val=1 → pick l2.
tail.next = node1(l2). l2 → node3. tail → node1(l2).
merged so far: dummy → 1 → 1
Iter 3: l1.val=2, l2.val=3 → pick l1.
tail.next = node2(l1). l1 → node4(l1). tail → node2.
merged so far: dummy → 1 → 1 → 2
Iter 4: l1.val=4, l2.val=3 → pick l2.
tail.next = node3. l2 → node4(l2). tail → node3.
merged so far: dummy → 1 → 1 → 2 → 3
Iter 5: l1.val=4, l2.val=4 → pick l1 (<=).
tail.next = node4(l1). l1 → None. tail → node4(l1).
merged so far: dummy → 1 → 1 → 2 → 3 → 4
Loop exits (l1 is None).
tail.next = l2 = node4(l2).
Final: 1 → 1 → 2 → 3 → 4 → 4 ✅
Recursive version (elegant, O(n+m) stack space)
def mergeTwoLists_recursive(l1, l2):
if not l1: return l2
if not l2: return l1
if l1.val <= l2.val:
l1.next = mergeTwoLists_recursive(l1.next, l2)
return l1
else:
l2.next = mergeTwoLists_recursive(l1, l2.next)
return l2
Beautiful, but uses O(n+m) stack space. Prefer the iterative version for long lists in production.
Extension: Merge K Sorted Lists (LC #23)
Given K sorted linked lists, merge all of them into one sorted list.
Naive: merge pairs repeatedly. K lists × n nodes each. Merge l1+l2 → n+n = 2n. Merge result + l3 → 3n. Total work: n + 2n + 3n + … + Kn = O(K² × n). For K=1000 and n=1000 that is 1 billion operations.
Better: min-heap. Push the head of each list into a min-heap. Always extract the minimum (O(log K)), link it to the result, and push the extracted node's next (if it exists) back into the heap.
import heapq
def mergeKLists(lists):
dummy = ListNode(0)
tail = dummy
heap = []
# Initialise heap with head of each non-empty list
for i, node in enumerate(lists):
if node:
heapq.heappush(heap, (node.val, i, node)) # (value, index, node)
while heap:
val, i, node = heapq.heappop(heap) # extract minimum
tail.next = node
tail = tail.next
if node.next:
heapq.heappush(heap, (node.next.val, i, node.next))
return dummy.next
Why (node.val, i, node)? Python's heap compares tuples element by element. If two nodes have the same value, it would try to compare the nodes themselves — which fails (ListNode has no < operator). The index i breaks ties without comparing nodes.
Complexity: O(N log K) where N = total nodes, K = number of lists. For K=1000 and N=10⁶, that is 10⁷ operations — 100× faster than the naive approach.
Where to spot this pattern
Trigger words:
- "merge two sorted lists/arrays"
- "merge k sorted lists"
- "sort a linked list" — merge sort: split at middle, sort halves, merge
- "find the median of a stream" — conceptually similar (Chapter 9.4)
Common traps
Watch out for these
- Forgetting
tail = tail.nextinside the loop. If you linktail.next = l1but don't advancetail, the next iteration overwrites the same connection — every node gets linked todummy.nextand only the last survives. - Not using a dummy head. Without a dummy, the first node addition requires an
if result is None: result = node; tail = resultbranch — messy. The dummy node makes every addition uniform:tail.next = node; tail = tail.next. - Using
tail.next = l1 or l2instead oftail.next = l1 if l1 else l2. In Python,l1 or l2works correctly for None-checks, but it's semantically fragile — ifl1is a node with val=0 (falsy in some languages),orfails. Use the explicit ternary orif/else. - In Merge K Lists: comparing nodes directly in the heap. Without a tiebreaker index,
heapqcrashes when two node values are equal and tries to compare ListNode objects (which don't support<). Always include an index in the heap tuple.
Remember this forever
Merge Two Sorted Lists
dummy = ListNode(0); tail = dummy
while l1 and l2:
if l1.val <= l2.val:
tail.next = l1; l1 = l1.next
else:
tail.next = l2; l2 = l2.next
tail = tail.next # ← must advance tail!
tail.next = l1 if l1 else l2
return dummy.next
Merge K Lists: min-heap with (val, index, node) — extract min, push next. O(N log K).
Trap: advance tail every iteration. Use index tiebreaker in K-list heap.
Check yourself
Why does the dummy head node make the code simpler? What would the code look like without it?
Without a dummy, you need to initialise the result head separately:
result = None
if l1.val <= l2.val:
result = l1; l1 = l1.next
else:
result = l2; l2 = l2.next
tail = result
Then the loop body is the same. This is 4 extra lines and an edge case (if not l1 and not l2: return None). The dummy node absorbs all of this — tail.next = node works identically for the first node and every subsequent node. One idiom, zero special cases.
For Merge K Sorted Lists with a heap, why is the complexity O(N log K) and not O(N log N)?
The heap never holds more than K elements at a time — one per list. Every heap push/pop operation costs O(log K), not O(log N). We perform exactly N pushes and N pops (one push and one pop per node across all lists). Total: O(N log K). Since K ≤ N always, this is at least as good as O(N log N) and often much better (K = 100, N = 100 000 → 100 000 × 7 ≈ 700 000 vs 100 000 × 17 ≈ 1 700 000).
Practice problems
| Problem | Difficulty | What to notice | Link |
|---|---|---|---|
| Merge Two Sorted Lists | Easy | Dummy node; advance tail every iteration | LC #21 |
| Merge K Sorted Lists | Hard | Min-heap with (val, idx, node) tiebreaker | LC #23 |
| Sort List | Medium | Split at middle (Chapter 4.2) + merge recursively | LC #148 |
| Merge Sorted Array | Easy | Same merge idea but on arrays in-place from the back | LC #88 |
Next up: Remove Nth Node from End — the N-gap two-pointer trick that finds the node to delete in a single pass without knowing the list length.