Two support queues — chat and email — are singly linked chains of ticket hops. After a tooling cutover, both prefixes splice onto one shared escalation suffix: the same node objects from a sev-1 hop onward. Ops asked for that first shared hop so one pager covered both channels. The intern scanned titles. Two unrelated hops both named ack looked like a merge. The real Y sat later, identical as a pointer. The pager fired on the wrong ticket.
Intersection is the first node that belongs to both lists as an object — a Y-shaped suffix, not a value match.
This is an interview writeup, not a two-pointers lecture. The linked list post owns splice and why get(i) walks. The two pointers post owns the move rule. Here we only care about two walks of next that land on the same node object, or both on null.
The problem
Given heads headA and headB of two singly linked lists, return the first node that appears on both lists, or null if they never share a node. If they intersect, they share a suffix: from that node onward every next is the same object. Values may repeat. Intersection is identity.
A: 1 → 2 → 3 → 4 → 5 → null
B: 9 → 4 → 5 → null (4 and 5 are A's nodes)
→ node 4
A: 1 → 2 → 3 → null
B: 4 → 5 → 6 → null (no shared node)
→ null
A: 1 → 2 → null
B: 1 → 2 → null (same values, different objects)
→ null
A and B both empty, or exactly one empty
→ null
A node is the usual two fields:
final class ListNode {
int val;
ListNode next;
ListNode(int val) {
this.val = val;
}
}
Note: This is a Y, not a loop. Each list still ends at null. Linked List Cycle is a different question: some next points at an earlier node. Do not run the slow/fast meeting test here.
A HashSet of nodes is the honest brute force
Walk A and store every node object. Walk B. The first node of B already in the set is the start of the shared suffix — B cannot hit a later shared node without hitting that one first. If B ends, they never met.
ListNode intersectionSeen(ListNode headA, ListNode headB) {
Set<ListNode> seen = new HashSet<>();
for (ListNode p = headA; p != null; p = p.next) {
seen.add(p);
}
for (ListNode p = headB; p != null; p = p.next) {
if (seen.contains(p)) {
return p;
}
}
return null;
}
ListNode does not override equals / hashCode, so the set hashes identity. That is the correct brute. A HashSet of val is not intersection. Duplicate titles are false positives; a real Y of unique titles is still the wrong node if an earlier B hop reuses a title from A’s prefix. The set is honest and still costs one extra entry per hop on A. Interviewers will ask whether you can drop it.
Switch heads so remaining distance is equal
Apply two pointers on two lists, not opposite ends of one array. pA starts at headA. pB starts at headB. Each step follows next. When a pointer hits null, send it to the other list’s head. Each pointer walks one list then the other, so remaining distance to the merge (or to null) becomes equal, and they land on the same node together.
If the prefixes have lengths a and b and the shared suffix has length c, pA finishes A (a + c) then walks B’s prefix (b) to the merge; pB finishes B then walks A’s prefix. Same total a + b + c. If there is no merge, both finish the two lists and sit on null at the same step.
Walk A 1 → 2 → 3 → 4 → 5 and B 9 → 4 → 5, with 4 and 5 the same objects:
start: pA=1 pB=9
step 1: pA=2 pB=4
step 2: pA=3 pB=5
step 3: pA=4 pB=null (B ended; next step B starts at headA)
step 4: pA=5 pB=1
step 5: pA=null pB=2 (A ended; next step A starts at headB)
step 6: pA=9 pB=3
step 7: pA=4 pB=4 meet, return node 4
Disjoint lists of any lengths meet at null: each pointer walks both chains once, then both are null and pA == pB. You never claim a meeting at the start unless the two heads are already the same object.
ListNode intersectionNode(ListNode headA, ListNode headB) {
ListNode pA = headA;
ListNode pB = headB;
while (pA != pB) {
pA = (pA == null) ? headB : pA.next;
pB = (pB == null) ? headA : pB.next;
}
return pA;
}
Time is O(n + m) — each pointer walks both lists at most once. Space is O(1) besides the two pointers. The HashSet walk is also linear time and spends O(n) extra; the switch spends the same time without pinning A’s nodes.
The same remaining-distance idea with an explicit count: walk each list for its length, advance the longer pointer by |lenA - lenB|, then step together until they refer to the same node or both hit null. Same bills. The switch version does that alignment without storing the two lengths.
The null hop is the length padding. Switch when the pointer is null, not when next is null. Skipping that step desynchronizes the two walks. == is reference equality; do not switch to equals on val.
Empty A, empty B, or one empty: both pointers sit on null after the same number of steps (zero if both heads are null), you return null. Two heads that are already the same object: the loop never runs, you return that node. No special cases.
What interviewers usually poke next
- Count, skip, walk. Length of each list, advance the longer head by the difference, then one step at a time. Same answer as the switch; say why the extra prefix is the only thing that was unaligned.
- Existence only. Return a boolean; the walk is unchanged, and you test the result against null.
- A cycle on one list. Then “hit null” never happens and the switch loops. That is Linked List Cycle, a different prompt. This problem’s lists are acyclic Ys.
- Mutate A, scan B, restore. Marking
nextorvalis a bug factory if a caller still holds those nodes. The set is the honest extra-space version; prefer the two pointers. - Doubly linked. You could walk both tails backward until the nodes diverge, then take the successor. Identity is still the test. The layout post already has
prev.
You are done with this problem when you can walk the switch on 1 → 2 → 3 → 4 → 5 / 9 → 4 → 5 without matching values, and you can name the disjoint return as both pointers sitting on null.