Problem overview
The method mergeKLists(lists) receives an array of k sorted linked lists. Each entry of lists is the head node of one list, or null when that list is empty. The method merges all of them into one sorted linked list and returns its head.
For lists = [[1,4,5],[1,3,4],[2,6]], the three lists are 1->4->5, 1->3->4 and 2->6, and the merged list is 1->1->2->3->4->4->5->6. For lists = [] and for lists = [[]] the answer is an empty list, which is null. There are up to 10,000 lists, each sorted in ascending order, and at most 10,000 nodes in total.
The lesson asks for O(n × k) time and O(1) extra space, where n is the total number of nodes and k is the number of lists. A faster solution with a min-heap has its own lesson.
Approach: always take the smallest head
Every input list is sorted, so the smallest value among all the remaining nodes is always the head of one of the lists. The merged list can be built one node at a time: look at the k heads, pick the smallest, move it to the output, and move that list on to its next node. Repeat until every list is empty.
Each output node costs one scan of the k heads, and there are n output nodes, which gives O(n × k). It is not the fastest approach, because a min-heap finds the smallest head in O(log k) instead of O(k). It is the simplest, because finding a minimum is a plain loop.
Here is the first example. The heads are 1, 1 and 2. The scan picks the first 1 (a tie keeps the earlier list), so the output starts 1 and the first list's head becomes 4. The heads are now 4, 1 and 2, the scan picks 1 from the second list, the output is 1->1, and so on, until the heads have all been used up.
Building the output needs a place to start. The first node of the result is special, because there is no previous node to attach it to. A dummy node avoids that case: the code attaches every node after a tail that begins at the dummy, and the real result is dummy.next.
Step by step
- Create a dummy node with
const dummy = new ListNode();. It is a placeholder at the front of the output, so the first real node needs no special handling. - Keep a
tailpointer withlet tail = dummy;. It always points at the last node of the output so far, and new nodes are attached after it. - Loop while some list still has nodes. The condition
lists.some(l => l !== null)is true as long as at least one entry is notnull.somestops at the first such entry. - Start each round with
let ind = -1;. It remembers which list holds the smallest head, and-1means "none chosen yet". - Scan all the lists with a counted
forloop fromi = 0whilei < lists.length. This is the brute-force step, and it is where the factorkcomes from. - Update the choice. If
lists[i]is not empty, and either nothing has been chosen yet or its head value is strictly smaller than the chosen one, setind = i. The testlists[i]first skips empty lists, which arenull. - Append a copy of the smallest node with
tail.next = new ListNode(lists[ind].val);. - Advance the chosen list with
lists[ind] = lists[ind].next;, so its next node becomes the head to compare in the next round. - Move
tailonto the node just appended withtail = tail.next;. - Return
dummy.nextafter the loop. It is the head of the merged list, ornullwhen no list had any nodes.
The solution in JavaScript
class Solution {
mergeKLists(lists) {
const dummy = new ListNode();
let tail = dummy;
while (lists.some(l => l !== null)) {
let ind = -1;
for (let i = 0; i < lists.length; i++) {
if (lists[i] && (ind === -1 || lists[i].val < lists[ind].val)) {
ind = i;
}
}
tail.next = new ListNode(lists[ind].val);
lists[ind] = lists[ind].next;
tail = tail.next;
}
return dummy.next;
}
}
Some JavaScript details worth a closer look:
ListNodecomes from the lesson. The constructor setsvalto 0 andnexttonullwhen they are not given.new ListNode()is therefore a node withval0 andnextequal tonull, andnew ListNode(5)hasval5.- Short-circuit
&&and||. Inlists[i] && (ind === -1 || ...), the right side runs only whenlists[i]is truthy. A node object is always truthy, even when itsvalis 0, andnullis falsy. Whenind === -1the comparisonlists[ind].valis never evaluated, so it doesn't read from index -1. - Strict
!==againstnull. The check expects empty lists to be exactlynull. A list that ends isnullbecause the constructor setsnexttonull. An entry that isundefinedwould passl !== nulland lead to an error onlists[ind].val. - The input array is modified. The line
lists[ind] = lists[ind].nextoverwrites the entries oflists, and at the end they are allnull. The nodes themselves are not changed, since the output is built from new nodes. A caller that wants to reuselistsafterwards needs to copy it first. - "O(1) extra space" excludes the output. The merged list has one new node per value, and that is the result being returned. Apart from it, the method keeps the dummy node and a few variables.
Complexity
Time: O(n × k). The method produces n output nodes, one per pass of the while loop. Each pass scans all k entries of lists, and the some call at the top of the loop looks at up to k entries too, so a pass costs O(k). That gives O(n × k) in total.
Space: O(1) extra. Not counting the nodes of the returned list, the method uses the dummy node, tail, ind and i.
Edge cases and common mistakes
- No lists. With
lists = [],somereturnsfalseat once, the loop never runs, and the method returnsdummy.next, which isnull. - Only empty lists. With
lists = [[]], the only entry isnull, so the result is againnull. - Empty lists mixed with others. For
[null, 0->0, null]the scan skips thenullentries and the result is0->0. Thelists[i] &&test is what stops the scan from reading.valofnull. - Ties. Using
<and not<=keeps the earlier list on equal heads. Both choices give a correct sorted result, since equal values are interchangeable. - Returning
dummy. The dummy node's value of 0 would appear at the front of the answer. The head of the merged list isdummy.next. - Forgetting to advance the chosen list. Without
lists[ind] = lists[ind].nextthe same node is picked forever and the loop never ends. - Forgetting to move
tail. Each new node would overwrite the previous one'snext, and the result would hold only the last node.
Testing
lists = [[1,4,5],[1,3,4],[2,6]]gives[1,1,2,3,4,4,5,6].lists = []gives an empty list.lists = [[]]gives an empty list.lists = [[], [0,0], []]gives[0,0].
