Problem overview
The method mergeKLists receives an array lists of k linked lists, each sorted in ascending order. Some of the entries can be empty, so the array has type [ListNode?]. The method returns one sorted list that contains every value from all the lists, or nil if there are none.
In the first example, [1, 4, 5], [1, 3, 4] and [2, 6] merge into [1, 1, 2, 3, 4, 4, 5, 6]. An empty array gives an empty list, and an array holding one empty list gives an empty list too.
The statement asks for O(n log k) time and O(k) extra space, where n is the total number of nodes and k is the number of lists.
Approach: always take the smallest head
Each list is sorted, so the smallest value that is not yet in the result is always at the head of one of the lists. The merged list can therefore be built by repeating one step: find the smallest head among the k lists, append it, and move that list forward.
Scanning all k heads each time costs O(k) per output node, which gives O(n * k) overall. A min-heap does the same job faster. It is a collection that always gives back its smallest element, and both inserting and removing the smallest take O(log k). The heap here never holds more than one node per list, so it never has more than k entries.
The process looks like this on the first example. The heap starts with the heads 1, 1 and 2. Remove a 1 and append it; that list's next node, 4, joins the heap. The smallest is now the other 1, whose successor 3 joins. Then 2 comes out and its successor 6 joins, and so on, until the heap is empty and the result is complete.
Step by step
- Create the heap. Swift has no built-in priority queue, so the lesson provides a
MinHeapvalue type that ordersListNodeobjects by theirval. Call itminNodes. It is avar, because pushing and popping are mutating methods. - Visit every list head with
for head in lists. - Push the non-empty ones.
if let head = headunwraps the optional, so an empty list is skipped, and the heap starts with the heads of all non-empty lists. - Create a dummy node
dummy. The merged list will bedummy.next, which avoids a special case for the first node. - Keep a walker
var tail = dummythat points at the last appended node. - Loop while the heap has entries, that is, while
minNodes.count > 0. Each iteration produces one node of the output and costsO(log k). - Pop the smallest node into
minNode. - Append a copy of its value:
tail.next = ListNode(minNode.val). - Feed the heap. If
minNodehas a successor, unwrap it withif let next = minNode.nextand push it. A list keeps supplying nodes until it is used up. - Advance the walker with
tail = tail.next!. The value was assigned one statement earlier, so the force-unwrap is safe. - Return
dummy.next, the head of the merged list.
The solution in Swift
class Solution {
func mergeKLists(_ lists: [ListNode?]) -> ListNode? {
var minNodes = MinHeap()
for head in lists {
if let head = head {
minNodes.push(head)
}
}
let dummy = ListNode()
var tail = dummy
while minNodes.count > 0 {
let minNode = minNodes.pop()
tail.next = ListNode(minNode.val)
if let next = minNode.next {
minNodes.push(next)
}
tail = tail.next!
}
return dummy.next
}
}
Swift details worth knowing:
if let head = head. This unwraps an optional and names the result with the same name. Inside the braces,headis a plainListNode, so it can be pushed without!. The loop variableheadis an optional, and the unwrapped one shadows it.- Value type, mutating methods.
MinHeapis a struct, so the constant formlet minNodeswould not allowpushorpop. That's why it is avar. - New nodes, untouched inputs. The solution builds
ListNode(minNode.val)instead of relinking the original nodes, so the input lists are left exactly as they were. The cost isnnew nodes, which the output needs anyway; the extra working memory is just the heap. - The list type is a class. The heap stores references to the lists' own nodes. Reading
minNode.nextafter the pop still reaches the rest of that list. minNodes.count > 0. Swift requires aBoolcondition, so the check is explicit. An integer alone would not compile as a condition.
Complexity
Time: O(n log k). Pushing the first heads costs at most k pushes of O(log k) each. Each of the n output nodes needs one pop and at most one push, each O(log k), because the heap never holds more than k nodes. That is O(n log k) in total.
Space: O(k) extra. The heap holds at most one node per list. The merged list itself is the output.
Edge cases and common mistakes
- No lists.
[]leaves the heap empty, the loop never runs, anddummy.nextisnil. - Only empty lists. An array like
[nil]behaves the same way, because empty heads are never pushed. - Some lists empty.
[nil, [2], nil, [-1, 3]]gives[-1, 2, 3]. Skipping thenilheads is whatif letis for. - Duplicates. Equal values are fine:
[1, 4, 5]and[1, 3, 4]produce both 1s and both 4s. - Forgetting the successor. If the popped node's
nextisn't pushed, the result would hold only the first node of each list. - Pushing every node at the start. That would work but would make the heap size
ninstead ofk, so the time isO(n log n)and the spaceO(n). - Returning
dummy. The dummy holds a meaningless 0 and would appear as the first value.
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].[[1, 2], [3]]gives[1, 2, 3], and the two input lists still read[1, 2]and[3]afterwards.
