Problem overview
The method mergeKLists receives an array lists of k singly linked lists, each sorted in ascending order. It must return one sorted linked list that holds every value from all of them. Some lists can be empty (null), and the array itself can be empty.
In the first example, the lists are 1->4->5, 1->3->4 and 2->6. Merged, they give 1->1->2->3->4->4->5->6. An empty array and an array holding a single empty list both give an empty list, which in Java is null.
The lesson asks for O(n × k) time, where n is the total number of nodes and k is the number of lists, and O(1) extra space apart from the list you return.
Approach: always take the smallest head
Every input list is already sorted, so the smallest value that hasn't been used yet is always at the front of one of the lists. Compare the current heads of all the lists, take the smallest, and repeat. After n rounds every value has been taken, in sorted order.
Finding the smallest head means looking at all k heads, which costs O(k). Doing that once for each of the n output nodes gives O(n × k). This is the plain, brute-force version of the idea. A min-heap makes each round O(log k) and has its own lesson. Another option is to merge the lists two at a time, which is also faster. Here the point is to get the pointer work right with the simplest possible search.
The output list is built with a dummy node: a throwaway node placed before the real head. Without it, appending the first node would be a special case, because there is no previous node to attach it to. With it, every append is the same statement, and the answer is dummy.next at the end.
Step by step
- Create the dummy node. The merged list will hang off
dummy.next. - Keep a
tailpointer to the last node appended. It starts on the dummy. - Loop while any list still has nodes. A helper,
anyNonNull, answers that, and the method defines it at the end. - Start each round with
ind = -1. This variable holds the index of the list with the smallest head, and -1 means no candidate yet. - Scan all
klists with a counted loop. This scan is theO(k)cost paid per output node. - Update the candidate. A list competes only if it isn't
null. It wins if no candidate exists yet or its head value is strictly smaller than the candidate's. - Append a new node with the winning value after
tail. - Advance the winning list by one node, so its next value becomes its head.
- Move
tailonto the node you just appended. - Return
dummy.nextonce the loop ends. - Write the helper. It is a private method that takes the array, and a for-each loop returns
trueat the first list that isn'tnull. - Return
falseafter that loop. Every entry wasnull, so the merge is done.
The solution in Java
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 boolean anyNonNull(ListNode[] lists) {
for (ListNode l : lists) {
if (l != null) {
return true;
}
}
return false;
}
}
Java details worth a closer look:
- Short-circuit
&&and||. The condition checkslists[i] != nullfirst, solists[i].valis only read when the list exists. Likewiseind == -1comes beforelists[ind].val, so the code never readslists[-1], which would throw anArrayIndexOutOfBoundsException. - The input array is changed.
lists[ind] = lists[ind].nextoverwrites array entries. The nodes themselves are never modified, because the result is built from newListNodeobjects, but the caller's array ends up full ofnulls. Callers who need the original heads afterwards should keep their own copies of them. dummyis built with the no-argument constructor, so itsvalis0. That value is never read, since the dummy is never returned.- Strict
<keeps equal values in list order. When two heads are equal, the earlier list keeps the candidate slot. For plain integers the output is the same either way. - An empty result is
null. If no list has nodes, the loop never runs anddummy.nextisnull.
Complexity
Time: O(n × k). The outer loop runs once per output node, so n times. Each round runs anyNonNull, which stops at the first non-empty list and costs at most O(k), and then scans k heads. Both are O(k), so a round costs O(k) and the total is O(n × k).
Space: O(1) extra. The merged list needs one new node per value, but that is the output. Besides it, the method keeps the dummy node, tail, ind and a loop counter.
Edge cases and common mistakes
- An empty array.
lists = []makesanyNonNullreturnfalseat once, and the method returnsnull. - Lists that are
null.[null]also gives an empty result, and[null, 2->..., null]works because the scan skipsnullentries. Comparinglists[i].valwithout the null check throws aNullPointerException. - Forgetting to advance the list. If
lists[ind]never moves forward, the same head is chosen forever and the loop never ends. - Not moving
tail. Withouttail = tail.next, every new node overwrites the previous one, and the result has a single node. - Returning
dummyinstead ofdummy.next. That adds a stray leading node with value 0. - Duplicate values across lists. Both 1s in the example appear in the output. A merge keeps duplicates.
Testing
[[1,4,5],[1,3,4],[2,6]]gives[1,1,2,3,4,4,5,6].[]gives[].[[]]gives[].[[], [2], [], [1,3]]gives[1,2,3].
