Problem overview
The method mergeKLists receives an array lists of k singly linked lists, each sorted in ascending order, and returns one sorted linked list holding all their values. Entries in the array can be null, which stands for an empty list, and the array itself can be empty.
For lists = [[1,4,5],[1,3,4],[2,6]] the result is 1->1->2->3->4->4->5->6. For an empty array, or for an array whose only entry is an empty list, the result is an empty list, which means null.
The statement asks for O(n log k) time and O(k) extra space, where n is the total number of nodes and k the number of lists.
Approach: a min-heap of list fronts
At any moment, the next value in the merged list is the smallest among the current front nodes of the k lists. The simple method scans all k fronts for every output node, which costs O(k) per node and O(n * k) overall.
A min-heap does better. It is a structure that always gives you its smallest element quickly: taking the smallest or adding an element costs O(log k) for a heap of k elements. The plan:
- Put the head of every non-empty list into the heap, ordered by the node's value.
- Repeatedly take the smallest node out and append its value to the result.
- If the node that was taken has a successor, put the successor into the heap. This node is the new front of that list.
- Stop when the heap is empty. Every list has then been consumed.
The heap never holds more than one node per list, so its size is at most k. Each of the n output nodes costs one removal and at most one insertion, each O(log k), which gives O(n log k).
On the example, the heap starts with the nodes 1 (from the first list), 1 (second list) and 2. The first removal gives a 1, and its successor 4 enters the heap. The heap now holds 4, 1 and 2, so the next removal gives the other 1, and its successor 3 enters. The process continues until it has produced 1, 1, 2, 3, 4, 4, 5, 6.
Step by step
- Create the heap. A
PriorityQueue<ListNode>calledminNodes. It can't orderListNodeobjects by itself, so you give it a comparator that compares thevalfield. - Visit every list head with an enhanced
forloop overlists. - Add only non-null heads. A
nullhead means an empty list, and the heap can't holdnull. - Create a sentinel node
dummy. The merged list will start atdummy.next, which spares you a special case for the first node. - Create the walker
tail, set todummy. It always points at the last node appended. - Loop while the heap is not empty.
- Take the smallest node with
poll()and keep it inminNode. - Append a new node with the same value as
tail.next. The merged list is built from fresh nodes, so the input lists are left unchanged. - Feed the heap. If
minNode.nextis notnull, add it. Each list refills the heap one node at a time. - Move the walker with
tail = tail.next. - Return
dummy.next, the head of the merged list. If nothing was ever added, it isnull, the correct empty result.
The solution in Java
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
PriorityQueue<ListNode> minNodes = new PriorityQueue<>(Comparator.comparingInt(node -> node.val));
for (ListNode head : lists) {
if (head != null) {
minNodes.add(head);
}
}
ListNode dummy = new ListNode();
ListNode tail = dummy;
while (!minNodes.isEmpty()) {
ListNode minNode = minNodes.poll();
tail.next = new ListNode(minNode.val);
if (minNode.next != null) {
minNodes.add(minNode.next);
}
tail = tail.next;
}
return dummy.next;
}
}
The Java details worth a closer look:
PriorityQueueis a min-heap by default.poll()removes and returns the smallest element according to the ordering, and returnsnullif the queue is empty. The loop condition makes sure it is never empty at that point. It is theaddandpollcalls that costO(log k). Looking at the head withpeek()isO(1).- The comparator.
Comparator.comparingInt(node -> node.val)builds a comparator from a function that extracts anintkey. Java infers the type ofnodeasListNodefrom the queue's declared type. Writing(a, b) -> a.val - b.valalso works for values in the range of this lesson, but subtracting can overflow for large values of opposite sign, socomparingIntis the safer habit. - Without a comparator. A
PriorityQueueof objects that are notComparablethrows aClassCastExceptionwhen you add a second element, so the comparator is required. - Equal values are fine. Two nodes with the same
valare both stored in the heap. The order in which aPriorityQueuereturns equal elements is not specified, but equal values give the same output either way. - The sentinel pattern.
new ListNode()uses the no-argument constructor, sodummy.valis 0 and never read. Becausetailanddummyrefer to the same node at first,tail.next = ...also setsdummy.next.
Complexity
Let n be the total number of nodes and k the number of lists.
Time: O(n log k). Seeding the heap with up to k heads costs O(k log k). Then the loop runs n times. Each run does one poll and at most one add on a heap of at most k nodes, so each costs O(log k). Since every non-empty list contributes at least one node, k is at most n among non-empty lists, and the total stays O(n log k).
Space: O(k) extra. The heap holds at most one node per list. The merged list is the output and is not counted as extra space, though it does occupy O(n) memory.
Edge cases and common mistakes
- An empty array. The heap stays empty, the loop doesn't run, and
dummy.nextisnull. - Null entries.
[null]and[null, 1->2, null]work: null heads are skipped, so the heap never sees them. - Adding a null head.
PriorityQueue.add(null)throws aNullPointerException, which is why the check comes first. - Forgetting to push the successor. The result would hold only the first node of each list.
- Adding the whole list again. Push
minNode.next, the next node, notminNode, which would loop forever. - Returning
dummy. The sentinel is not part of the answer. Returndummy.next. - Duplicates.
[[1], [1]]gives1->1. Nothing is dropped.
Testing
[[1,4,5],[1,3,4],[2,6]]gives[1,1,2,3,4,4,5,6].[]gives[].[[]]gives[].[[], [-2, 7], []]gives[-2, 7].[[5], [1]]gives[1, 5].