Find Middle Element of Linked List in DSA Rust - Time & Space Complexity
We want to understand how long it takes to find the middle element in a linked list as the list grows.
How does the time needed change when the list gets bigger?
Analyze the time complexity of the following code snippet.
struct Node {
value: i32,
next: Option<Box<Node>>,
}
fn find_middle(head: &Option<Box<Node>>) -> Option<i32> {
let mut slow = head.as_ref();
let mut fast = head.as_ref();
while let Some(f) = fast {
if f.next.is_none() {
break;
}
slow = slow.and_then(|s| s.next.as_ref());
fast = f.next.as_ref().and_then(|n| n.next.as_ref());
}
slow.map(|node| node.value)
}
This code uses two pointers moving at different speeds to find the middle node of a linked list.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: The while loop moves the fast pointer two steps and the slow pointer one step through the list.
- How many times: The loop runs about half the number of nodes in the list.
As the list size grows, the number of steps grows roughly half as fast because the fast pointer jumps two nodes at a time.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | 5 |
| 100 | 50 |
| 1000 | 500 |
Pattern observation: The operations grow linearly with the size of the list.
Time Complexity: O(n)
This means the time to find the middle grows directly with the number of nodes in the list.
[X] Wrong: "Since the fast pointer jumps two steps, the time complexity is O(n/2), which is faster than O(n)."
[OK] Correct: We ignore that constants are dropped in Big-O notation, so O(n/2) is still O(n). The time still grows linearly with input size.
Understanding this method shows you can use pointers smartly to solve problems efficiently, a skill valued in many coding challenges.
"What if we used only one pointer to traverse the list once? How would the time complexity change?"
Practice
Solution
Step 1: Understand two-pointer technique
The slow pointer moves one node at a time, while the fast pointer moves two nodes at a time.Step 2: Identify middle when fast pointer reaches the end
When the fast pointer reaches the end of the list, the slow pointer will be at the middle node.Final Answer:
Use one slow pointer moving one step and one fast pointer moving two steps until fast reaches the end -> Option BQuick Check:
Two-pointer technique = Use one slow pointer moving one step and one fast pointer moving two steps until fast reaches the end [OK]
- Trying to count nodes first wastes time
- Reversing list is unnecessary
- Using extra space like stack is inefficient
Solution
Step 1: Recall Rust struct initialization syntax
Rust structs are initialized with field names and values inside curly braces.Step 2: Match correct syntax for Node struct
Using Node { value: 5, next: None } matches Rust's struct initialization rules.Final Answer:
let node = Node { value: 5, next: None }; -> Option CQuick Check:
Rust struct init = let node = Node { value: 5, next: None }; [OK]
- Using parentheses instead of braces
- Omitting field names
- Trying to call a constructor that doesn't exist
struct Node {
value: i32,
next: Option,
}
fn find_middle(head: &Option) -> i32 {
let mut slow = head;
let mut fast = head;
while fast.is_some() && fast.as_ref().unwrap().next.is_some() {
slow = &slow.as_ref().unwrap().next;
fast = &fast.as_ref().unwrap().next.as_ref().unwrap().next;
}
slow.as_ref().unwrap().value
}
fn main() {
let node3 = Node { value: 3, next: None };
let node2 = Node { value: 2, next: Some(Box::new(node3)) };
let node1 = Node { value: 1, next: Some(Box::new(node2)) };
let head = Some(Box::new(node1));
println!("{}", find_middle(&head));
}Solution
Step 1: Trace slow and fast pointers through the list
Initially, slow and fast point to node with value 1. After first loop iteration, slow moves to node 2, fast moves to node 3.Step 2: Check loop condition and final slow pointer value
Fast's next is None after moving, so loop stops. Slow points to node with value 2, which is the middle.Final Answer:
2 -> Option AQuick Check:
Middle element = 2 [OK]
- Returning fast pointer value instead of slow
- Not checking fast.next before accessing
- Confusing Option and Box unwraps
fn find_middle(head: &Option) -> i32 {
let mut slow = head;
let mut fast = head;
while fast.is_some() && fast.as_ref().unwrap().next.is_some() {
slow = &slow.as_ref().unwrap().next;
fast = &fast.as_ref().unwrap().next;
}
slow.as_ref().unwrap().value
}Solution
Step 1: Analyze pointer movements inside the loop
Slow pointer moves one step correctly, but fast pointer moves only one step instead of two.Step 2: Understand impact on finding middle
Fast pointer must move two steps to reach end faster; moving one step breaks the logic.Final Answer:
fast pointer moves only one step instead of two -> Option AQuick Check:
Fast pointer must move two steps [OK]
- Moving fast pointer only one step
- Not checking fast.next before accessing
- Confusing slow and fast pointer updates
Solution
Step 1: Understand default behavior for even nodes
Default two-pointer returns first middle when list length is even.Step 2: Modify loop to continue while fast and fast.next exist
By continuing loop while fast and fast.next are Some, slow moves to second middle when fast reaches end.Final Answer:
Change the loop condition to continue while fast and fast.next are not None, moving fast two steps and slow one step -> Option DQuick Check:
Loop condition controls which middle is returned [OK]
- Swapping slow and fast pointer speeds
- Stopping loop too early
- Using counting instead of two-pointer method
Ready to prove it?
Pass the test and this pattern moves unknown
