Difficulty: Medium, Asked-in: Amazon, Microsoft, Yahoo
Key takeaway: An excellent problem to learn problem-solving using two-pointers in a linked list.
Given two sorted linked lists, write a program to return the intersections of the linked lists.
Input: list1 = 2 -> 9 -> 13 -> 20 -> 22 -> 27 -> NULL, list2 = 2 -> 4 -> 9 -> 20 -> 27 -> NULL
Output: 2 -> 9 -> 20 -> 27 -> NULL
Explanation: 2, 9, 20, and 27 are the common nodes to both linked lists.
Input: list1 = 13 -> 22 -> 27 -> NULL, list2 = 4 -> 23 -> NULL
Output: NULL
Explanation: No nodes are common to both linked lists.
Important note: before moving on to the solutions, we recommend trying this problem on paper for atleast 15 or 30 minutes. Enjoy problem-solving!
Both linked lists are sorted in increasing order. How can we use this information to find the common node to both linked lists? Let's think! One idea is: Common nodes will be present in the same order in both sorted linked lists. For example, if one linked list is 2->7->9->12 ->17 and other linked list is 1->7->12->15->17, then the order of common nodes will be the same i.e. (7, 12, 17).
So using the sorted order property, we can scan both lists from the start, compare values one by one and generate the intersection list. When both node values are the same, we add that node to the output. Otherwise, whichever node value is smaller, we move forward in that list to search for the next value of the intersection.
At any intermediate point, suppose we are at a node x in list1 and node y in list2.
Suppose we are using a function listIntersection(ListNode list1, ListNode list2), which takes heads of the linked list as an input and returns the head of the intersection linked list.
Step 1: We initialize two pointers intersecHead and intersecTail with NULL.
Step 2: We run a loop till we reach the end of any one of the linked lists i.e. while (list1 != NULL && list2 != NULL). At each step of the iteration:
Step 3: By the end of the loop, we return the intersecHead as an output.
struct ListNode
{
int data;
ListNode* next;
ListNode(int val)
{
data = val;
next = NULL;
}
};
ListNode* listIntersection(ListNode* list1, ListNode* list2)
{
ListNode* intersecHead = NULL;
ListNode* intersecTail = NULL;
while (list1 != NULL && list2 != NULL)
{
if (list1->data == list2->data)
{
if (intersecHead == NULL)
{
intersecHead = new ListNode(list1->data);
intersecTail = intersecHead;
}
else
{
intersecTail->next = new ListNode(list1->data);
intersecTail = intersecTail->next;
}
list1 = list1->next;
list2 = list2->next;
}
else if (list1->data < list2->data)
list1 = list1->next;
else
list2 = list2->next;
}
return intersecHead;
}
class ListNode:
def __init__(self, val):
self.data = val
self.next = None
def listIntersection(list1, list2):
intersec_head = None
intersec_tail = None
while list1 is not None and list2 is not None:
if list1.data == list2.data:
if intersec_head is None:
intersec_head = ListNode(list1.data)
intersec_tail = intersec_head
else:
intersec_tail.next = ListNode(list1.data)
intersec_tail = intersec_tail.next
list1 = list1.next
list2 = list2.next
elif list1.data < list2.data:
list1 = list1.next
else:
list2 = list2.next
return intersec_head
In the worst-case scenario, we traverse each node of both linked lists at least once and perform an O(1) operation. If the length of both linked lists is m and n, then time complexity = (m + n) * O(1) = O(m + n). Space complexity = O(1), as we are using a constant number of variables.
This strategy is similar to the above approach, but we use a temporary dummy node as the start of the output list. Here dummy->next would be the head node, which we return as an output.
We also use an intersecTail pointer which points to the last node in the output list, so appending new nodes will be easy at the end. The dummy node gives the tail something to point initially when the output list is empty!
ListNode* listIntersection(ListNode* list1, ListNode* list2)
{
ListNode* dummy = new ListNode(0);
ListNode* intersecTail = dummy;
while (list1 != NULL && list2 != NULL)
{
if (list1->data == list2->data)
{
intersecTail->next = new ListNode(list1->data);
intersecTail = intersecTail->next;
list1 = list1->next;
list2 = list2->next;
}
else if (list1->data < list2->data)
list1 = list1->next;
else
list2 = list2->next;
}
return dummy->next;
}
def listIntersection(list1, list2):
dummy = ListNode(0)
intersec_tail = dummy
while list1 is not None and list2 is not None:
if list1.data == list2.data:
intersec_tail.next = ListNode(list1.data)
intersec_tail = intersec_tail.next
list1 = list1.next
list2 = list2.next
elif list1.data < list2.data:
list1 = list1.next
else:
list2 = list2.next
return dummy.next
We are using the loop idea similar to the above approach. So the time and space complexity will be the same in the worst-case. Time complexity = O(m + n) and Space complexity = O(1).
Can we solve the problem using the solution of the smaller problem? Let's think!
Suppose our initial function call is listIntersection(list1, list2), then there will be three scenarios to build the recursive solution:
Base case: During the recursion, if any one of the lists reaches its end node i.e. if (list1 == NULL || list2 == NULL), we return NULL.
// Function to find the intersection of two sorted singly-linked lists
ListNode* listIntersection(ListNode* list1, ListNode* list2)
{
if (list1 == NULL || list2 == NULL)
return NULL;
if (list1->data < list2->data)
return listIntersection(list1->next, list2);
else if (list1->data > list2->data)
return listIntersection(list1, list2->next);
else
{
ListNode* temp = new ListNode(list1->data);
temp->next = listIntersection(list1->next, list2->next);
return temp;
}
}
def listIntersection(list1, list2):
if list1 is None or list2 is None:
return None
if list1.data < list2.data:
return listIntersection(list1.next, list2)
elif list1.data > list2.data:
return listIntersection(list1, list2.next)
else:
temp = ListNode(list1.data)
temp.next = listIntersection(list1.next, list2.next)
return temp
At each step of the recursion, we are performing O(1) operation and input size decreases by 1 or 2. Suppose the length of both linked lists are m and n, then the total number of recursive calls would be m + n in the worst case. So time complexity = O(m + n) * O(1) = O(m + n).
Here is the recurrence relation.
The space complexity depends on the size of the recursion call stack, which depends on the height of the recursion tree. Similarly, the height of the recursion tree depends on the value of m and n i.e. whichever value is larger, recursion tree depth depends on that value. So space complexity = O((max(m , n)). Think!
Please write in the message below if you find anything incorrect, or you want to share more insight. Enjoy learning, Enjoy algorithms!