Problem overview
The method MergeKLists receives an array of k linked lists, ListNode[] lists. Each list is sorted in ascending order. It returns one sorted linked list holding all the values. The lesson's ListNode has an int val, a ListNode next and constructors that take no argument or a value.
For [[1, 4, 5], [1, 3, 4], [2, 6]] the result is 1 -> 1 -> 2 -> 3 -> 4 -> 4 -> 5 -> 6. For [] (no lists at all) and for [[]] (one empty list) the result is the empty list, which is null. The array can hold up to 10,000 lists, with at most 10,000 nodes in total.
Approach: always take the smallest head
Each input list is sorted, so its smallest unused value is its first node, its head. The smallest unused value overall is therefore the smallest of the k heads. That gives a simple procedure: repeat the following until every list is empty.
- Scan the heads of all lists and find the smallest.
- Append its value to the output.
- Advance that list to its next node.
With n values in total, the output grows n times, and each time the scan looks at up to k heads: O(n × k) time. The lesson's note asks for exactly this, and says a faster O(n log k) solution with a min-heap has its own lesson.
Two details make the code clean. A dummy node placed before the real head removes the special case of appending the very first node. A tail variable always points at the last node appended, so appending is tail.next = ....
On the first example, the heads are 1, 1 and 2. The first list wins the tie, since only a strictly smaller head replaces the current best, so its 1 is appended and its head becomes 4. Next the heads are 4, 1 and 2, the second list's 1 is appended, and so on, until all lists have run out.
Step by step
- Create the sentinel
ListNode dummy = new ListNode();. The merged list isdummy.next, and the sentinel's own value is never used. - Create the walker
ListNode tail = dummy;. - Loop while any list still has nodes. The loop condition calls a helper,
AnyNonNull(lists), written at the end. - Start each round with
int ind = -1;. It remembers which list has the smallest head. The value -1 means that none has been picked yet. - Scan all lists with a
forloop from0tolists.Length - 1. - Update the best choice. If
lists[i]is notnulland either nothing has been picked yet or its head is strictly smaller than the picked list's head, setind = i. Thenulltest skips exhausted lists, and it comes first so the.valreads never touch anull. - Append a new node carrying the smallest value:
tail.next = new ListNode(lists[ind].val);. - Advance the chosen list:
lists[ind] = lists[ind].next;. A list that runs out becomesnull. - Move
tailforward onto the node just appended. - Return
dummy.next, the head of the merged list. - Write the helper
AnyNonNullbelowMergeKLists. It takes the array of lists and answers whether any list still has nodes. - Loop over the array with
foreach (ListNode l in lists). - Return
trueat the first non-nulllist. One live list is enough to continue. - Return
falseafter the loop. Every entry wasnull, so the merge is done.
The solution in C#
class Solution {
public ListNode MergeKLists(ListNode[] lists) {
ListNode dummy = new ListNode();
ListNode tail = dummy;
while (AnyNonNull(lists)) {
int ind = -1;
for (int i = 0; i < lists.Length; i++) {
if (lists[i] != null && (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;
}
private bool AnyNonNull(ListNode[] lists) {
foreach (ListNode l in lists) {
if (l != null) {
return true;
}
}
return false;
}
}
The C# details worth knowing:
- Short-circuit evaluation. In
lists[i] != null && (...), the right side runs only when the left side istrue. That is what protectslists[i].valfrom anull. Inside the parentheses,ind == -1 || ...works the same way: whenindis -1 the comparison withlists[ind]is skipped, sincelists[-1]would throw anIndexOutOfRangeException. - Linked list nodes are references.
ListNodeis a class, solists[ind] = lists[ind].nextonly moves a reference. The input nodes are never changed. - The method does change the
listsarray. Its entries are advanced as the merge goes, and when the method returns, they are allnull. A caller who needs the array afterwards must keep its own copy of the heads. - The result is made of new nodes.
new ListNode(lists[ind].val)copies each value. The merged list shares no node with the inputs, so changing it doesn't affect them. - Empty input.
AnyNonNullisfalseat once, the loop never runs, anddummy.nextis stillnull. That is the right answer for[]and for[[]], where the only entry isnull. privatehelper. The tests call onlyMergeKLists, so the helper can beprivate. Both methods belong to the same class and can call each other.
Complexity
Let n be the total number of nodes and k the number of lists.
Time: O(n × k). The while loop runs once per node, so n times. Each round scans k heads, and AnyNonNull looks at up to k entries, so a round costs O(k). With the largest allowed inputs, this is on the order of 100 million simple steps.
Space: O(1) extra. Besides the returned list, which has one new node per value, the method keeps the dummy node and a few variables.
Edge cases and common mistakes
- No lists.
[]returnsnull. - Only empty lists.
[[]]returnsnull, and so does an array of several empty lists. The same goes for entries that arenull. - Some lists empty.
[[], [7]]returns the one-node list7. Thenulltest skips the empty list in every scan. - Equal heads. With
[1, 4, 5]and[1, 3, 4], either 1 can go first, and the result is the same. Because the comparison is strictly less than, the earlier list wins ties. - Negative values.
[[-5, -5], [-5]]returns-5 -> -5 -> -5. The comparison needs no special case. - Testing
valbeforenull. Readinglists[i].valon an exhausted list throwsNullReferenceException. - Forgetting to advance the chosen list. The same node would be picked forever, and the loop never ends.
- Returning
dummyinstead ofdummy.next. The sentinel would appear as an extra 0 at the front of the list.
Testing
[[1, 4, 5], [1, 3, 4], [2, 6]]gives[1, 1, 2, 3, 4, 4, 5, 6].[]gives[].[[]]gives[].[[], [7]]gives[7].[[-5, -5], [], [-5]]gives[-5, -5, -5].
