Problem overview
The method mergeKLists receives lists, a Python list holding k linked lists. Each linked list is sorted in ascending order and is given by its head node, or by None when it is empty. The method must return the head of one sorted linked list that contains every value from all of them.
With lists = [1->4->5, 1->3->4, 2->6] the merged list is 1->1->2->3->4->4->5->6. If lists is empty, or holds only empty lists such as [None], the answer is an empty list, so the method returns None.
The statement asks for O(n × k) time, where n is the total number of nodes and k is the number of lists. The extra space is O(1): the merged list is the output itself, and apart from it the code keeps only a few variables.
Approach: always take the smallest head
Each input list is already sorted, so its smallest unused value is always its head. The smallest unused value overall is therefore the smallest of the k heads. That gives a simple procedure: look at all the heads, take the smallest one, add it to the output, move that list forward by one node, and repeat until every list is empty.
Each output node costs one scan of the k heads, which is O(k). There are n output nodes, so the total is O(n × k). This is the simplest correct method. Merging two lists at a time, or keeping the heads in a min-heap, brings the cost down to O(n log k), but this lesson stays with the plain scan.
Two small tools make the code short. A dummy node sits before the real head of the output, so the first appended node needs no special case. A tail variable always points at the last node of the output, which is where the next node is attached.
Step by step
- Create the dummy node.
ListNode()uses the default value 0, which never appears in the answer because the result starts atdummy.next. - Point
tailat the dummy. Nothing has been appended yet, so the dummy is the last node of the output. - Loop while any list still has nodes.
any(lists)is true as soon as one entry is a node rather thanNone. - Reset
indto -1 at the top of each pass. It holds the index of the list with the smallest head, and -1 means no list has been chosen yet. - Scan every list with
for i in range(len(lists)). - Update the choice. A list can win only if it still has a head, and it wins if nothing has been chosen yet or its head value is strictly smaller than the current best. The strict comparison means that on a tie the earlier list keeps the win.
- Append a new node with the chosen value after
tail. - Advance the chosen list by replacing its entry with its
nextnode. - Move
tailforward onto the node just appended. - Return
dummy.nextonce the loop ends: the head of the merged list, orNoneif nothing was ever appended.
The solution in Python
class Solution:
def mergeKLists(self, lists: List[ListNode]) -> ListNode:
dummy = ListNode()
tail = dummy
while any(lists):
ind = -1
for i in range(len(lists)):
if lists[i] and (ind == -1 or lists[i].val < lists[ind].val):
ind = i
tail.next = ListNode(lists[ind].val)
lists[ind] = lists[ind].next
tail = tail.next
return dummy.next
The Python details worth a closer look:
- Truthiness of nodes. A
ListNodeobject is truthy, because the class defines neither__bool__nor__len__, andNoneis falsy. That is whyany(lists)andif lists[i]work as "is there a node here". It would break for a class that defined__len__returning 0, but a plain node doesn't. andshort-circuits. Inlists[i] and (ind == -1 or lists[i].val < lists[ind].val), the right side runs only whenlists[i]is a node. The innerorlikewise skipslists[ind].valwhenindis -1. Without that order, the code would read.valonNoneor onlists[-1].- Negative indices are valid in Python. If
indwere -1 whenlists[ind]was read, Python wouldn't raise an error. It would silently read the last list. The loop condition guarantees that at least one list has a node, so the scan always setsindto a real index before the append. - The input list is consumed.
lists[ind] = lists[ind].nextoverwrites entries of the caller's list. After the call,listsholds onlyNonevalues. The original nodes are not changed, since the method copies each value into a newListNode, so the input chains stay intact if you kept their heads elsewhere. - Local names.
tailanddummyrefer to the same object at first. Assigningtail = tail.nextrebinds only the nametail, whiledummystays at the front, which is what makesdummy.nextthe head at the end.
Complexity
Time: O(n × k). The outer loop runs once per output node, so n times. Each pass scans k entries. The call to any(lists) also looks at entries, but it stops at the first node it finds, and the scan costs O(k) anyway, so it doesn't change the bound. In the worst case any must pass over many None entries before it finds a node, which is still at most k checks per pass.
Space: O(1) extra. The result list has one new node per value, which is the output and not working memory. Besides it, the code keeps the dummy node and the variables tail, ind and i. Nothing grows with n or k.
Edge cases and common mistakes
- No lists.
lists = []makesany(lists)false right away, and the method returnsdummy.next, which isNone. - Only empty lists.
[None]and[None, None]behave the same way and returnNone. - Some lists empty.
[None, 1->2]works because the scan skipsNoneentries and later passes keep skipping them. - Duplicates and negative values.
[1->1, 1]keeps every copy of 1. Values below zero, such as -7 and -5, order correctly because the code compares values and never uses them as indices. - Non-strict comparison. Using
<=instead of<still produces a sorted list, but the tie goes to the later list. With<the earlier list wins, which keeps equal values from different lists in list order. - Forgetting to advance a list. If
lists[ind]isn't moved forward, the same node is chosen on every pass and the loop never ends. - Returning
dummy. That would put an extra leading node holding 0 into the answer. Returndummy.next.
Testing
[1->4->5, 1->3->4, 2->6]gives[1, 1, 2, 3, 4, 4, 5, 6].[]gives an empty list (None).[None]gives an empty list (None).[-5, None, -7->0]gives[-7, -5, 0].
