Finding Intersecting Node of Two Linked Lists

Intersecting linked lists are two separate linked lists which share a common node or sequence of nodes. This occurs when two initially distinct lists converge at a specific point, creating a Y-shaped structure. Intersecting linked lists are often used to model real-world scenarios such as network topologies or genealogical trees.

Below is how two intersecting linked lists looks like:

Linked List-1
Head Node
Linked List-1Head...
1
1
2
2
3
3
4
4
5
5
Tail Node
Tail Node
6
6
7
7
Linked List-2
Head Node
Linked List-2...
Text is not SVG - cannot display

Prerequisites: Basics of Linked List Data Structure, Creating and Connecting Nodes in a Linked List, Traverse and Print all values present in a Linked List Data Structure.

Problem Statement

Given two singly linked lists that may or may not intersect, determine if there is a point of intersection. If an intersection exists, find and return the node at which the two lists intersect.

Let’s illustrate this with examples:

  1. Intersecting Lists: In this case, the intersecting node is the one with value 4.

  2. Non-intersecting Lists: In this case, there is no point of intersection.

Constraints and assumptions:

  • The lists are singly linked, meaning each node only has a reference to the next node.
  • There are no cycles in either of the lists.
  • If an intersection exists, it is guaranteed that the nodes after the intersecting node are identical for both lists.
  • The intersecting node is compared by reference, not by value. Two nodes are considered intersecting only if they are the same node by reference.

Approaches to Solve the Problem

Brute Force Approach

The brute force approach is the most straightforward method to solve this problem. It involves comparing each node of one list with every node of the other list to find a common node.

Steps of the algorithm:

  1. Start with the head of the first linked list.
  2. For each node in the first list:
    • Traverse the entire second list.
    • Compare the current node of the first list with each node of the second list during the iteration process.
    • If a match is found, return this node as the intersection point.
  3. If no match is found after comparing all nodes, return null (no intersection).

Time and Space Complexity:

  • Time Complexity: O(m * n), where m and n are the lengths of the two lists. In the worst case, we need to compare every node of the first list with every node of the second list.
  • Space Complexity: O(1), as we only use a constant amount of extra space regardless of the input size.

Below is the implementation of this approach:


# Definition of the Node class for the linked list
class Node:
  def __init__(self, data):
    self.data = data
    self.next = None

# Helper function to create a linked list
def create_linked_list(values):
  if not values:
    return None
  head = Node(values[0])
  current = head
  for value in values[1:]:
    current.next = Node(value)
    current = current.next
  return head

# Helper function to print the linked list
def print_linked_list(head):
  current = head
  while current:
    print(current.data, end=" -> ")
    current = current.next
  print("None")

def find_intersection(head1, head2):
  # Start with the head of the first linked list
  current1 = head1
  
  # For each node in the first list
  while current1:
    # Traverse the entire second list
    current2 = head2
    
    # Compare the current node of the first list
    # with each node of the second list
    while current2:
      # If a match is found,
      # return this node as the intersection point
      if current1 == current2:
        return current1
      current2 = current2.next
    
    current1 = current1.next
  
  # If no match is found after comparing all nodes,
  # return None (no intersection)
  return None

# Create two intersecting linked lists for testing
list1 = create_linked_list([1, 2, 3, 4, 5])
list2 = create_linked_list([9, 8])

# Create the intersection point
intersection_node = list1.next.next  # Node with value 3
list2.next.next = intersection_node

print("List 1:")
print_linked_list(list1)
print("List 2:")
print_linked_list(list2)

# Find the intersection point
intersection = find_intersection(list1, list2)

if intersection:
  print(f"Intersection found at node with value: {intersection.data}")
else:
  print("No intersection found")

Drawbacks of this approach:

  • Inefficient for large lists due to the quadratic time complexity.
  • Does not take advantage of the list structure or the problem constraints.
  • May time out for very long lists in interview or coding competition scenarios.

While this approach is easy to understand and implement, it’s not optimal for solving the problem efficiently, especially for large inputs. More sophisticated approaches can significantly improve the time complexity.

Hash Table Approach

The Hash Table approach offers a more efficient solution compared to the brute force method. This technique leverages a hash table to store nodes from one list and then checks for their presence while traversing the other list.

Steps of the algorithm:

  1. Create an empty hash set.
  2. Traverse the first linked list and add each node to the hash set.
  3. Traverse the second linked list:
    • For each node, check if it’s already in the hash set.
    • If a node is found in the hash set, return it as the intersection point.
  4. If no intersection is found after traversing both lists, return null.

Time and Space Complexity:

  • Time Complexity: O(m + n), where m and n are the lengths of the two lists. We traverse each list once.
  • Space Complexity: O(m), where m is the length of the first list. In the worst case, we store all nodes of the first list in the hash set.

Below is the implementation of this approach:


class Node:
  def __init__(self, data):
    self.data = data
    self.next = None

# Helper function to create a linked list
def create_linked_list(values):
  if not values:
    return None
  head = Node(values[0])
  current = head
  for value in values[1:]:
    current.next = Node(value)
    current = current.next
  return head

# Helper function to print a linked list
def print_linked_list(head):
  current = head
  while current:
    print(current.data, end=" -> ")
    current = current.next
  print("None")

def find_intersection(list1, list2):
  # Step 1: Create an empty hash set
  node_set = set()

  # Step 2: Traverse the first linked list
  # and add each node to the hash set
  current = list1
  while current:
    node_set.add(current)
    current = current.next

  # Step 3: Traverse the second linked list
  current = list2
  while current:
    # Check if the current node is already in the hash set
    if current in node_set:
      # If found, return it as the intersection point
      return current
    current = current.next

  # Step 4: If no intersection is found, return None
  return None

# Create two intersecting linked lists for testing
list1 = create_linked_list([1, 2, 3, 4, 5])
list2 = create_linked_list([9, 8])

# Create the intersection point
intersection_node = list1.next.next  # Node with value 3
list2.next.next = intersection_node

print("List 1:")
print_linked_list(list1)
print("List 2:")
print_linked_list(list2)

# Find the intersection point
intersection = find_intersection(list1, list2)

if intersection:
  print(f"Intersection found at node with value: {intersection.data}")
else:
  print("No intersection found")

Drawbacks of this approach:

  • Requires additional space proportional to the size of the first list.
  • The constant factor for time complexity is higher due to hash computations.
  • If the lists are very large, the space overhead might be significant.

While this approach significantly improves the time complexity compared to the brute force method, it does so at the cost of increased space usage. For scenarios where memory is a constraint, or when dealing with extremely large lists, this trade-off might not be ideal. However, for most practical purposes, this approach offers a good balance between time efficiency and implementation simplicity.

Two-Pointer Approach (Optimal Solution)

The Two-Pointer approach provides an elegant and optimal solution to the problem of finding the intersection point of two linked lists. This method uses constant extra space and runs in linear time, making it highly efficient.

Steps of the algorithm:

  1. Initialize two pointers, pointerA and pointerB, to the heads of the first and second lists respectively.
  2. Traverse both lists simultaneously:
    • Move pointerA one step forward in the first list.
    • Move pointerB one step forward in the second list.
  3. If either pointer reaches the end of its list, redirect it to the head of the other list.
  4. Continue this process until:
    • The pointers meet at a node (intersection found), or
    • Both pointers become null (no intersection).
  5. Return the intersection node or null.

Time and Space Complexity:

  • Time Complexity: O(m + n), where m and n are the lengths of the two lists. In the worst case, we traverse each list twice.
  • Space Complexity: O(1), as we only use two pointers regardless of the input size.

Below is the implementation of this approach:


# Definition of the Node class for the linked list
class Node:
  def __init__(self, data):
    self.data = data
    self.next = None

# Helper function to create a linked list
def create_linked_list(values):
  if not values:
    return None
  head = Node(values[0])
  current = head
  for value in values[1:]:
    current.next = Node(value)
    current = current.next
  return head

# Helper function to print the linked list
def print_linked_list(head):
  current = head
  while current:
    print(current.data, end=" -> ")
    current = current.next
  print("None")

def find_intersection(headA, headB):
  # Step 1: Initialize two pointers
  pointerA = headA
  pointerB = headB

# Steps 2-4: Traverse both lists
  while pointerA != pointerB:
    # Move pointerA one step forward
    if pointerA:
      pointerA = pointerA.next
    else:
      pointerA = headB

    # Move pointerB one step forward
    if pointerB:
      pointerB = pointerB.next
    else:
      pointerB = headA
  # Step 5: Return the intersection node or None
  return pointerA

# Example run
if __name__ == "__main__":
  # Create the intersecting part of the lists
  intersection = create_linked_list([3, 4, 5])

  # Create the first list: 1 -> 2 -> 3 -> 4 -> 5
  listA = create_linked_list([1, 2])
  listA.next.next = intersection

  # Create the second list: 8 -> 9 -> 3 -> 4 -> 5
  listB = create_linked_list([8, 9])
  listB.next.next = intersection

  print("List A:")
  print_linked_list(listA)
  print("List B:")
  print_linked_list(listB)

  # Find the intersection
  intersection_node = find_intersection(listA, listB)

  if intersection_node:
    print(f"Intersection found at node with value: {intersection_node.data}")
  else:
    print("No intersection found")

Why this approach works: The key insight behind this approach is that by switching the pointers to the head of the other list when they reach the end, we effectively “align” the two lists. If there’s an intersection, both pointers will meet at the intersecting node after traversing the same total distance.

Consider two lists of lengths a + c and b + c, where c is the length of the common part:

  • Pointer A travels: a + c + b
  • Pointer B travels: b + c + a

By the time both pointers have switched lists once, they will have traveled the same distance and will meet at the intersection point. If there’s no intersection, both pointers will reach the end (null) simultaneously.

This approach elegantly handles lists of different lengths and requires no additional data structures, making it both space-efficient and easy to implement.

Applications

Finding intersecting nodes in linked lists has several practical applications and is related to various real-world scenarios:

  • Network Topology Analysis: In computer networks, intersecting linked lists can represent converging network paths or shared resources. Identifying intersection points helps in optimizing routing algorithms and detecting bottlenecks.

  • Version Control Systems: Git and other VCS use linked list-like structures to represent commit histories. Finding the intersection point can help identify where two branches diverged or merged.

  • File Systems: In file systems with hard links, multiple directory entries can point to the same inode. Detecting intersections can help in identifying shared files and optimizing storage.

  • Detecting Cycles in Linked Lists: The Floyd’s Cycle-Finding Algorithm (Tortoise and Hare) uses a similar two-pointer approach to identify loops in a linked list.

  • Finding the Middle of a Linked List: This problem employs a fast and slow pointer technique, conceptually similar to the two-pointer approach used in finding intersections.

  • Intersection of Multiple Linked Lists: An extended version of the problem where the task is to find a common node among more than two lists, increasing complexity.

  • Intersection with Cycles: A more intricate variation where one or both lists may contain cycles, requiring additional considerations in the solution approach.