0023. Merge k Sorted Lists¶
Table of Contents¶
- Problem
- Problem Breakdown
- Approach 1: Brute Force (Collect All Values, Sort)
- Approach 2: Min Heap / Priority Queue
- Approach 3: Divide and Conquer (Merge Sort Style)
- Complexity Comparison
- Edge Cases
- Common Mistakes
- Related Problems
- Visual Animation
Problem¶
| Leetcode | 23. Merge k Sorted Lists |
| Difficulty | |
| Tags | Linked List, Divide and Conquer, Heap (Priority Queue), Merge Sort |
Description¶
You are given an array of
klinked-listslists, each linked-list is sorted in ascending order. Merge all the linked-lists into one sorted linked-list and return it.
Examples¶
Example 1:
Input: lists = [[1,4,5],[1,3,4],[2,6]]
Output: [1,1,2,3,4,4,5,6]
Explanation: The linked-lists are:
1 -> 4 -> 5
1 -> 3 -> 4
2 -> 6
Merging them into one sorted list: 1 -> 1 -> 2 -> 3 -> 4 -> 4 -> 5 -> 6
Example 2:
Input: lists = []
Output: []
Example 3:
Input: lists = [[]]
Output: []
Constraints¶
k == lists.length0 <= k <= 10^40 <= lists[i].length <= 500-10^4 <= lists[i][j] <= 10^4lists[i]is sorted in ascending order- The sum of
lists[i].lengthwill not exceed10^4
Problem Breakdown¶
1. What is being asked?¶
Given k sorted linked lists, merge them all into a single sorted linked list. This is a generalization of merging 2 sorted lists (problem 21) to k lists.
2. What is the input?¶
| Parameter | Type | Description |
|---|---|---|
lists | ListNode[] | Array of heads of k sorted linked lists |
Important observations about the input: - k can be 0 (empty array) or lists can contain empty lists - Each individual list is already sorted in ascending order - Total number of nodes across all lists is at most 10^4
3. What is the output?¶
- The head of a single sorted linked list containing all nodes from all input lists
- If all input lists are empty, return
null/nil/None
4. Constraints analysis¶
| Constraint | Significance |
|---|---|
0 <= k <= 10^4 | Could have many lists — O(k) operations per node matters |
0 <= lists[i].length <= 500 | Individual lists can be empty |
sum of lengths <= 10^4 | Total nodes N is moderate — O(N log k) is efficient |
lists[i] sorted ascending | We can exploit sorted property with a min heap or merge |
5. Step-by-step example analysis¶
Example 1: lists = [[1,4,5],[1,3,4],[2,6]] -> [1,1,2,3,4,4,5,6]¶
List 0: 1 -> 4 -> 5
List 1: 1 -> 3 -> 4
List 2: 2 -> 6
Step-by-step merge (heap approach):
Heap: [(1, list0), (1, list1), (2, list2)]
Pop min (1, list0) -> result: [1], push (4, list0)
Heap: [(1, list1), (2, list2), (4, list0)]
Pop min (1, list1) -> result: [1,1], push (3, list1)
Heap: [(2, list2), (3, list1), (4, list0)]
Pop min (2, list2) -> result: [1,1,2], push (6, list2)
Heap: [(3, list1), (4, list0), (6, list2)]
Pop min (3, list1) -> result: [1,1,2,3], push (4, list1)
Pop min (4, list0) -> result: [1,1,2,3,4], push (5, list0)
Pop min (4, list1) -> result: [1,1,2,3,4,4], list1 exhausted
Pop min (5, list0) -> result: [1,1,2,3,4,4,5], list0 exhausted
Pop min (6, list2) -> result: [1,1,2,3,4,4,5,6], list2 exhausted
Result: [1,1,2,3,4,4,5,6]
6. Key Observations¶
- Generalization of merge two — merging k lists extends the classic merge-two-sorted-lists problem.
- Heap as k-way selector — a min heap can efficiently pick the smallest among k candidates in O(log k) time.
- Divide and conquer — recursively merge pairs of lists, halving the number of lists each round.
- All lists sorted — we only need to compare the current heads, not all remaining elements.
- Empty list handling — must gracefully handle empty input array and individual empty lists.
7. Pattern identification¶
| Pattern | Why it fits | Example |
|---|---|---|
| Min Heap / Priority Queue | Efficiently select minimum among k elements | This problem |
| Divide and Conquer | Merge pairs recursively like merge sort | This problem |
| Merge Two Sorted Lists | Building block for both approaches | LeetCode 21 |
Chosen pattern: Min Heap or Divide and Conquer Reason: Both achieve O(N log k) time. The heap approach processes one node at a time; divide and conquer merges pairs of lists level by level.
Approach 1: Brute Force (Collect All Values, Sort)¶
Thought process¶
Simplest approach: traverse all linked lists, collect every value into an array, sort the array, then build a new linked list from the sorted values.
Algorithm (step-by-step)¶
- Traverse all k linked lists, collecting every node's value into an array
- Sort the array
- Create a new linked list from the sorted values
- Return the head of the new list
Pseudocode¶
function mergeKLists(lists):
values = []
for each list in lists:
node = list
while node != null:
values.append(node.val)
node = node.next
sort(values)
dummy = ListNode(0)
curr = dummy
for v in values:
curr.next = ListNode(v)
curr = curr.next
return dummy.next
Complexity¶
| Complexity | Explanation | |
|---|---|---|
| Time | O(N log N) | Sorting all N values dominates |
| Space | O(N) | Storing all values in an array |
Implementation¶
Go¶
func mergeKListsBrute(lists []*ListNode) *ListNode {
vals := []int{}
for _, l := range lists {
for l != nil {
vals = append(vals, l.Val)
l = l.Next
}
}
sort.Ints(vals)
dummy := &ListNode{}
curr := dummy
for _, v := range vals {
curr.Next = &ListNode{Val: v}
curr = curr.Next
}
return dummy.Next
}
Java¶
public ListNode mergeKListsBrute(ListNode[] lists) {
List<Integer> vals = new ArrayList<>();
for (ListNode l : lists) {
while (l != null) {
vals.add(l.val);
l = l.next;
}
}
Collections.sort(vals);
ListNode dummy = new ListNode(0);
ListNode curr = dummy;
for (int v : vals) {
curr.next = new ListNode(v);
curr = curr.next;
}
return dummy.next;
}
Python¶
def mergeKListsBrute(self, lists):
vals = []
for l in lists:
while l:
vals.append(l.val)
l = l.next
vals.sort()
dummy = ListNode(0)
curr = dummy
for v in vals:
curr.next = ListNode(v)
curr = curr.next
return dummy.next
Dry Run¶
Input: lists = [[1,4,5],[1,3,4],[2,6]]
Step 1 — Collect values:
List 0: 1, 4, 5
List 1: 1, 3, 4
List 2: 2, 6
values = [1, 4, 5, 1, 3, 4, 2, 6]
Step 2 — Sort:
values = [1, 1, 2, 3, 4, 4, 5, 6]
Step 3 — Build linked list:
1 -> 1 -> 2 -> 3 -> 4 -> 4 -> 5 -> 6
Result: [1, 1, 2, 3, 4, 4, 5, 6] ✅
Approach 2: Min Heap / Priority Queue¶
The problem with Brute Force¶
Brute force ignores the fact that each list is already sorted. We discard all ordering information by dumping into an array. We can do better by exploiting the sorted property.
Optimization idea¶
Min Heap as a k-way merge selector! Maintain a min heap of size at most k, containing the current head of each non-empty list. Repeatedly extract the minimum, append it to the result, and push the next node from that list (if any).
Key insight: - At any point, the next smallest node must be the head of one of the k lists - A min heap finds the minimum among k elements in O(log k) time - We process each of the N total nodes exactly once
Algorithm (step-by-step)¶
- Create a min heap
- Push the head of each non-empty list into the heap
- While the heap is not empty: a. Pop the minimum node b. Append it to the result list c. If the popped node has a next, push next into the heap
- Return the result head
Pseudocode¶
function mergeKLists(lists):
heap = new MinHeap()
for i = 0 to k-1:
if lists[i] != null:
heap.push(lists[i])
dummy = ListNode(0)
curr = dummy
while heap is not empty:
node = heap.pop()
curr.next = node
curr = curr.next
if node.next != null:
heap.push(node.next)
return dummy.next
Complexity¶
| Complexity | Explanation | |
|---|---|---|
| Time | O(N log k) | Each of N nodes is pushed/popped from a heap of size at most k |
| Space | O(k) | Heap stores at most k nodes at any time |
Implementation¶
Go¶
func mergeKLists(lists []*ListNode) *ListNode {
h := &MinHeap{}
heap.Init(h)
for _, l := range lists {
if l != nil {
heap.Push(h, l)
}
}
dummy := &ListNode{}
curr := dummy
for h.Len() > 0 {
node := heap.Pop(h).(*ListNode)
curr.Next = node
curr = curr.Next
if node.Next != nil {
heap.Push(h, node.Next)
}
}
return dummy.Next
}
Java¶
public ListNode mergeKLists(ListNode[] lists) {
PriorityQueue<ListNode> pq = new PriorityQueue<>((a, b) -> a.val - b.val);
for (ListNode l : lists) {
if (l != null) pq.offer(l);
}
ListNode dummy = new ListNode(0);
ListNode curr = dummy;
while (!pq.isEmpty()) {
ListNode node = pq.poll();
curr.next = node;
curr = curr.next;
if (node.next != null) pq.offer(node.next);
}
return dummy.next;
}
Python¶
def mergeKLists(self, lists):
heap = []
for i, l in enumerate(lists):
if l:
heapq.heappush(heap, (l.val, i, l))
dummy = ListNode(0)
curr = dummy
while heap:
val, i, node = heapq.heappop(heap)
curr.next = node
curr = curr.next
if node.next:
heapq.heappush(heap, (node.next.val, i, node.next))
return dummy.next
Dry Run¶
Input: lists = [[1,4,5],[1,3,4],[2,6]]
Initial heap (val, list_index):
Push (1, 0), (1, 1), (2, 2)
Heap: [(1,0), (1,1), (2,2)]
Iteration 1: Pop (1, 0) -> result: [1], push (4, 0)
Heap: [(1,1), (2,2), (4,0)]
Iteration 2: Pop (1, 1) -> result: [1,1], push (3, 1)
Heap: [(2,2), (3,1), (4,0)]
Iteration 3: Pop (2, 2) -> result: [1,1,2], push (6, 2)
Heap: [(3,1), (4,0), (6,2)]
Iteration 4: Pop (3, 1) -> result: [1,1,2,3], push (4, 1)
Heap: [(4,0), (4,1), (6,2)]
Iteration 5: Pop (4, 0) -> result: [1,1,2,3,4], push (5, 0)
Heap: [(4,1), (5,0), (6,2)]
Iteration 6: Pop (4, 1) -> result: [1,1,2,3,4,4], list 1 exhausted
Heap: [(5,0), (6,2)]
Iteration 7: Pop (5, 0) -> result: [1,1,2,3,4,4,5], list 0 exhausted
Heap: [(6,2)]
Iteration 8: Pop (6, 2) -> result: [1,1,2,3,4,4,5,6], list 2 exhausted
Heap: [] -> done
Result: [1, 1, 2, 3, 4, 4, 5, 6] ✅
Approach 3: Divide and Conquer (Merge Sort Style)¶
Alternative to Heap¶
Instead of using a heap, we can apply merge sort's divide-and-conquer strategy. Pair up the lists and merge each pair. This halves the number of lists each round. After O(log k) rounds, only one merged list remains.
Optimization idea¶
Pair-wise merging like merge sort! In each round, merge lists[0] with lists[1], lists[2] with lists[3], etc. After one round, k lists become k/2 lists. After log(k) rounds, we have 1 list.
Key insight: - Each round processes all N nodes exactly once -> O(N) per round - There are O(log k) rounds - Total: O(N log k) — same as heap, but with better constant factors (no heap overhead)
Algorithm (step-by-step)¶
- If lists is empty, return null
- While there is more than one list: a. Create a new list of merged pairs b. For i = 0, 2, 4, ..., merge lists[i] and lists[i+1] c. If there's an odd one out, carry it over d. Replace lists with the merged results
- Return the single remaining list
Pseudocode¶
function mergeKLists(lists):
if lists is empty: return null
while len(lists) > 1:
merged = []
for i = 0 to len(lists) - 1 step 2:
l1 = lists[i]
l2 = lists[i+1] if i+1 < len(lists) else null
merged.append(mergeTwoLists(l1, l2))
lists = merged
return lists[0]
function mergeTwoLists(l1, l2):
dummy = ListNode(0)
curr = dummy
while l1 != null and l2 != null:
if l1.val <= l2.val:
curr.next = l1
l1 = l1.next
else:
curr.next = l2
l2 = l2.next
curr = curr.next
curr.next = l1 if l1 != null else l2
return dummy.next
Complexity¶
| Complexity | Explanation | |
|---|---|---|
| Time | O(N log k) | O(log k) rounds, each processing all N nodes |
| Space | O(1) | In-place merging (ignoring recursion stack); O(log k) if using recursive merge |
Implementation¶
Go¶
func mergeKLists(lists []*ListNode) *ListNode {
if len(lists) == 0 {
return nil
}
for len(lists) > 1 {
merged := []*ListNode{}
for i := 0; i < len(lists); i += 2 {
var l2 *ListNode
if i+1 < len(lists) {
l2 = lists[i+1]
}
merged = append(merged, mergeTwoLists(lists[i], l2))
}
lists = merged
}
return lists[0]
}
func mergeTwoLists(l1, l2 *ListNode) *ListNode {
dummy := &ListNode{}
curr := dummy
for l1 != nil && l2 != nil {
if l1.Val <= l2.Val {
curr.Next = l1
l1 = l1.Next
} else {
curr.Next = l2
l2 = l2.Next
}
curr = curr.Next
}
if l1 != nil {
curr.Next = l1
} else {
curr.Next = l2
}
return dummy.Next
}
Java¶
public ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0) return null;
List<ListNode> current = new ArrayList<>(Arrays.asList(lists));
while (current.size() > 1) {
List<ListNode> merged = new ArrayList<>();
for (int i = 0; i < current.size(); i += 2) {
ListNode l1 = current.get(i);
ListNode l2 = (i + 1 < current.size()) ? current.get(i + 1) : null;
merged.add(mergeTwoLists(l1, l2));
}
current = merged;
}
return current.get(0);
}
private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode curr = dummy;
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
curr.next = l1;
l1 = l1.next;
} else {
curr.next = l2;
l2 = l2.next;
}
curr = curr.next;
}
curr.next = (l1 != null) ? l1 : l2;
return dummy.next;
}
Python¶
def mergeKLists(self, lists):
if not lists:
return None
while len(lists) > 1:
merged = []
for i in range(0, len(lists), 2):
l1 = lists[i]
l2 = lists[i + 1] if i + 1 < len(lists) else None
merged.append(self.mergeTwoLists(l1, l2))
lists = merged
return lists[0]
def mergeTwoLists(self, l1, l2):
dummy = ListNode(0)
curr = dummy
while l1 and l2:
if l1.val <= l2.val:
curr.next = l1
l1 = l1.next
else:
curr.next = l2
l2 = l2.next
curr = curr.next
curr.next = l1 if l1 else l2
return dummy.next
Dry Run¶
Input: lists = [[1,4,5],[1,3,4],[2,6]]
Round 1 — Merge pairs:
Merge lists[0]=[1,4,5] and lists[1]=[1,3,4]:
Compare 1 vs 1 -> take 1 (list0), 1 (list1)
Compare 4 vs 3 -> take 3 (list1)
Compare 4 vs 4 -> take 4 (list0), 4 (list1)
Remaining: 5 (list0)
Merged: [1,1,3,4,4,5]
lists[2]=[2,6] has no pair -> carry over
After round 1: [[1,1,3,4,4,5], [2,6]]
Round 2 — Merge pairs:
Merge [1,1,3,4,4,5] and [2,6]:
1, 1, 2, 3, 4, 4, 5, 6
After round 2: [[1,1,2,3,4,4,5,6]]
Result: [1, 1, 2, 3, 4, 4, 5, 6] ✅
Complexity Comparison¶
| # | Approach | Time | Space | Pros | Cons |
|---|---|---|---|---|---|
| 1 | Brute Force (Sort) | O(N log N) | O(N) | Simple, easy to implement | Ignores sorted property, extra memory |
| 2 | Min Heap / Priority Queue | O(N log k) | O(k) | Optimal time, works well for streaming | Heap overhead per node |
| 3 | Divide and Conquer | O(N log k) | O(1)* | Optimal time, in-place, no extra DS | Slightly more complex to implement |
*O(1) extra space for iterative version; O(log k) for recursive merge-two.
Which solution to choose?¶
- In an interview: Approach 2 (Heap) or Approach 3 (D&C) — both O(N log k) and demonstrate different skills
- In production: Approach 3 — better cache locality, no heap overhead
- On Leetcode: Both Approach 2 and 3 pass easily
- For learning: Start with Approach 1 (build intuition), then Approach 2 (heap), then Approach 3 (D&C)
Edge Cases¶
| # | Case | Input | Expected Output | Reason |
|---|---|---|---|---|
| 1 | Empty array | lists = [] | [] | No lists to merge |
| 2 | Array with empty list | lists = [[]] | [] | Single empty list |
| 3 | Single list | lists = [[1,2,3]] | [1,2,3] | Nothing to merge |
| 4 | Multiple empty lists | lists = [[], [], []] | [] | All lists empty |
| 5 | Lists of different lengths | lists = [[1],[2,3],[4,5,6]] | [1,2,3,4,5,6] | Varying sizes |
| 6 | All same values | lists = [[1,1],[1,1]] | [1,1,1,1] | Duplicate handling |
| 7 | Negative values | lists = [[-2,-1],[0,1]] | [-2,-1,0,1] | Negative number support |
Common Mistakes¶
Mistake 1: Not handling empty lists in the input array¶
# ❌ WRONG — crashes if lists[i] is None/empty
for l in lists:
heapq.heappush(heap, (l.val, i, l)) # l could be None!
# ✅ CORRECT — check for non-empty lists
for i, l in enumerate(lists):
if l:
heapq.heappush(heap, (l.val, i, l))
Mistake 2: Comparing ListNode objects directly in Python heap¶
# ❌ WRONG — Python can't compare ListNode objects when values are equal
heapq.heappush(heap, (node.val, node)) # TypeError if two nodes have same val
# ✅ CORRECT — use a unique index as tiebreaker
heapq.heappush(heap, (node.val, unique_index, node))
Why: Python's heapq compares tuples element by element. If two nodes have the same value, it tries to compare the ListNode objects, which raises a TypeError.
Mistake 3: Forgetting to advance to next node in heap approach¶
# ❌ WRONG — keeps pushing the same node forever
node = heapq.heappop(heap)
heapq.heappush(heap, node) # infinite loop!
# ✅ CORRECT — push node.next, not node itself
node = heapq.heappop(heap)
if node.next:
heapq.heappush(heap, (node.next.val, i, node.next))
Mistake 4: Not handling odd number of lists in divide and conquer¶
# ❌ WRONG — index out of bounds when k is odd
for i in range(0, len(lists), 2):
merged.append(mergeTwoLists(lists[i], lists[i+1])) # fails if i+1 >= len
# ✅ CORRECT — handle the last unpaired list
for i in range(0, len(lists), 2):
l1 = lists[i]
l2 = lists[i+1] if i+1 < len(lists) else None
merged.append(mergeTwoLists(l1, l2))
Related Problems¶
| # | Problem | Difficulty | Similarity |
|---|---|---|---|
| 1 | 21. Merge Two Sorted Lists | Building block — merging two sorted lists | |
| 2 | 148. Sort List | Merge sort on linked list | |
| 3 | 264. Ugly Number II | Uses min heap to select next smallest | |
| 4 | 378. Kth Smallest Element in a Sorted Matrix | k-way merge / heap on sorted sequences | |
| 5 | 355. Design Twitter | Merging k sorted feeds using heap |
Visual Animation¶
Interactive animation: animation.html
In the animation: - Min Heap tab — visualizes the priority queue selecting the minimum among k list heads - Divide and Conquer tab — shows pair-wise merging of lists in rounds - Preset examples, custom input, step/play/pause/reset controls