Merge Two Sorted Linked Lists in DSA Rust - Time & Space Complexity
We want to know how the time needed to merge two sorted linked lists changes as the lists get bigger.
Specifically, how does the merging process scale with the size of the input lists?
Analyze the time complexity of the following code snippet.
fn merge_two_lists(
mut list1: Option>,
mut list2: Option>
) -> Option> {
let mut dummy = Box::new(ListNode::new(0));
let mut tail = &mut dummy;
while list1.is_some() && list2.is_some() {
if list1.as_ref().unwrap().val <= list2.as_ref().unwrap().val {
tail.next = list1;
tail = tail.next.as_mut().unwrap();
list1 = tail.next.take();
} else {
tail.next = list2;
tail = tail.next.as_mut().unwrap();
list2 = tail.next.take();
}
}
tail.next = if list1.is_some() { list1 } else { list2 };
dummy.next
}
This code merges two sorted linked lists into one sorted linked list by repeatedly choosing the smaller head node.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: The while loop that compares nodes and moves pointers.
- How many times: It runs until all nodes from both lists are processed, roughly the total number of nodes combined.
As the total number of nodes increases, the number of comparisons and pointer moves grows roughly the same amount.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | About 10 comparisons and moves |
| 100 | About 100 comparisons and moves |
| 1000 | About 1000 comparisons and moves |
Pattern observation: The operations grow linearly with the total number of nodes.
Time Complexity: O(n + m)
This means the time to merge grows directly with the total number of nodes in both lists combined.
[X] Wrong: "Merging two lists takes quadratic time because of nested loops."
[OK] Correct: The code only uses one loop that moves through both lists once, so it does not repeat work inside another loop.
Understanding this merging process helps you handle linked list problems efficiently, a common skill interviewers look for.
"What if the input lists were arrays instead of linked lists? How would the time complexity change?"
Practice
Solution
Step 1: Understand the problem
Merging two sorted linked lists means combining them into one list that remains sorted.Step 2: Identify the goal
The goal is to create a new sorted linked list that contains all elements from both input lists.Final Answer:
Create a new sorted linked list containing all elements from both lists -> Option CQuick Check:
Merging = Combined sorted list [OK]
- Thinking merging means reversing lists
- Trying to sort lists separately without merging
- Confusing merging with removing duplicates
Solution
Step 1: Understand Rust linked list node
Rust needs heap allocation for recursive types, so next must be Option.Step 2: Check each option
struct ListNode { val: i32, next: Option } uses Option, which is correct. Others use references or direct types causing errors.Final Answer:
struct ListNode { val: i32, next: Option } -> Option BQuick Check:
Use Option for linked list nodes [OK]
- Using direct ListNode type for next causes infinite size error
- Using references without lifetimes causes borrow errors
- Using Option<ListNode> misses boxing needed for recursion
let l1: Option = None;
let l2 = Some(Box::new(ListNode { val: 0, next: Some(Box::new(ListNode { val: 2, next: Some(Box::new(ListNode { val: 4, next: None })) })) }));
let merged = merge_two_lists(l1, l2);Solution
Step 1: Check input lists
l1 is None (empty), l2 is 0 -> 2 -> 4 -> null.Step 2: Merge function returns non-empty list
When one list is None, function returns the other list as is.Final Answer:
0 -> 2 -> 4 -> null -> Option AQuick Check:
Empty + list = list unchanged [OK]
- Returning null when one list is empty
- Trying to merge empty list nodes incorrectly
- Losing nodes from non-empty list
fn merge_two_lists(l1: Option, l2: Option) -> Option {
match (l1, l2) {
(None, None) => None,
(Some(node), None) | (None, Some(node)) => Some(node),
(Some(mut n1), Some(mut n2)) => {
if n1.val < n2.val {
n1.next = merge_two_lists(n1.next.take(), Some(n2));
Some(n1)
} else {
n2.next = merge_two_lists(Some(n1), n2.next.take());
Some(n2)
}
}
}
}Solution
Step 1: Trace first comparison
Compare 1 and 2: 1 < 2, so 1 is first node.Step 2: Recursively merge remaining lists
Next merge 3->5 and 2->4->6. 2 < 3, so 2 next, then 3, 4, 5, 6.Final Answer:
1 -> 2 -> 3 -> 4 -> 5 -> 6 -> null -> Option AQuick Check:
Sorted merge = 1,2,3,4,5,6 [OK]
- Appending lists instead of merging sorted
- Not updating next pointers correctly
- Mixing order of nodes in merged list
fn merge_two_lists(l1: Option, l2: Option) -> Option {
match (l1, l2) {
(None, None) => None,
(Some(node), None) => Some(node),
(None, Some(node)) => Some(node),
(Some(mut n1), Some(mut n2)) => {
if n1.val <= n2.val {
n1.next = merge_two_lists(n1.next, Some(n2));
Some(n1)
} else {
n2.next = merge_two_lists(Some(n1), n2.next);
Some(n2)
}
}
}
}Solution
Step 1: Identify ownership issue
n1.next and n2.next are Options; assigning without take() causes borrowing errors.Step 2: Fix by using take()
Using n1.next.take() and n2.next.take() moves ownership out safely for recursion.Final Answer:
Use n1.next.take() and n2.next.take() to avoid borrowing errors -> Option DQuick Check:
Use take() to move Option ownership [OK]
- Not using take() causes borrow checker errors
- Changing comparison operator unnecessarily
- Using references incorrectly in pattern matching
Ready to prove it?
Pass the test and this pattern moves unknown
