Introduction to the Intersection of Two Linked Lists Problem
The "Intersection of Two Linked Lists" is a classic algorithmic problem frequently encountered in coding interviews and competitive programming. Given the heads of two singly linked lists, the task is to determine the node at which the two lists intersect. If they do not intersect, the function should return null. The challenge lies not just in finding the intersection, but in doing so efficiently — ideally in linear time and constant space.
Understanding this problem is essential because it tests your grasp of pointer manipulation, list traversal, and algorithmic optimization. In this tutorial, we will explore the problem in depth, walk through multiple solutions, and discuss best practices for implementing them in JavaScript.
What Is a Linked List Intersection?
A linked list intersection occurs when two distinct linked lists share a common tail. That is, after a certain node, both lists merge into a single sequence of nodes. The intersection point is the first node that is common to both lists. It is important to note that intersection is based on reference equality, not value equality — two nodes with the same value but at different memory locations are not considered intersecting.
For example, consider the following two lists:
List A: 4 → 1 ↘
8 → 4 → 5 → null
List B: 5 → 6 → 1 ↗
Here, both lists merge at the node with value 8. The intersection node is the one containing 8, not the earlier nodes with value 1 in either list.
Key Constraints to Remember
- Intersection is determined by reference, not by value.
- Once two lists intersect, they share all subsequent nodes until the end.
- The lists may have different lengths before the intersection point.
- There may be no intersection at all, in which case the result is
null.
Why This Problem Matters
This problem is more than an academic exercise. It has practical implications in several areas of software development:
- Interviews: It is a staple in technical interviews at major tech companies because it evaluates multiple skills simultaneously — pointer manipulation, edge case handling, and algorithmic optimization.
- Memory Awareness: Understanding reference equality versus value equality deepens your knowledge of how JavaScript handles objects and memory.
- Real-World Applications: Linked list intersections can model scenarios like merging version control branches, detecting common paths in routing, or identifying shared resources in dependency graphs.
- Foundation for Advanced Topics: The techniques used here, such as the two-pointer approach, are foundational for solving more complex problems involving cycles, palindromes, and list reordering.
Setting Up the Linked List Structure in JavaScript
Before solving the problem, we need a basic linked list node structure. In JavaScript, we typically define a node using a class or a constructor function.
class ListNode {
constructor(val) {
this.val = val;
this.next = null;
}
}
// Helper function to create a linked list from an array
function createList(arr) {
if (arr.length === 0) return null;
const head = new ListNode(arr[0]);
let current = head;
for (let i = 1; i < arr.length; i++) {
current.next = new ListNode(arr[i]);
current = current.next;
}
return head;
}
// Helper to print a list
function printList(head) {
const values = [];
while (head) {
values.push(head.val);
head = head.next;
}
console.log(values.join(' → ') + ' → null');
}
With this structure in place, we can now construct test cases and implement our solutions.
Approach 1: Brute Force
The most straightforward approach is to compare every node of list A with every node of list B. If a match is found by reference, that node is the intersection. While simple, this approach has significant drawbacks.
function getIntersectionNodeBruteForce(headA, headB) {
let currentA = headA;
while (currentA) {
let currentB = headB;
while (currentB) {
if (currentA === currentB) {
return currentA;
}
currentB = currentB.next;
}
currentA = currentA.next;
}
return null;
}
Complexity Analysis
- Time Complexity: O(m × n), where m and n are the lengths of the two lists. This is inefficient for large lists.
- Space Complexity: O(1), since no additional data structures are used.
This approach works but is not suitable for production or interview settings due to its quadratic time complexity.
Approach 2: Hash Set for Reference Tracking
A more efficient approach uses a hash set to store references to all nodes in one list, then traverses the second list checking for membership in the set. This trades space for time.
function getIntersectionNodeHashSet(headA, headB) {
const visited = new Set();
let current = headA;
while (current) {
visited.add(current);
current = current.next;
}
current = headB;
while (current) {
if (visited.has(current)) {
return current;
}
current = current.next;
}
return null;
}
Complexity Analysis
- Time Complexity: O(m + n), since we traverse each list once.
- Space Complexity: O(m) or O(n), depending on which list we store in the set.
This is a solid solution and is often acceptable in interviews. However, if the problem requires constant space, we need a better approach.
Approach 3: Two-Pointer Technique (Optimal Solution)
The two-pointer technique is the most elegant and optimal solution. The idea is to use two pointers, one for each list. When a pointer reaches the end of its list, it redirects to the head of the other list. If the lists intersect, the pointers will meet at the intersection node after at most two traversals. If they do not intersect, both pointers will reach null simultaneously.
The key insight is that by switching lists, both pointers traverse the same total number of nodes: the length of list A plus the length of list B. This equalizes the difference in lengths before the intersection point.
function getIntersectionNode(headA, headB) {
if (!headA || !headB) return null;
let pointerA = headA;
let pointerB = headB;
// When a pointer reaches the end, redirect it to the head of the other list.
// They will meet at the intersection, or both become null.
while (pointerA !== pointerB) {
pointerA = pointerA === null ? headB : pointerA.next;
pointerB = pointerB === null ? headA : pointerB.next;
}
return pointerA;
}
How It Works Step by Step
Let us trace through an example where list A has length 5 and list B has length 6, and they intersect at the third node of the shared tail:
- Pointer A traverses list A (5 nodes), then switches to list B.
- Pointer B traverses list B (6 nodes), then switches to list A.
- After switching, both pointers have traversed 5 + 6 = 11 nodes total.
- Because they have now covered the same total distance, they align at the intersection point.
Complexity Analysis
- Time Complexity: O(m + n). Each pointer traverses at most m + n nodes.
- Space Complexity: O(1). Only two pointers are used regardless of input size.
This is the optimal solution and the one you should aim to implement in interviews.
Approach 4: Length Difference Method
Another approach is to first calculate the lengths of both lists, compute the difference, and advance the pointer of the longer list by that difference. Then traverse both lists simultaneously until the pointers meet.
function getLength(head) {
let length = 0;
while (head) {
length++;
head = head.next;
}
return length;
}
function getIntersectionNodeByLength(headA, headB) {
if (!headA || !headB) return null;
const lenA = getLength(headA);
const lenB = getLength(headB);
let longer = lenA > lenB ? headA : headB;
let shorter = lenA > lenB ? headB : headA;
const diff = Math.abs(lenA - lenB);
// Advance the longer list pointer by the difference
for (let i = 0; i < diff; i++) {
longer = longer.next;
}
// Traverse both lists simultaneously
while (longer !== shorter) {
longer = longer.next;
shorter = shorter.next;
}
return longer;
}
Complexity Analysis
- Time Complexity: O(m + n), as we traverse both lists to find lengths and then again to find the intersection.
- Space Complexity: O(1), using only a few variables.
This approach is also optimal in terms of complexity and can be easier to explain to an interviewer than the two-pointer technique.
Testing the Solutions
To verify our solutions, let us create a test case where two lists intersect. We will build the shared tail first, then attach different prefixes to create two lists.
// Create the shared portion: 8 → 4 → 5
const shared = createList([8, 4, 5]);
// Create list A: 4 → 1 → [shared]
const headA = new ListNode(4);
headA.next = new ListNode(1);
headA.next.next = shared;
// Create list B: 5 → 6 → 1 → [shared]
const headB = new ListNode(5);
headB.next = new ListNode(6);
headB.next.next = new ListNode(1);
headB.next.next.next = shared;
// Test all solutions
console.log('Brute Force:', getIntersectionNodeBruteForce(headA, headB).val);
console.log('Hash Set:', getIntersectionNodeHashSet(headA, headB).val);
console.log('Two-Pointer:', getIntersectionNode(headA, headB).val);
console.log('Length Diff:', getIntersectionNodeByLength(headA, headB).val);
// All should output: 8
Let us also test the case where there is no intersection:
const listA = createList([1, 2, 3]);
const listB = createList([4, 5, 6]);
console.log('No intersection:', getIntersectionNode(listA, listB));
// Should output: null
Best Practices
Always Handle Edge Cases
Before implementing the core logic, check for edge cases such as empty lists, single-node lists, and lists of vastly different lengths. Defensive programming prevents runtime errors and demonstrates thoroughness.
Prefer the Two-Pointer Approach
Among all solutions, the two-pointer technique is the most elegant. It achieves O(m + n) time and O(1) space without requiring length calculations or additional data structures. It is concise, easy to write, and performs well in practice.
Use Reference Equality, Not Value Equality
A common mistake is comparing node values instead of node references. Always use === to compare node objects directly. Two nodes with the same value but different references are not the same node.
Write Clear Helper Functions
Utility functions like createList, printList, and getLength make your code more readable and easier to test. Keep them in a shared module so you can reuse them across different problems.
Test Thoroughly
Test your solution with multiple scenarios:
- Lists that intersect at the head.
- Lists that intersect at the tail.
- Lists of equal length that intersect.
- Lists of unequal length that intersect.
- Lists that do not intersect.
- One or both lists being empty.
Avoid Modifying the Input Lists
Your solution should not alter the structure of the input lists. Some approaches might be tempted to reverse the lists or modify pointers, but this can cause side effects in the calling code. Always preserve the integrity of the input data.
Common Pitfalls to Avoid
- Confusing value equality with reference equality: Using
==or comparing.valinstead of the node object itself will produce incorrect results. - Infinite loops in the two-pointer approach: If implemented incorrectly, the pointer redirection can loop forever. Ensure that when a pointer is
null, it switches to the other list's head, not its own. - Not handling the no-intersection case: The two-pointer approach naturally handles this because both pointers become
nullafter switching once, but other approaches need explicit checks. - Forgetting to advance pointers: In while loops, always ensure pointers are advanced to avoid infinite loops.
Conclusion
The Intersection of Two Linked Lists problem is a fundamental algorithmic challenge that tests your understanding of linked data structures, pointer manipulation, and algorithmic optimization. We explored four approaches — brute force, hash set, two-pointer, and length difference — each with its own trade-offs. The two-pointer technique stands out as the optimal solution, achieving linear time complexity with constant space, and it should be your go-to approach in both interviews and production code. By mastering this problem, you build a strong foundation for tackling more advanced linked list challenges and deepen your understanding of how JavaScript handles object references and memory. Practice implementing each solution from scratch, test against diverse edge cases, and you will be well-prepared to solve this problem confidently under pressure.