Problem overview
The function mergeKLists receives a vector lists of k singly linked lists, each sorted in ascending order. Each node is a ListNode with an int val and a ListNode* next. The function returns the head of one sorted list that contains all the values.
For lists = [[1,4,5],[1,3,4],[2,6]] the result is 1->1->2->3->4->4->5->6. The vector can be empty, which gives an empty list, and some of the lists can be empty too. In C++ an empty list is a null pointer.
The statement asks for O(n log k) time, where n is the total number of nodes, and O(k) extra working space for the algorithm itself.
Approach: always take the smallest head
At any moment, the smallest unmerged value is at the head of one of the k lists, because each list is sorted. The simplest method scans all k heads for the smallest, takes it, and repeats. That costs O(k) per output node, so O(n · k) in total.
A min-heap finds the smallest of k items in O(log k) instead. Put the head of each non-empty list into the heap. Then repeat: take the smallest node, append its value to the result, and push that node's successor, if there is one, into the heap. The heap never holds more than one node from each list, so it never has more than k entries. Every one of the n output nodes costs one removal and at most one insertion, each O(log k), which gives O(n log k).
Step by step
- Write a comparator. The lambda
cmptakes twoListNode*values and returnsa->val > b->val.std::priority_queueis a max-heap by default: itstop()is the element that is "largest" under the comparator. Reversing the comparison makes the smallest value come out on top. - Declare the heap. Its type is
priority_queue<ListNode*, vector<ListNode*>, decltype(cmp)>, constructed withcmp. Naming a custom comparator needs all three template arguments: the element type, the underlying container, and the comparator type. - Loop over the lists with a range-based
for. - Seed the heap. Push each head that is not null. Empty lists contribute nothing.
- Create a dummy node. It holds the place before the first real node, so appending the first value needs no special case. The merged list is
dummy->next. - Create a tail pointer that always points at the last node appended.
- Loop while the heap isn't empty. Each iteration produces one output node.
- Take the smallest. Read it with
top(), then remove it withpop(). Two calls are needed becausepop()returns nothing. - Append a new node with the same value as
tail->next. - Feed the heap. If the removed node has a successor, push it. That keeps one candidate from each unfinished list in the heap.
- Advance the tail to the node just added.
- Return
dummy->next, the head of the merged list.
The solution in C++
class Solution {
public:
ListNode* mergeKLists(vector<ListNode*>& lists) {
auto cmp = [](ListNode* a, ListNode* b) { return a->val > b->val; };
priority_queue<ListNode*, vector<ListNode*>, decltype(cmp)> minNodes(cmp);
for (auto* head : lists) {
if (head) {
minNodes.push(head);
}
}
ListNode* dummy = new ListNode();
ListNode* tail = dummy;
while (!minNodes.empty()) {
ListNode* minNode = minNodes.top();
minNodes.pop();
tail->next = new ListNode(minNode->val);
if (minNode->next) {
minNodes.push(minNode->next);
}
tail = tail->next;
}
return dummy->next;
}
};
The C++ details worth knowing:
- The heap stores pointers, not values. The comparator receives two pointers and looks inside them. Without the custom comparator, the queue would order the pointers by memory address, which says nothing about the values.
- Comparator direction. The usual mistake is to write
a->val < b->val. That gives the default max-heap behaviour and merges in descending order. decltype(cmp). A lambda has a unique compiler-generated type that can't be written by hand.decltypenames it, and the objectcmpis passed to the constructor so that the queue has a copy to use.auto* head. The element type isListNode*, soauto*deducesListNodeand keeps the pointer visible. The loop copies each pointer, which is cheap.- Testing a pointer.
if (head)is true for a non-null pointer, so it filters out empty lists.minNode->nextworks the same way. - New nodes are allocated. The result is built from copies of the values, so the input lists are left untouched. That costs
O(n)memory for the output. The statement'sO(k)counts the working space of the algorithm, the heap. Thedummynode is also never freed. In production code, reuse the input nodes by relinking them, and free or avoid the dummy.
Complexity
Time: O(n log k). The heap never holds more than k nodes, so each push and each pop costs O(log k). Each of the n nodes is pushed once and popped once.
Space: O(k) beyond the output. The heap stores at most one node pointer per list. The merged list itself takes O(n).
Edge cases and common mistakes
- No lists. With
lists = [], the heap stays empty, the loop doesn't run, anddummy->nextisnullptr: the empty list. - Only empty lists.
[[]], a vector holding one null pointer, gives the same empty result. - Pushing null heads. The comparator would dereference a null pointer, which crashes the program. That is why the seeding loop checks
if (head). - Duplicates and negatives. Equal values are fine, since the comparator uses strict
>, and negative values compare like any other integers. - Using
top()afterpop(). Read the node first. Afterpop()the heap's top is already the next node. - Forgetting to push the successor. The result would end after the first node of each list.
- Returning
dummy. That includes the placeholder node, which has value 0, at the front of the answer.
Testing
[[1,4,5],[1,3,4],[2,6]]gives1->1->2->3->4->4->5->6.[]gives an empty list.[[]]gives an empty list.[[], [-3, -3]]gives-3->-3.