Problem overview
The function merge_k_lists receives a vector of k linked lists, each sorted in ascending order. Each list is an Option<Box<ListNode>>: None for an empty list, or Some holding the first node. The function returns one sorted linked list containing every value from all of them. There can be no lists at all, or only empty ones, and then the answer is the empty list, None.
For the lists 1->4->5, 1->3->4 and 2->6, the merged list is 1->1->2->3->4->4->5->6. The values are between -10,000 and 10,000,000, and the lists hold at most 10,000 nodes in total.
The lesson's target is O(n × k) time, where n is the total number of nodes and k the number of lists. A faster O(n log k) version with a min-heap exists, and this lesson is the simpler scan.
Approach: always take the smallest head
Each input list is sorted, so its smallest unused value is its current first node, its head. The smallest value still waiting anywhere is therefore the smallest of the k heads. Take that one, append it to the output, advance that list to its next node, and repeat until every list is empty.
Finding the smallest head is a plain loop over the k lists. It is O(k) per output node, and there are n output nodes, which gives O(n × k).
To build the output without special-casing the first node, use a dummy node placed before the real head, plus a tail reference that points at the last node appended so far. Every append happens at tail.next. At the end, dummy.next is the head of the merged list.
On the example, the heads are 1, 1 and 2. The scan picks the first 1, because ties go to the earlier list. That list's head becomes 4, so the heads are now 4, 1 and 2, the next pick is the second list's 1, and so on until the heads run out.
Step by step
- Create the dummy.
Box::new(ListNode::new(0))is a node whose value is never used. Declare itmut, because itsnextwill change. - Create the walker
tailas&mut dummy, a mutable reference to the dummy. It will move down the output as nodes are appended. - Loop while any list has nodes. The condition calls a helper,
any_non_null, that the lesson writes at the end. - Start with no choice.
indis ani32set to-1, meaning "no list picked yet". It is signed so that -1 can be stored, and it is cast tousizewhen used as an index. - Scan all the lists with
for i in 0..lists.len(). - Update the best choice. A list is a candidate when it is
Some. It replaces the current choice when nothing has been picked yet, or when its head value is strictly smaller than the picked list's head. Strict comparison keeps the earlier list on ties. - Append a new node with the chosen head's value, at
tail.next. - Advance the chosen list.
take()removes the head node from the vector slot, leavingNone. The code then moves on to that node'snextand stores it back in the slot. - Move
tailonto the node just appended. - Return
dummy.nextas the last expression of the function. - Write the helper.
any_non_nullborrows the vector as a slice, loops over it, returnstrueat the firstSome, and returnsfalseafter the loop.
The solution in Rust
struct ListNode {
val: i32,
next: Option<Box<ListNode>>,
}
impl ListNode {
fn new(val: i32) -> Self {
ListNode { val, next: None }
}
}
impl Solution {
fn merge_k_lists(mut lists: Vec<Option<Box<ListNode>>>) -> Option<Box<ListNode>> {
let mut dummy = Box::new(ListNode::new(0));
let mut tail = &mut dummy;
while Self::any_non_null(&lists) {
let mut ind: i32 = -1;
for i in 0..lists.len() {
if lists[i].is_some() && (ind == -1 || lists[i].as_ref().unwrap().val < lists[ind as usize].as_ref().unwrap().val) {
ind = i as i32;
}
}
tail.next = Some(Box::new(ListNode::new(lists[ind as usize].as_ref().unwrap().val)));
lists[ind as usize] = lists[ind as usize].take().unwrap().next;
tail = tail.next.as_mut().unwrap();
}
dummy.next
}
fn any_non_null(lists: &[Option<Box<ListNode>>]) -> bool {
for l in lists {
if l.is_some() {
return true;
}
}
false
}
}
On the first example it produces [1, 1, 2, 3, 4, 4, 5, 6], on an empty vector of lists it produces an empty list, and on [None] it produces an empty list as well.
The Rust details worth a closer look:
- The list type.
Option<Box<ListNode>>is Rust's way of writing a nullable pointer.Noneends a list, andBoxgives each node a fixed-size handle to its heap allocation, which a recursive struct needs. mut lists. The vector is owned and modified: each step assigns a new head into one slot, so the parameter must be declaredmut.as_ref().unwrap(). It looks at a head without taking it.as_ref()turns&Option<Box<ListNode>>intoOption<&Box<ListNode>>, andunwrap()gets the reference out. Theis_some()check earlier in the same condition keeps the unwrap safe, since the&&short-circuits.take(). Writinglists[i].take()moves the value out and leavesNonebehind. That is how the code gets ownership of a node without breaking the vector. Without it, moving a node out of an indexed slot is a compile error.- The new nodes are copies of values. The merged list is built from fresh
ListNodevalues, and the input nodes are dropped as they are consumed. The code doesn't re-link the old nodes. - The reference
tail.&mut dummyborrows the dummy mutably, andtail = tail.next.as_mut().unwrap()re-points it to the node it just appended.dummy.nextis returned only after the loop, whentailis no longer used. - Casting the index.
ind as usizeis safe here becauseindis only read after the scan has set it to a valid position. With at least one non-empty list, the scan always does.
Complexity
Time: O(n × k). The outer loop runs once per output node, so n times. Each pass scans k lists to find the smallest head, and the helper's check costs up to k more. That is O(k) per node, giving O(n × k).
Space: O(1) beyond the output. The merged list holds one new node per value and is the required result. Apart from it, the function keeps the dummy, the walker and a few integers.
Edge cases and common mistakes
- No lists. An empty vector never enters the loop, so
dummy.nextisNone. - Only empty lists.
[None]or[None, None]returnsNonefor the same reason. - Some lists empty.
[None, 3->5, None]gives[3, 5]. Theis_some()test skips empty slots. - Equal heads. Lists
1->1and1give[1, 1, 1]. No value is lost or merged, because equal values are each appended in turn. - Using
<=instead of<. The result is still sorted, but the choice then goes to the later list on ties. - Reading a head before checking
is_some(). Unwrapping aNonepanics. Keep the check ahead of the comparison in the same condition. - Forgetting to advance the list. If the chosen slot isn't replaced with its
next, the same value is taken forever and the loop never ends.
Testing
[1->4->5, 1->3->4, 2->6]gives1->1->2->3->4->4->5->6.[]gives the empty list.[None]gives the empty list.[None, -3->5, None]gives-3->5.[1->1, 1]gives1->1->1.
