Swapping Nodes in Pairs: A Rust Adventure with Linked Lists
I am a nerdy software engineer
Linked list problems are a rite of passage for any programmer. They're fundamental, test your understanding of pointers , and often unveil elegant solutions. Today, we're tackling a classic: Swap Nodes in Pairs.
The problem statement is simple: given a linked list, swap every two adjacent nodes and return its head. The crucial constraint? You must solve the problem without modifying the values in the list's nodes (i.e., only nodes themselves may be changed).
This constraint is where Rust's powerful ownership and borrowing system truly shines (and sometimes frustrates!).
The Problem: Swap Nodes in Pairs
Link to problem : https://leetcode.com/problems/swap-nodes-in-pairs
Example:
Input: head = [1,2,3,4]
Output: [2,1,4,3]
Let's dive into how we can approach this in Rust.
Approach 1: The Elegant Recursive Solution
Recursion often provides a very clean and intuitive way to solve linked list problems. For "Swap Nodes in Pairs," it's particularly elegant in Rust.
The Logic:
- Base Case: If the list is empty or has only one node, there's nothing to swap. Return the head as is.
- Recursive Step:
- Take the first node.
- If there's a second node:
- Recursively call
swap_pairson the rest of the list (starting from what was originally the third node). This will return the correctly swapped tail. - Link the original first node to this newly swapped tail.
- Link the original second node to the original first node.
- Return the original second node (which is now the head of the swapped pair).
Rust Implementation (Recursive):
impl Solution {
pub fn swap_pairs(head: Option<Box<ListNode>>) -> Option<Box<ListNode>> {
// Use and_then to handle the Option gracefully
head.and_then(|mut first| { // 'first' is the current head node
match first.next.take() { // Try to take the next node (becomes 'second')
// Case 1: We have a 'second' node, so we can swap
Some(mut second) => {
// Recursively swap the rest of the list (starting from third node)
// first.next is currently None because we took it, so we can reassign
first.next = Self::swap_pairs(second.next.take());
// The 'first' node now points to the swapped remainder.
// Now, make 'second' point to 'first'.
second.next = Some(first);
// 'second' is now the new head of this swapped pair.
Some(second)
}
// Case 2: No 'second' node (list is empty or only one node left)
None => Some(first), // Return the single node as is
}
})
}
}
Why it works in Rust:
head.and_then(...): This is a clean way to start. IfheadisNone,and_thenreturnsNone. If it'sSome(node), it unwrapsnodefor us.first.next.take(): This is the magic!take()moves the value out of theBox<ListNode>and leavesNonein its place. This allows us to reassignfirst.nextlater without violating Rust's ownership rules. We effectively extractsecondto manipulate it.- Ownership Transfer: The
Boxes are moved and reassigned. There are no raw pointers to manage, and Rust ensures everything is dropped correctly.
Approach 2: The Iterative Solution with a Dummy Node
While recursion is elegant, for very long lists, an iterative solution might be preferred to avoid potential stack overflow issues. The iterative approach requires a bit more careful pointer management.
The Logic:
- Dummy Node: Introduce a
dummyhead node. This simplifies edge cases where the original head might be swapped.dummy.nextwill eventually point to our actual new head. currPointer: Maintain acurrmutable reference that points to the node before the pair we are about to swap. Initially,currpoints to thedummynode.- Loop and Swap:
- Check if there are at least two nodes ahead of
currto swap. - Identify
firstandsecondnodes of the pair. - Perform the actual pointer reassignments to swap
firstandsecond. - Advance
currtwo steps forward to prepare for the next pair.
Rust Implementation (Iterative):
impl Solution {
pub fn swap_pairs(head: Option<Box<ListNode>>) -> Option<Box<ListNode>> {
// 1. Create a dummy node to simplify head manipulation
// It points to the original head.
let mut dummy = Box::new(ListNode { val: 0, next: head });
// 'curr' is a mutable reference to the node *before* the pair we want to swap.
// Initially, it's the dummy node.
let mut curr = &mut dummy;
// Loop as long as there are at least two nodes ahead to swap
while curr.next.is_some() && curr.next.as_ref().unwrap().next.is_some() {
// 2. Take the 'first' node out of 'curr.next'
// curr.next becomes None temporarily
let mut first = curr.next.take().unwrap();
// 3. Take the 'second' node out of 'first.next'
// first.next becomes None temporarily
let mut second = first.next.take().unwrap();
// --- Perform the actual swapping of pointers ---
// 4. 'first' node should now point to what 'second' *used* to point to
first.next = second.next.take();
// 5. 'second' node should now point to 'first'
second.next = Some(first);
// 6. The node before the pair ('curr') should now point to 'second'
curr.next = Some(second);
// --- Move 'curr' forward two positions for the next iteration ---
// 'curr' now points to the original 'second' node (which is now the head of the swapped pair)
// Then move again to point to the original 'first' node (which is now the second in the pair)
// This sets 'curr' *before* the next pair.
curr = curr.next.as_mut().unwrap().next.as_mut().unwrap();
}
// The actual head of the swapped list is what dummy.next points to
dummy.next
}
}
Challenges and Rust-specific solutions:
- Borrow Checker Dance: The iterative solution involves more
take(),unwrap(), and mutable references (&mut) because we need to temporarily unhook nodes to re-link them. The compiler ensures we don't create invalid states. curr.next.as_mut().unwrap().next.as_mut().unwrap(): This looks a bit clunky, but it's how we navigate throughOption<Box<ListNode>>chains with mutable references. Eachas_mut().unwrap()gives us a mutable reference to theBox<ListNode>inside theOption, allowing us to access itsnextfield.
Conclusion
Both recursive and iterative solutions effectively solve the "Swap Nodes in Pairs" problem while adhering to the constraint of not modifying node values.
- The recursive solution offers remarkable clarity and conciseness, especially suitable for Rust's functional approach to
OptionandBox. - The iterative solution provides more explicit control over memory and avoids recursion depth limits, though it requires a deeper understanding of mutable borrowing and
take()operations in Rust.
Which approach do you find more intuitive? Let me know in the comments!

