Facebook Pixel

23. Merge k Sorted Lists

Problem Description

You have k sorted linked lists, where each linked list contains nodes arranged in ascending order. Your task is to merge all these linked lists into a single sorted linked list.

For example, if you have 3 linked lists:

  • List 1: 1 → 4 → 5
  • List 2: 1 → 3 → 4
  • List 3: 2 → 6

The merged result should be: 1 → 1 → 2 → 3 → 4 → 4 → 5 → 6

The solution uses a min heap (priority queue) to efficiently track and extract the smallest node among all list heads. Here's how it works:

  1. Initialize the heap: Add all non-null head nodes from the input lists to a min heap. The code adds a comparison method to ListNode so nodes can be compared by their values in the heap.

  2. Build the result: Create a dummy node to simplify list construction. While the heap isn't empty:

    • Extract the node with minimum value from the heap
    • If this node has a next node, add it to the heap
    • Append the extracted node to the result list
    • Move the current pointer forward
  3. Return the result: Return dummy.next, which points to the head of the merged list.

The time complexity is O(n * log k) where n is the total number of nodes across all lists and k is the number of lists, since each node is processed once and heap operations take O(log k) time. The space complexity is O(k) for the heap.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When merging two sorted lists, we repeatedly compare the heads and take the smaller one. But with k lists, comparing all k heads each time would be inefficient. We need a data structure that can quickly give us the minimum value among k elements and allow us to update it efficiently.

Think about the merge process: at any moment, we only care about the current smallest node among all the list heads. Once we take that node, we move to its next node in that particular list. This pattern suggests we need:

  1. A way to quickly find the minimum among k elements
  2. A way to remove that minimum
  3. A way to add a new element (the next node)

A min heap perfectly fits these requirements. It maintains the minimum element at the root with O(1) access, and both insertion and deletion take O(log k) time.

The key insight is that we don't need to store all nodes in the heap at once - that would use too much memory and wouldn't leverage the fact that each list is already sorted. Instead, we only keep track of the "frontier" - the current head of each list. As we consume nodes from one list, we advance that list's representative in the heap.

The comparison function __lt__ is added to ListNode because Python's heap needs to know how to order the nodes. By defining it to compare values, we ensure the heap maintains nodes in ascending order by their values.

This approach elegantly reduces the problem from "merge k sorted lists" to "repeatedly find and extract the minimum from k elements", which is exactly what a heap is designed to do efficiently.

Learn more about Linked List, Divide and Conquer, Heap (Priority Queue) and Merge Sort patterns.

Solution Approach

The solution implements a priority queue (min heap) approach to efficiently merge k sorted linked lists.

Step 1: Enable Node Comparison

setattr(ListNode, "__lt__", lambda a, b: a.val < b.val)

We add a less-than comparison method to the ListNode class. This allows Python's heap to compare nodes based on their values when maintaining heap order.

Step 2: Initialize the Min Heap

pq = [head for head in lists if head]
heapify(pq)

We create a list containing all non-null head nodes from the input lists, then convert it into a min heap using heapify(). This operation takes O(k) time where k is the number of lists.

Step 3: Create Dummy Node for Result

dummy = cur = ListNode()

A dummy node simplifies list construction by eliminating edge cases when adding the first node. The cur pointer tracks where to append the next node.

Step 4: Process Nodes from the Heap

while pq:
    node = heappop(pq)
    if node.next:
        heappush(pq, node.next)
    cur.next = node
    cur = cur.next

The main merging loop:

  • Extract the minimum node from the heap using heappop() - O(log k) time
  • If this node has a successor in its original list, add that successor to the heap with heappush() - O(log k) time
  • Append the extracted node to the result list
  • Move the cur pointer forward

This maintains the invariant that the heap always contains exactly one node from each non-exhausted list, ensuring we always select the global minimum among all current heads.

Step 5: Return Result

return dummy.next

Return the head of the merged list, which is dummy.next.

Time Complexity: O(n * log k) where n is the total number of nodes across all lists. Each node is processed exactly once, and each heap operation costs O(log k).

Space Complexity: O(k) for the heap, which stores at most k nodes at any time.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through merging 3 sorted linked lists:

  • List 1: 1 → 4 → 5
  • List 2: 1 → 3 → 4
  • List 3: 2 → 6

Initial Setup:

  • Add all head nodes to the min heap: [1(List1), 1(List2), 2(List3)]
  • After heapify, the heap maintains min property with 1 at the root
  • Create dummy node and cur pointer pointing to it

Iteration 1:

  • Extract min node: 1 from List 1
  • Add its next node 4 to heap: heap becomes [1(List2), 2(List3), 4(List1)]
  • Append 1 to result: dummy → 1
  • Move cur pointer to node 1

Iteration 2:

  • Extract min node: 1 from List 2
  • Add its next node 3 to heap: heap becomes [2(List3), 3(List2), 4(List1)]
  • Append 1 to result: dummy → 1 → 1
  • Move cur pointer forward

Iteration 3:

  • Extract min node: 2 from List 3
  • Add its next node 6 to heap: heap becomes [3(List2), 4(List1), 6(List3)]
  • Append 2 to result: dummy → 1 → 1 → 2
  • Move cur pointer forward

Iteration 4:

  • Extract min node: 3 from List 2
  • Add its next node 4 to heap: heap becomes [4(List1), 4(List2), 6(List3)]
  • Append 3 to result: dummy → 1 → 1 → 2 → 3
  • Move cur pointer forward

Iteration 5:

  • Extract min node: 4 from List 1
  • Add its next node 5 to heap: heap becomes [4(List2), 5(List1), 6(List3)]
  • Append 4 to result: dummy → 1 → 1 → 2 → 3 → 4
  • Move cur pointer forward

Iteration 6:

  • Extract min node: 4 from List 2
  • No next node to add to heap: heap becomes [5(List1), 6(List3)]
  • Append 4 to result: dummy → 1 → 1 → 2 → 3 → 4 → 4
  • Move cur pointer forward

Iteration 7:

  • Extract min node: 5 from List 1
  • No next node to add to heap: heap becomes [6(List3)]
  • Append 5 to result: dummy → 1 → 1 → 2 → 3 → 4 → 4 → 5
  • Move cur pointer forward

Iteration 8:

  • Extract min node: 6 from List 3
  • No next node to add to heap: heap becomes empty []
  • Append 6 to result: dummy → 1 → 1 → 2 → 3 → 4 → 4 → 5 → 6
  • Move cur pointer forward

Final Result: Return dummy.next which points to the head of the merged list: 1 → 1 → 2 → 3 → 4 → 4 → 5 → 6

The heap ensures we always extract the globally smallest node among all current list heads, maintaining the sorted order in the final result.

Solution Implementation

1from typing import List, Optional
2from heapq import heapify, heappop, heappush
3
4# Definition for singly-linked list.
5# class ListNode:
6#     def __init__(self, val=0, next=None):
7#         self.val = val
8#         self.next = next
9
10class Solution:
11    def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
12        """
13        Merge k sorted linked lists into one sorted linked list.
14      
15        Args:
16            lists: List of head nodes of k sorted linked lists
17          
18        Returns:
19            Head of the merged sorted linked list
20        """
21        # Add comparison method to ListNode for heap operations
22        # This allows heap to compare ListNode objects by their values
23        setattr(ListNode, "__lt__", lambda self, other: self.val < other.val)
24      
25        # Initialize priority queue with all non-null head nodes
26        priority_queue = [head for head in lists if head]
27      
28        # Convert list into a min-heap based on node values
29        heapify(priority_queue)
30      
31        # Create dummy node to simplify list construction
32        dummy_head = ListNode()
33        current_node = dummy_head
34      
35        # Process nodes from heap until empty
36        while priority_queue:
37            # Extract node with minimum value
38            min_node = heappop(priority_queue)
39          
40            # If extracted node has a next node, add it to heap
41            if min_node.next:
42                heappush(priority_queue, min_node.next)
43          
44            # Append the minimum node to result list
45            current_node.next = min_node
46            current_node = current_node.next
47      
48        # Return the head of merged list (skip dummy node)
49        return dummy_head.next
50
1/**
2 * Definition for singly-linked list.
3 * public class ListNode {
4 *     int val;
5 *     ListNode next;
6 *     ListNode() {}
7 *     ListNode(int val) { this.val = val; }
8 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
9 * }
10 */
11class Solution {
12    public ListNode mergeKLists(ListNode[] lists) {
13        // Initialize a min-heap to store nodes based on their values
14        // The heap will always give us the node with the smallest value
15        PriorityQueue<ListNode> minHeap = new PriorityQueue<>((a, b) -> a.val - b.val);
16      
17        // Add the head of each non-empty linked list to the heap
18        for (ListNode head : lists) {
19            if (head != null) {
20                minHeap.offer(head);
21            }
22        }
23      
24        // Create a dummy node to simplify list construction
25        ListNode dummyHead = new ListNode();
26        ListNode current = dummyHead;
27      
28        // Process nodes until the heap is empty
29        while (!minHeap.isEmpty()) {
30            // Extract the node with the smallest value
31            ListNode smallestNode = minHeap.poll();
32          
33            // If this node has a next node, add it to the heap
34            // This ensures we continue processing the list this node came from
35            if (smallestNode.next != null) {
36                minHeap.offer(smallestNode.next);
37            }
38          
39            // Append the smallest node to our result list
40            current.next = smallestNode;
41            current = current.next;
42        }
43      
44        // Return the merged list, skipping the dummy head
45        return dummyHead.next;
46    }
47}
48
1/**
2 * Definition for singly-linked list.
3 * struct ListNode {
4 *     int val;
5 *     ListNode *next;
6 *     ListNode() : val(0), next(nullptr) {}
7 *     ListNode(int x) : val(x), next(nullptr) {}
8 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
9 * };
10 */
11class Solution {
12public:
13    ListNode* mergeKLists(vector<ListNode*>& lists) {
14        // Define custom comparator for min-heap (priority queue)
15        // Returns true if a's value is greater than b's value (for min-heap behavior)
16        auto compare = [](ListNode* a, ListNode* b) { 
17            return a->val > b->val; 
18        };
19      
20        // Create a min-heap to store list nodes
21        // The node with smallest value will be at the top
22        priority_queue<ListNode*, vector<ListNode*>, decltype(compare)> minHeap(compare);
23      
24        // Add the head of each non-empty list to the min-heap
25        for (ListNode* head : lists) {
26            if (head != nullptr) {
27                minHeap.push(head);
28            }
29        }
30      
31        // Create a dummy head to simplify list construction
32        ListNode* dummyHead = new ListNode(0);
33        ListNode* current = dummyHead;
34      
35        // Process nodes from the min-heap until it's empty
36        while (!minHeap.empty()) {
37            // Extract the node with minimum value
38            ListNode* minNode = minHeap.top();
39            minHeap.pop();
40          
41            // If this node has a next node, add it to the heap
42            if (minNode->next != nullptr) {
43                minHeap.push(minNode->next);
44            }
45          
46            // Append the minimum node to the result list
47            current->next = minNode;
48            current = current->next;
49        }
50      
51        // Return the merged list (skip dummy head)
52        return dummyHead->next;
53    }
54};
55
1/**
2 * Definition for singly-linked list.
3 * class ListNode {
4 *     val: number
5 *     next: ListNode | null
6 *     constructor(val?: number, next?: ListNode | null) {
7 *         this.val = (val===undefined ? 0 : val)
8 *         this.next = (next===undefined ? null : next)
9 *     }
10 * }
11 */
12
13/**
14 * Merges k sorted linked lists into one sorted linked list
15 * @param lists - Array of sorted linked list heads
16 * @returns Head of the merged sorted linked list
17 */
18function mergeKLists(lists: Array<ListNode | null>): ListNode | null {
19    // Initialize a min-heap priority queue with nodes ordered by their values
20    const minHeap = new MinPriorityQueue({ 
21        priority: (node: ListNode) => node.val 
22    });
23  
24    // Add all non-null list heads to the priority queue
25    lists
26        .filter((head: ListNode | null): head is ListNode => head !== null)
27        .forEach((head: ListNode) => minHeap.enqueue(head));
28  
29    // Create a dummy node to simplify list construction
30    const dummyHead: ListNode = new ListNode();
31    let currentNode: ListNode = dummyHead;
32  
33    // Process nodes from the priority queue until empty
34    while (!minHeap.isEmpty()) {
35        // Extract the node with minimum value
36        const minNode: ListNode = minHeap.dequeue().element;
37      
38        // Append the minimum node to the result list
39        currentNode.next = minNode;
40        currentNode = currentNode.next;
41      
42        // If the extracted node has a next node, add it to the queue
43        if (minNode.next !== null) {
44            minHeap.enqueue(minNode.next);
45        }
46    }
47  
48    // Return the head of the merged list (skip dummy node)
49    return dummyHead.next;
50}
51

Time and Space Complexity

Time Complexity: O(n × log k)

The algorithm uses a min-heap to efficiently merge k sorted linked lists. Breaking down the operations:

  • Initially, we add at most k heads to the heap, which takes O(k) time for heapify operation
  • For each of the n total nodes across all lists:
    • We perform one heappop operation: O(log k)
    • We potentially perform one heappush operation: O(log k)
    • Other operations (pointer updates) are O(1)
  • Since we process all n nodes and each node requires O(log k) heap operations, the total time complexity is O(n × log k)

Space Complexity: O(k)

The space usage comes from:

  • The min-heap pq stores at most k nodes at any given time (one from each list)
  • The dummy node and cur pointer use O(1) additional space
  • The setattr operation modifies the class but doesn't add space proportional to input size
  • Therefore, the overall space complexity is O(k)

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Heap Comparison Error with Duplicate Values

When multiple nodes have the same value, Python's heapq might try to compare the entire ListNode objects beyond just their values, leading to a TypeError since ListNode objects don't have a natural ordering.

Problem Example:

# If two nodes have the same value, heapq might compare the objects themselves
# This happens when using tuples like (node.val, node) in the heap
priority_queue = [(node.val, node) for node in lists if node]
heapify(priority_queue)  # May raise TypeError if two nodes have same value

Solution: The provided code correctly handles this by adding the __lt__ method directly to the ListNode class. However, an alternative approach using a wrapper or index-based comparison is also valid:

# Alternative 1: Use unique index as tiebreaker
priority_queue = []
for i, head in enumerate(lists):
    if head:
        heappush(priority_queue, (head.val, i, head))

while priority_queue:
    val, idx, node = heappop(priority_queue)
    if node.next:
        heappush(priority_queue, (node.next.val, idx, node.next))
    # ... rest of logic

2. Memory Leak with Large Lists

The solution maintains references to all nodes through the heap and the result list simultaneously, which could cause memory issues with very large lists.

Problem:

# All nodes remain in memory until the entire process completes
cur.next = node  # Original list structure is preserved

Solution: If memory is critical, consider breaking the original list connections:

while priority_queue:
    node = heappop(priority_queue)
    next_node = node.next
    node.next = None  # Break original connection to allow garbage collection
    if next_node:
        heappush(priority_queue, next_node)
    cur.next = node
    cur = cur.next

3. Modifying Global ListNode Class

Using setattr to modify the ListNode class affects all instances globally, which could cause issues in environments where multiple solutions run concurrently or in test frameworks.

Problem:

setattr(ListNode, "__lt__", lambda self, other: self.val < other.val)
# This modification persists beyond this function call

Solution: Use a wrapper class or custom comparison function instead:

class HeapNode:
    def __init__(self, node):
        self.node = node
  
    def __lt__(self, other):
        return self.node.val < other.node.val

# Use HeapNode wrapper in heap
priority_queue = [HeapNode(head) for head in lists if head]
heapify(priority_queue)

while priority_queue:
    heap_node = heappop(priority_queue)
    node = heap_node.node
    if node.next:
        heappush(priority_queue, HeapNode(node.next))
    # ... rest of logic

4. Edge Case: Empty Lists Array

If all input lists are None or the lists array itself is empty, the code handles it correctly, but it's worth being explicit about this edge case.

Verification:

# These cases are handled correctly:
lists = []  # Returns None
lists = [None, None, None]  # Returns None

The current implementation handles these cases well due to the list comprehension filter [head for head in lists if head], but developers should be aware of these edge cases when testing.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

How many times is a tree node visited in a depth first search?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More