Problem overview
The function get_intersection_node receives the heads of two singly linked lists, head_a and head_b, and returns the node where they meet, or None when they never do. Two lists intersect when, from some node on, they are the same nodes: they share one common tail. Each list can have a different number of nodes before that point.
In the first example, list A is 4, 1, 8, 4, 5 and list B is 5, 6, 1, 8, 4, 5. The nodes holding 8, 4, 5 are shared, so the answer is the node holding 8. A has two nodes before it and B has three. The two lists must keep their structure, and there are no cycles. The follow-up asks for O(m + n) time and O(1) memory, where m and n are the lengths.
In this lesson a Link is an optional shared pointer to a node: Option<Rc<RefCell<...>>>. Rc lets two lists own the same node, which a plain Box cannot do, and RefCell gives access to the node's fields through borrow().
Approach: align the ends, then walk together
The brute force compares every node of A with every node of B, which costs O(m * n). A hash set of A's nodes brings the time to O(m + n) but uses O(m) memory. The constant-memory idea uses one fact: because the tail is shared, both lists end with the same number of nodes after the meeting point. Only the part before the meeting point can differ in length.
So measure both lists. Suppose A is longer by d nodes. Those d extra leading nodes of A cannot be the intersection, because B has no node that far back to match. Skip them. Now both pointers are the same distance from the end, and walking them forward together makes them reach the shared node on the same step. If there is no shared node, both reach the end together, and the end is None on both sides.
For the first example the lengths are 5 and 6, so B skips its first node and stands on 6. A stands on its head, 4. The pointers differ, so both advance: A is on 1 and B is on its own 1, which is a different node with the same value, so they differ again. They advance once more and both stand on the node 8, the same allocation, so the loop stops and the function returns it.
Step by step
- Measure both lists with a helper that is written last:
Self::list_len(&head_a)and the same for B. - Clone the heads into the walkers
aandb. Cloning anRccopies the pointer and increases the reference count by one. The nodes are not copied. - Skip A's extra nodes with
for _ in 0..len_a.saturating_sub(len_b). When A is not longer,saturating_subgives 0 and the loop does nothing. - Step forward in two statements. First read the next link into a variable, then assign it to
a. - Do the same for B with the arguments of
saturating_subswapped. - Walk together with
while !Self::ptr_eq(&a, &b), advancing both walkers on each pass. - Return
a. When the loop ends, both walkers are on the intersection node, or both areNone. - Write
ptr_eq, which compares by identity: twoSomelinks are equal ifRc::ptr_eqsays they share the same allocation, twoNonelinks are equal, and a mixed pair is not. - Write
list_len: a counter, a cloned cursor, and awhile let Some(node) = curloop that increments the counter and moves the cursor tonode.borrow().next.clone().
The solution in Rust
impl Solution {
fn get_intersection_node(head_a: Link, head_b: Link) -> Link {
let len_a = Self::list_len(&head_a);
let len_b = Self::list_len(&head_b);
let mut a = head_a.clone();
let mut b = head_b.clone();
for _ in 0..len_a.saturating_sub(len_b) {
let next = a.as_ref().unwrap().borrow().next.clone();
a = next;
}
for _ in 0..len_b.saturating_sub(len_a) {
let next = b.as_ref().unwrap().borrow().next.clone();
b = next;
}
while !Self::ptr_eq(&a, &b) {
let next_a = a.as_ref().unwrap().borrow().next.clone();
let next_b = b.as_ref().unwrap().borrow().next.clone();
a = next_a;
b = next_b;
}
a
}
fn ptr_eq(a: &Link, b: &Link) -> bool {
match (a, b) {
(Some(ra), Some(rb)) => Rc::ptr_eq(ra, rb),
(None, None) => true,
_ => false,
}
}
fn list_len(head: &Link) -> usize {
let mut count = 0;
let mut cur = head.clone();
while let Some(node) = cur {
count += 1;
cur = node.borrow().next.clone();
}
count
}
}
Rust details worth knowing:
- Identity, not equality. The two lists in the first example both contain a node holding 1 before the shared part, but these are different nodes.
Rc::ptr_eqasks whether two pointers refer to the same allocation, which is the question the problem asks. Comparingvalfields would stop too early. saturating_sub.len_aandlen_bareusize, which cannot go below zero. With twousizevariables holding 3 and 5,x.saturating_sub(y)is 0, where plainx - ypanics in a debug build and wraps to a huge number in an optimized one. This is why only the longer list is advanced.- Two statements per step. Writing
a = a.as_ref().unwrap().borrow().next.clone();in one line does not compile. The temporaryRefreturned byborrow()is still alive at the end of the statement, so the compiler rejects the assignment with error E0506, "cannot assign toabecause it is borrowed". Reading intonextfirst lets the temporary die, and the assignment then follows. - Cloning is cheap.
head_a.clone()on anOption<Rc<...>>copies one pointer and bumps a counter. With one node and one owner the count is 1, and after a clone it is 2. while let Some(node) = curmovescur. The body assignscuragain before the next test, so this is fine.
Complexity
Time: O(m + n). Measuring takes m + n steps. Skipping takes at most |m - n| steps, and the joint walk takes at most min(m, n) steps.
Space: O(1). The walkers are pointers, and cloning an Rc allocates nothing.
Edge cases and common mistakes
- No intersection. The walkers reach
Nonetogether,ptr_eqsees(None, None)and the function returnsNone. This also holds when the two lists have equal values but separate nodes. - Same list twice. If both heads are the same node, the lists are already aligned and the first test succeeds. The function returns the head.
- Intersection at a head. When B's head is a node inside A, B has no extra nodes, A skips its extra ones, and the walkers meet on the first test.
- Comparing values. Two different nodes can hold the same number. Only identity is correct.
- Subtracting lengths directly.
len_a - len_bunderflows whenever B is the longer list. - Calling
unwrap()onNone. The skip loops run at most the length difference, which is smaller than the longer list, and the joint loop stops at the latest when both areNone, so everyunwrap()is on a real node.
Testing
- A =
[4, 1, 8, 4, 5]and B =[5, 6, 1, 8, 4, 5]sharing the tail8, 4, 5gives the node with value 8. - Two separate lists
[1, 2]and[1, 2]giveNone. - A =
[3, 9]and B =[9]where B's node is A's last node gives that node. - The same head passed twice returns the head.
