Loading content...
Merging two sorted lists into a single sorted list is one of the most elegant operations in computer science. It's the heart of merge sort—the algorithm that guarantees O(n log n) sorting regardless of input. It appears in database query execution, stream processing, and file handling. And it's a beautiful demonstration of how local decisions (comparing two elements) lead to global structure (a fully sorted list).
Unlike array merging where you need auxiliary space, linked list merging can be done entirely in-place by rewiring pointers. No new nodes are created; the existing nodes are simply stitched together in sorted order. This makes linked list merging both space-efficient and conceptually satisfying.
This page will take you from the basic two-list merge to the more advanced k-list merge, building the pattern recognition that makes these problems tractable.
By the end of this page, you will: (1) Merge two sorted linked lists efficiently, (2) Understand both iterative and recursive approaches, (3) Extend to merging k sorted lists with optimal complexity, (4) Connect list merging to merge sort, (5) Handle edge cases and corner scenarios confidently, (6) Recognize merging as a pattern in larger problems.
Problem Statement:
Given the heads of two sorted linked lists, merge them into a single sorted linked list and return the head of the merged list. The merged list should be made by splicing together the nodes of the original two lists.
Example:
list1: 1 → 3 → 5 → null
list2: 2 → 4 → 6 → null
result: 1 → 2 → 3 → 4 → 5 → 6 → null
Constraints:
| Aspect | Requirement |
|---|---|
| Input | Heads of two sorted linked lists |
| Output | Head of the merged sorted list |
| Space Complexity | O(1) for iterative, O(n+m) for recursive (call stack) |
| Time Complexity | O(n + m) where n and m are list lengths |
| Stability | Equal elements preserve their relative order |
| In-place | Yes — only pointer modifications, no new nodes |
Why is this important?
Merging sorted lists is a building block for:
Mastering this pattern opens doors to understanding many advanced algorithms.
The merge algorithm is beautifully simple once you see it:
Imagine two people reading from two sorted stacks of cards. Each person holds their stack with the smallest card on top. At each step:
In linked list terms:
Because both lists are already sorted, comparing just the current heads tells us which element is globally smallest among all remaining elements. We never need to look ahead or backtrack. This is what makes merging O(n + m) instead of O((n + m)²).
Step-by-Step Trace:
Let's trace merging 1 → 3 → 5 and 2 → 4 → 6:
| Step | Pointer 1 | Pointer 2 | Compare | Action | Result Built |
|---|---|---|---|---|---|
| 1 | 1 | 2 | 1 < 2 | Take 1, advance p1 | 1 |
| 2 | 3 | 2 | 3 > 2 | Take 2, advance p2 | 1 → 2 |
| 3 | 3 | 4 | 3 < 4 | Take 3, advance p1 | 1 → 2 → 3 |
| 4 | 5 | 4 | 5 > 4 | Take 4, advance p2 | 1 → 2 → 3 → 4 |
| 5 | 5 | 6 | 5 < 6 | Take 5, advance p1 | 1 → 2 → 3 → 4 → 5 |
| 6 | null | 6 | p1 empty | Attach p2 | 1 → 2 → 3 → 4 → 5 → 6 |
Total comparisons: 5 (one per step until exhaustion)
| Property | Value | Explanation |
|---|---|---|
| Comparisons | ≤ n + m - 1 | At most one comparison per element (except last) |
| Pointer advances | Exactly n + m | Each node visited exactly once |
| Memory writes | n + m | Each node's next pointer set once |
| Stability | Guaranteed | Equal elements from list1 come first |
The iterative approach uses a dummy node to simplify edge cases. Instead of special-casing which list provides the first element, we create a placeholder that we'll skip at the end. A "tail" pointer tracks where to attach the next node.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def merge_two_lists_iterative(list1: ListNode, list2: ListNode) -> ListNode: """ Merge two sorted linked lists into one sorted list. Time Complexity: O(n + m) where n, m are list lengths Space Complexity: O(1) - only uses a few pointers The dummy node pattern simplifies handling the head of the result. """ # Create a dummy node - its .next will be our answer dummy = ListNode(0) tail = dummy # tail always points to the last node of merged list # While both lists have nodes remaining while list1 is not None and list2 is not None: if list1.val <= list2.val: # Take from list1 tail.next = list1 list1 = list1.next else: # Take from list2 tail.next = list2 list2 = list2.next # Advance tail to the node we just added tail = tail.next # At most one of these is non-null # Attach whichever list still has nodes if list1 is not None: tail.next = list1 else: tail.next = list2 # dummy.next is the head of our merged list return dummy.next def merge_two_lists_clean(l1: ListNode, l2: ListNode) -> ListNode: """ Alternative: More compact version using walrus operator. """ dummy = tail = ListNode() 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 tail.next = l1 or l2 return dummy.nextWithout a dummy node, you'd need special logic to determine the head: which list provides the first element? The dummy node eliminates this: we always start building after the dummy, and return dummy.next. This pattern is ubiquitous in linked list problems involving list construction.
The recursive approach expresses merge with beautiful simplicity: the merged list is the smaller head followed by the merge of the remaining lists. This directly models the problem's recursive structure.
Recursive Insight:
merge(L1, L2) =
if L1 is empty: return L2
if L2 is empty: return L1
if L1.val ≤ L2.val:
return L1 with L1.next = merge(L1.next, L2)
else:
return L2 with L2.next = merge(L1, L2.next)
Each call reduces the problem size by one node, eventually hitting a base case.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
def merge_two_lists_recursive(list1: ListNode, list2: ListNode) -> ListNode: """ Merge two sorted linked lists recursively. Time Complexity: O(n + m) Space Complexity: O(n + m) due to recursion stack Each call handles one node, reducing problem size by 1. """ # Base case 1: first list empty, return second if list1 is None: return list2 # Base case 2: second list empty, return first if list2 is None: return list1 # Recursive case: compare heads if list1.val <= list2.val: # list1's head is smaller, so it comes first # Recursively merge the rest list1.next = merge_two_lists_recursive(list1.next, list2) return list1 else: # list2's head is smaller list2.next = merge_two_lists_recursive(list1, list2.next) return list2 def merge_with_trace(l1: ListNode, l2: ListNode, depth: int = 0) -> ListNode: """ Same algorithm with tracing to visualize recursion. """ indent = " " * depth l1_val = l1.val if l1 else "null" l2_val = l2.val if l2 else "null" print(f"{indent}merge({l1_val}, {l2_val})") if l1 is None: print(f"{indent} Base case: l1 empty, return l2") return l2 if l2 is None: print(f"{indent} Base case: l2 empty, return l1") return l1 if l1.val <= l2.val: print(f"{indent} {l1.val} <= {l2.val}, take {l1.val}") l1.next = merge_with_trace(l1.next, l2, depth + 1) return l1 else: print(f"{indent} {l1.val} > {l2.val}, take {l2.val}") l2.next = merge_with_trace(l1, l2.next, depth + 1) return l2A robust merge implementation handles all edge cases gracefully. Let's enumerate them:
Edge Case 1: Both lists empty
list1: null, list2: null → result: null
Edge Case 2: One list empty
list1: 1 → 2 → 3, list2: null → result: 1 → 2 → 3
The non-empty list is returned unchanged.
Edge Case 3: Single-element lists
list1: 5, list2: 3 → result: 3 → 5
Simplest non-trivial case.
Edge Case 4: Duplicate values
list1: 1 → 2 → 2, list2: 2 → 3 → 3
result: 1 → 2 → 2 → 2 → 3 → 3
Using <= instead of < ensures stability: equal elements from list1 come first.
Edge Case 5: Disjoint ranges
list1: 1 → 2 → 3, list2: 10 → 20 → 30
result: 1 → 2 → 3 → 10 → 20 → 30
One list is exhausted before the other; remaining list attached.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
def test_merge_edge_cases(): """Comprehensive test suite for merge functionality.""" # Helper to create linked list from array def create_list(values): if not values: return None head = ListNode(values[0]) curr = head for val in values[1:]: curr.next = ListNode(val) curr = curr.next return head # Helper to convert linked list to array def to_array(head): result = [] while head: result.append(head.val) head = head.next return result # Test 1: Both empty result = merge_two_lists_iterative(None, None) assert result is None # Test 2: First empty l1 = None l2 = create_list([1, 2, 3]) result = merge_two_lists_iterative(l1, l2) assert to_array(result) == [1, 2, 3] # Test 3: Second empty l1 = create_list([1, 2, 3]) l2 = None result = merge_two_lists_iterative(l1, l2) assert to_array(result) == [1, 2, 3] # Test 4: Equal lengths, interleaved l1 = create_list([1, 3, 5]) l2 = create_list([2, 4, 6]) result = merge_two_lists_iterative(l1, l2) assert to_array(result) == [1, 2, 3, 4, 5, 6] # Test 5: Duplicates l1 = create_list([1, 2, 2]) l2 = create_list([2, 3, 3]) result = merge_two_lists_iterative(l1, l2) assert to_array(result) == [1, 2, 2, 2, 3, 3] # Test 6: Disjoint ranges l1 = create_list([1, 2, 3]) l2 = create_list([10, 20, 30]) result = merge_two_lists_iterative(l1, l2) assert to_array(result) == [1, 2, 3, 10, 20, 30] # Test 7: Single elements l1 = create_list([5]) l2 = create_list([3]) result = merge_two_lists_iterative(l1, l2) assert to_array(result) == [3, 5] print("All merge tests passed! ✓")Using <= (less than or equal) instead of < ensures the merge is stable: when elements are equal, we take from list1 first. This preserves original ordering, which is crucial for merge sort's stability guarantee. Always use <= unless you have a specific reason not to.
A natural extension is merging k sorted lists instead of just two. This is a classic interview problem and appears in real systems like multi-way merge sort and consolidating sorted log files.
Problem Statement:
Given an array of k linked list heads, each sorted in ascending order, merge all lists into one sorted list.
Approach 1: Sequential Merge — O(kn)
Merge lists one by one: merge list[0] with list[1], then result with list[2], etc.
Approach 2: Divide and Conquer — O(n log k)
Merge pairs of lists, then merge results, like merge sort.
Approach 3: Priority Queue (Min-Heap) — O(n log k)
Maintain all k current heads in a min-heap. Pop smallest, add its next.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
import heapqfrom typing import List, Optional def merge_k_lists_heap(lists: List[Optional[ListNode]]) -> Optional[ListNode]: """ Merge k sorted lists using a min-heap. Time Complexity: O(n log k) where n = total nodes, k = number of lists Space Complexity: O(k) for the heap The heap always contains at most k nodes (one from each list). """ if not lists: return None # Python's heapq doesn't support custom comparators directly # We use (value, index, node) tuples where index breaks ties heap = [] # Add the head of each non-empty list to the heap for i, head in enumerate(lists): if head: heapq.heappush(heap, (head.val, i, head)) dummy = ListNode(0) tail = dummy while heap: # Pop the smallest element val, idx, node = heapq.heappop(heap) # Add it to our result tail.next = node tail = tail.next # If this list has more nodes, add the next one if node.next: heapq.heappush(heap, (node.next.val, idx, node.next)) return dummy.next def merge_k_lists_divide_conquer(lists: List[Optional[ListNode]]) -> Optional[ListNode]: """ Merge k sorted lists using divide and conquer. Time Complexity: O(n log k) Space Complexity: O(log k) for recursion stack This approach feels like running merge sort on the list of lists. """ if not lists: return None if len(lists) == 1: return lists[0] # Divide: split into two halves mid = len(lists) // 2 left = merge_k_lists_divide_conquer(lists[:mid]) right = merge_k_lists_divide_conquer(lists[mid:]) # Conquer: merge the two halves return merge_two_lists_iterative(left, right) def merge_k_lists_sequential(lists: List[Optional[ListNode]]) -> Optional[ListNode]: """ Merge k sorted lists sequentially (naive approach). Time Complexity: O(kn) in worst case Space Complexity: O(1) Simple but suboptimal - each element may be revisited k times. """ if not lists: return None result = lists[0] for i in range(1, len(lists)): result = merge_two_lists_iterative(result, lists[i]) return result| Approach | Time | Space | Best For |
|---|---|---|---|
| Sequential | O(kn) | O(1) | Small k, simplicity needed |
| Divide & Conquer | O(n log k) | O(log k) | Balanced complexity |
| Min-Heap | O(n log k) | O(k) | Streaming / large k |
For k=2, simple merge suffices. For small k (< 10), sequential merge is fine. For large k or streaming scenarios (lists arriving over time), the heap approach shines because it naturally handles dynamic inputs. Divide and conquer is the theoretical standard for batch processing.
List merging is the heart of merge sort, one of the most important sorting algorithms. On linked lists, merge sort has special advantages:
Array vs Linked List Merge Sort:
| Aspect | Array | Linked List |
|---|---|---|
| Merge Space | O(n) auxiliary | O(1) in-place |
| Element Access | Random O(1) | Sequential O(n) |
| Split Cost | O(1) with indices | O(n/2) to find middle |
| Cache Performance | Excellent | Poor |
Why Merge Sort is Preferred for Linked Lists:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
def sort_list(head: ListNode) -> ListNode: """ Sort a linked list using merge sort. Time Complexity: O(n log n) Space Complexity: O(log n) for recursion stack This is the preferred sorting algorithm for linked lists. """ # Base case: empty or single node if not head or not head.next: return head # Step 1: Find the middle using slow/fast pointers slow = head fast = head.next # Note: fast starts at head.next for correct split while fast and fast.next: slow = slow.next fast = fast.next.next # slow is now the end of the first half mid = slow.next slow.next = None # Split the list # Step 2: Recursively sort both halves left = sort_list(head) right = sort_list(mid) # Step 3: Merge sorted halves return merge_two_lists_iterative(left, right) def sort_list_bottom_up(head: ListNode) -> ListNode: """ Iterative (bottom-up) merge sort for linked lists. Time Complexity: O(n log n) Space Complexity: O(1) - no recursion stack This version avoids recursion for very long lists. """ if not head or not head.next: return head # Find length length = 0 curr = head while curr: length += 1 curr = curr.next dummy = ListNode(0) dummy.next = head # Merge sublists of increasing size: 1, 2, 4, 8, ... size = 1 while size < length: tail = dummy curr = dummy.next while curr: # Get first sublist of size 'size' left = curr right = split(left, size) # Returns head of second part curr = split(right, size) # Returns head of remainder # Merge and attach to tail merged = merge_two_lists_iterative(left, right) tail.next = merged while tail.next: tail = tail.next size *= 2 return dummy.next def split(head: ListNode, size: int) -> ListNode: """ Split list after 'size' nodes, return head of second part. """ for _ in range(size - 1): if not head: break head = head.next if not head: return None next_head = head.next head.next = None return next_headThe bottom-up (iterative) version uses O(1) space, making it safe for arbitrarily long lists. It's more complex but avoids stack overflow. Production systems handling millions of nodes should use this version.
List merging isn't just an academic exercise—it powers critical systems across the industry:
Whenever you see 'combine sorted inputs into sorted output,' think merge. The pattern applies whether inputs are lists, arrays, files, or streams. The core algorithm remains the same: compare heads, take smaller, advance.
tail.next = l1 or l2 after the loop.ListNode(val) for each element wastes memory and misses the point of in-place merging.tail = tail.next. Forgetting this overwrites nodes.dummy.next, not dummy. The dummy is a placeholder with garbage value.The most common bug in interviews is forgetting to attach the remainder. Test with disjoint ranges like [1,2,3] and [10,20,30]—if your result is [1,2,3] instead of [1,2,3,10,20,30], you have the remainder bug.
Merging sorted lists is a foundational pattern that appears throughout computer science. Let's consolidate the key insights:
What's Next:
In the next page, we'll explore Removing the Nth Node from End—a pattern that combines two-pointer traversal with precise pointer manipulation to solve a problem that seems like it would require two passes, but can be done in one.
You've mastered sorted list merging—a fundamental pattern powering algorithms from merge sort to database joins. You understand both two-list and k-list variants, can choose between iterative and recursive approaches, and recognize merging as a building block in larger systems.