← Back to DevBytes

Solving Intersection of Two Linked Lists in JavaScript: Step-by-Step Guide

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

Why This Problem Matters

This problem is more than an academic exercise. It has practical implications in several areas of software development:

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

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

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:

Complexity Analysis

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

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:

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

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.

— Ad —

Google AdSense will appear here after approval

← Back to all articles