Problem overview
The method getIntersectionNode receives the heads of two singly linked lists, headA and headB. The lists may merge into one shared tail. If they do, the method returns the first node that belongs to both lists. If they never meet, it returns null. The lists must keep their original structure, and the input has no cycles.
The word "intersect" means the same node object, not just equal values. In the first example, list A reads [4, 1, 8, 4, 5] and list B reads [5, 6, 1, 8, 4, 5]. Both contain a 1 before the 8, but those are two separate nodes. The lists share the tail that starts at the node holding 8, so the answer is that node. In the third example, [2, 6, 4] and [1, 5] share nothing, so the answer is null.
The follow-up asks for O(m + n) time and O(1) memory, where m and n are the lengths of the lists.
Approach: align the starting points
Storing every node of A in a set and checking B against it takes O(m) extra memory, which the follow-up rules out. Comparing every node of A with every node of B takes O(m * n) time.
The better idea uses one fact: once the lists meet, they are the same list from there to the end. So the part after the intersection has the same length in both. Any extra length is in the part before it, and only the longer list can have more of it.
Suppose A has 5 nodes and B has 6. Skip the first node of B. Now both remaining lists have 5 nodes, and any shared tail sits at the same distance from the end in both. Walk one step at a time with a pointer in each list. They are on the same node as soon as they reach the intersection, and if the lists don't intersect, they both reach null together. In both cases the pointers compare equal, so one loop handles both outcomes.
Step by step
- Measure list A with a helper,
val sizeA = size(headA). The helper gets written at the end. - Measure list B the same way,
val sizeB = size(headB). - Create two walkers,
var nA = headAandvar nB = headB. The parameters can't be reassigned, and the walkers will move. - Compare the lengths with
if (sizeA < sizeB). Whichever list is longer needs to skip its extra nodes. - Advance B's walker by
sizeB - sizeAusing the second helper,moveNode. The helper returns the node it stopped on, so the result must be assigned back:nB = moveNode(nB, sizeB - sizeA). - Otherwise advance A's walker by
sizeA - sizeB. When the lengths are equal, that distance is 0 and nothing moves. - Walk together with
while (nA ne nB). This compares references, not contents. - Advance both walkers one step inside the loop.
- Return
nAas the last expression. It is the intersection node, ornullwhen the walkers ran off the ends together. - Write
size. Copyheadintovar nodeand start avar count = 0. - Loop while
node != null. - Count and move with
count += 1andnode = node.next. - Yield
countas the method's value. - Write
moveNode. Copy both parameters into mutable locals,var n = nodeandvar kk = k. - Loop while
kk > 0. - Step forward with
n = n.nextandkk -= 1. - Yield
n, the node where the walk ended.
The solution in Scala
class Solution {
def getIntersectionNode(headA: ListNode, headB: ListNode): ListNode = {
val sizeA = size(headA)
val sizeB = size(headB)
var nA = headA
var nB = headB
if (sizeA < sizeB) {
nB = moveNode(nB, sizeB - sizeA)
} else {
nA = moveNode(nA, sizeA - sizeB)
}
while (nA ne nB) {
nA = nA.next
nB = nB.next
}
nA
}
def size(head: ListNode): Int = {
var count = 0
var node = head
while (node != null) {
count += 1
node = node.next
}
count
}
def moveNode(node: ListNode, k: Int): ListNode = {
var n = node
var kk = k
while (kk > 0) {
n = n.next
kk -= 1
}
n
}
}
The Scala details worth knowing:
neandeq. These operators compare references.a ne bis true whenaandbare different objects.==callsequals, which a class may override to compare contents (case classes do), so for "is this the same node?" the reference operators are the safe choice. Both operators acceptnullon either side, which is why the loop ends when both walkers reachnull.- The last expression is the result. The methods end with
nA,countandnand need noreturn. Anif/elseused as a statement, as here, simply runs one of its branches. - Parameters are immutable. That is why
sizeandmoveNodecopy their arguments intovarlocals before changing them. - Method order doesn't matter. Inside a class,
getIntersectionNodecan callsizeandmoveNodeeven though they are defined below it. - Return types. The methods declare
ListNodeandInt, andnAmay benullat the end, which is a legal value forListNodehere.
Complexity
Time: O(m + n). Measuring the lists costs m + n steps. Skipping the extra nodes costs at most |m - n| steps, and the final walk takes at most min(m, n) steps. Everything adds up to a small multiple of m + n.
Space: O(1). Only a few integers and two node references are stored, and the lists are never modified.
Edge cases and common mistakes
- No intersection. After alignment, the walkers step through the rest of their lists and both become
nullon the same step. ThennA ne nBis false and the method returnsnull. - Equal lengths. The skip is 0 and the loop starts right away. If the heads are the same node, the loop doesn't run and that head is returned.
- The intersection is the head of the shorter list. If B is itself the shared tail, A skips its extra nodes and both walkers start on the shared head, so the loop body never runs and that node is returned.
- Comparing values instead of nodes. Two different nodes can both hold 1, or even the same values all the way down. Only reference comparison identifies a shared node, which is why the loop uses
ne. - Dropping the assignment. Writing
moveNode(nB, sizeB - sizeA)withoutnB =does nothing useful: the helper returns a node but can't change which nodenBrefers to. - Skipping the wrong list. Advancing the shorter list makes the walkers misaligned, so they would run past the intersection and never compare equal at the right node.
- Breaking the lists. The code never changes a
nextlink, so the structure is preserved as the statement requires.
Testing
- A =
[4, 1, 8, 4, 5], B =[5, 6, 1, 8, 4, 5], sharing the tail from the node holding 8, returns that node (value 8). - A =
[1, 9, 1, 2, 4], B =[3, 2, 4], sharing the tail from the node holding 2, returns that node (value 2). - A =
[2, 6, 4], B =[1, 5], with no shared node, returnsnull. - The same list passed as both A and B returns its head.
