Merge k Sorted Lists

Hard Heaps

Problem

Given an array of k linked-lists lists, each linked-list is sorted in ascending order, merge all the linked-lists into one sorted linked-list and return it.

  • k == lists.length
  • 0 ≤ k ≤ 10⁴
  • 0 ≤ lists[i].length ≤ 500
  • −10⁴ ≤ lists[i][j] ≤ 10⁴
  • lists[i] is sorted in ascending order.
  • The sum of lists[i].length won't exceed 10⁴.

Example

Input: lists = [[1,4,5],[1,3,4],[2,6]]
Output: [1,1,2,3,4,4,5,6]

The brute-force approach concatenates all lists and sorts them, which is inefficient. Instead, the algorithm uses a min-heap to efficiently track the smallest current node among all lists. Initially, the heap contains the first node of each list. The algorithm repeatedly extracts the smallest node from the heap, appends it to the merged list, and if the extracted node has a next node, it pushes that next node into the heap. This process continues until the heap is empty, resulting in a fully merged sorted list.

Approach

Straightforward Solution

A naive approach concatenates all nodes into one list and sorts it, resulting in O(N log N) time where N is the total number of nodes. This is inefficient for large inputs.

Core Observation

Merging k sorted lists requires repeatedly selecting the smallest current element among the heads of all lists. This is a classic problem of efficiently merging multiple sorted sequences.

Path to Optimal

The key insight is to use a min-heap (priority queue) to keep track of the smallest current node among the k lists. By pushing the first node of each list into the heap, the algorithm can extract the minimum node in O(log k) time and then push the next node from that list. This reduces the complexity to O(N log k), where N is the total number of nodes.

Optimal Approach

Use a min-heap to store the current nodes of each list. Initialize the heap with the first node of each non-empty list. Repeatedly pop the smallest node from the heap, append it to the merged list, and if the popped node has a next node, push it into the heap. Continue until the heap is empty. This approach efficiently merges all lists in O(N log k) time.

Time

O(N log k)

Each of the N nodes is pushed and popped from the heap at most once. Each heap operation takes O(log k) time because the heap size is at most k.

Space

O(k)

The heap stores at most one node from each of the k lists at any time, resulting in O(k) auxiliary space.

Pattern Spotlight

Heaps (Priority Queue for Merging Sorted Sequences)

When merging multiple sorted sequences, maintain a min-heap of the current smallest elements from each sequence to efficiently extract the global minimum and advance pointers, reducing the problem from O(N log N) to O(N log k).

Solution

1import heapq
2
3class Solution:
4 def mergeKLists(self, lists: list[ListNode]) -> ListNode:
5 heap = []
6
7 for source, head in enumerate(lists):
8 if head is not None:
9 heapq.heappush(heap, (head.val, source, head))
10
11 dummy = ListNode()
12 tail = dummy
13
14 while heap:
15 _, source, node = heapq.heappop(heap)
16 next_node = node.next
17
18 tail.next = node
19 tail = node
20 tail.next = None
21
22 if next_node is not None:
23 heapq.heappush(heap, (next_node.val, source, next_node))
24
25 return dummy.next

Step-by-Step Solution

1

Initialize Min-Heap with the First Node of Each List

1import heapq
5 heap = []
7 for source, head in enumerate(lists):
8 if head is not None:
9 heapq.heappush(heap, (head.val, source, head))
11 dummy = ListNode()
12 tail = dummy

Objective

To prepare a min-heap containing the first node of each non-empty linked list for efficient minimum extraction.

Key Insight

By pushing the first node of each list into the heap, the algorithm sets up a data structure that always contains the smallest current candidates from all lists. This enables efficient retrieval of the next smallest node to append to the merged list. The heap stores tuples of (node value, source list index, node) to handle nodes with equal values and maintain stable ordering.

Interview Quick-Check

Core Logic

The heap stores tuples with node value and source index to ensure correct ordering and to distinguish nodes with identical values.

Common Pitfalls & Bugs

Forgetting to check if a list is empty before pushing its head into the heap can cause errors.

State & Boundaries

Only non-null heads are pushed into the heap to avoid invalid entries.

2

Build the Merged List by Extracting Minimum Nodes and Pushing Next Nodes

14while heap:
15 _, source, node = heapq.heappop(heap)
16 next_node = node.next
18 tail.next = node
19 tail = node
20 tail.next = None
22 if next_node is not None:
23 heapq.heappush(heap, (next_node.val, source, next_node))

Objective

To iteratively extract the smallest node from the heap, append it to the merged list, and push the next node from the same list into the heap if it exists.

Key Insight

The heap ensures that the smallest node among all current heads is always accessible in O(1) time (pop in O(log k)). After appending this node to the merged list, pushing its next node maintains the heap's invariant. This process continues until all nodes are merged, guaranteeing a sorted merged list. Setting tail.next to None after appending prevents accidental cycles.

Interview Quick-Check

Core Logic

Extracting the smallest node and pushing its successor maintains the heap invariant and ensures the merged list is sorted.

State & Boundaries

The loop continues until the heap is empty, meaning all nodes have been processed.

Common Pitfalls & Bugs

Forgetting to set tail.next to None can cause cycles or incorrect list termination.

3

Return the Head of the Fully Merged Sorted List

25return dummy.next

Objective

To return the merged linked list starting from the node following the dummy head.

Key Insight

Using a dummy head simplifies list construction by avoiding special cases for the first node. Returning dummy.next gives the actual head of the merged list, which contains all nodes in sorted order.

Interview Quick-Check

Core Logic

Returning dummy.next skips the placeholder node and returns the true head of the merged list.

Line Analysis

This solution has 3 Critical lines interviewers watch for.

Line 9 Critical
heapq.heappush(heap, (head.val, source, head))

Push the tuple (node value, source index, node) into the heap.

Storing the node value as the first element ensures the heap orders nodes by their values. Including the source index breaks ties when values are equal, preventing comparison errors between ListNode objects.

Line 15 Critical
_, source, node = heapq.heappop(heap)

Pop the smallest node tuple from the heap.

Extracting the minimum node maintains the sorted order of the merged list by always selecting the next smallest element.

Line 22 Critical
if next_node is not None:

If the next node exists, push it into the heap.

Pushing the successor node maintains the heap invariant and ensures all nodes from each list are eventually merged.

Line 5 Core
heap = []

Initialize an empty min-heap to store nodes.

The min-heap is the core data structure enabling efficient retrieval of the smallest node among all current list heads, which is essential for merging k sorted lists optimally.

Line 18 Core
tail.next = node

Append the extracted node to the merged list.

Linking the smallest node to the merged list builds the final sorted list incrementally.

Line 20 Core
tail.next = None

Set tail.next to None to terminate the list.

Explicitly terminating the list prevents accidental cycles or dangling pointers, ensuring the merged list is well-formed.

Line 23 Core
heapq.heappush(heap, (next_node.val, source, next_node))

Push the tuple (next node value, source index, next node) into the heap.

Including the source index and node value maintains heap ordering and tie-breaking, enabling correct and efficient merging.

Line 1 Core
import heapq

Import the min-heap operations used by the solution.

heapq provides the push and pop operations needed to maintain the smallest current list head efficiently.

Line 8 Core
if head is not None:

Check if the current list head is not None.

Only non-empty lists contribute nodes to the heap; this check prevents pushing invalid None entries that would cause errors.

Line 11 Core
dummy = ListNode()

Create a dummy head for the merged list.

The dummy head removes the need to special-case the first node appended to the result.

Line 12 Core
tail = dummy

Initialize tail at the dummy head.

Tail tracks the end of the merged list so each selected node can be appended in constant time.

Line 16 Core
next_node = node.next

Store the next node of the extracted node.

Tracking the next node allows the algorithm to continue merging nodes from the same source list by pushing successors into the heap.

Line 19 Core
tail = node

Advance the tail pointer to the newly appended node.

Updating the tail pointer maintains the end of the merged list for subsequent appends.

Line 25 Core
return dummy.next

Return the head of the merged linked list.

Returning dummy.next skips the placeholder node and provides the caller with the fully merged sorted list.

Line 14 Standard
while heap:

Continue looping while the heap is not empty.

The loop processes all nodes by repeatedly extracting the smallest node until no nodes remain, ensuring complete merging.

Line 7 Standard
for source, head in enumerate(lists):

Iterate over each list with its index.

Enumerating lists with their indices allows tracking the source list of each node, which is necessary to push the next node from the correct list into the heap.

Test Your Understanding

Why does using a min-heap reduce the time complexity from O(N log N) to O(N log k)?

Because the heap size is limited to k (the number of lists), each insertion and extraction takes O(log k) time instead of O(log N), and since each of the N nodes is processed once, the total time is O(N log k).

Related Problems

Heaps pattern

Don't just read it. Drill it.

Reconstruct Merge k Sorted Lists from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.

Unlock the Merge k Sorted Lists drill

or drill a free problem