Create a Circular Singly Linked List in DSA Rust - Time & Space Complexity
We want to understand how the time needed to build a circular singly linked list changes as we add more items.
How does the work grow when the list gets bigger?
Analyze the time complexity of the following code snippet.
struct Node {
value: i32,
next: Option>,
}
fn create_circular_list(values: &[i32]) -> Option> {
let mut head = None;
let mut tail = None;
for &val in values {
let new_node = Box::new(Node { value: val, next: None });
if head.is_none() {
head = Some(new_node);
tail = head.as_mut();
} else {
tail.as_mut().unwrap().next = Some(new_node);
tail = tail.as_mut().unwrap().next.as_mut();
}
}
if let Some(t) = tail {
t.next = head.clone();
}
head
}
This code creates a circular singly linked list by linking the last node back to the first.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: Looping through each value in the input array to create nodes.
- How many times: Exactly once for each element in the input list.
As the number of values increases, the number of steps grows directly with it.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | About 10 steps to create nodes and link them |
| 100 | About 100 steps |
| 1000 | About 1000 steps |
Pattern observation: The work grows in a straight line with the input size.
Time Complexity: O(n)
This means the time to create the list grows directly with the number of items you add.
[X] Wrong: "Linking the last node back to the head takes extra time for each node, so it makes the whole process slower than linear."
[OK] Correct: The linking back to the head happens only once after all nodes are created, so it adds just one step, not many.
Understanding how to build circular linked lists efficiently shows you can manage pointers and loops well, a key skill in many coding tasks.
"What if we changed the code to insert each new node at the head instead of the tail? How would the time complexity change?"
Practice
Solution
Step 1: Regular linked list
In a regular singly linked list, the last node's next is None - marking the end.Step 2: Circular difference
In a circular singly linked list, the last node's next points back to the head. There is no None terminator - traversal wraps around.Final Answer:
Last node points back to head. -> Option BQuick Check:
Circular = last.next ā head (not None). [OK]
- Confusing with doubly linked list (two pointers per node)
- Thinking circular means reverse storage
- Confusing circular with bidirectional
Solution
Step 1: With tail pointer
tail.next = new_node (new last). new_node.next = head (circular link). tail = new_node. All O(1) pointer updates.Step 2: Without tail pointer
Would need O(n) traversal to find the last node.Final Answer:
O(1) with tail pointer maintained. -> Option CQuick Check:
Tail pointer = O(1) insert at end. Without tail = O(n). [OK]
- Saying O(n) because traversal is needed without tail pointer
- Not knowing tail pointer eliminates the traversal
- Confusing with insertion at arbitrary position which is O(n)
Solution
Step 1: Box ownership
Box<T> gives unique ownership of heap data. The last node would need to own the head node.Step 2: Circular ownership cycle
But the head is also owned by the list itself. Two owners of the same node = illegal in Rust. Box cannot express shared or circular ownership.Step 3: Solution
Use Rc for shared ownership, or raw pointers with unsafe, or index-based representation with Vec.Final Answer:
Box implies unique ownership - circular structure needs shared ownership (Rc) or unsafe raw pointers. -> Option BQuick Check:
Circular = shared ownership. Box = unique ownership. Mismatch. [OK]
- Thinking Box can handle any heap structure
- Not knowing Rust's ownership applies to data structure design
- Confusing Box limitations with general Rust limitations
Solution
Step 1: No None terminator
Circular lists have no None - checking next is None never terminates traversal.Step 2: Termination condition
Start at head. Advance. Stop when current.next == head (back at start). This means we've visited all nodes exactly once.Final Answer:
Stop when current.next points to head. -> Option BQuick Check:
Circular traversal: stop when next == head, not when next is None. [OK]
- Checking for None which never occurs in circular lists
- Using a fixed count which breaks for unknown lengths
- Stopping at tail instead of checking if next == head
Solution
Step 1: Current state
tail.next ā head (circular link). new_node will become the new head.Step 2: Required updates
new_node.next = old_head. tail.next = new_node (maintains circular link to new head). Update head = new_node.Step 3: Critical update
tail.next must point to new_node - otherwise the circular link points to the old head, breaking the list.Final Answer:
Update tail.next to new_node to maintain the circular link. -> Option BQuick Check:
Insert at front: new.next=old_head, tail.next=new (critical!), head=new. [OK]
- Forgetting to update tail.next, breaking the circular structure
- Setting tail to new_node (tail should stay at end)
- Setting new_node.next to None instead of old head
Ready to prove it?
Pass the test and this pattern moves unknown
