986. Interval List Intersections
Problem Description
You are given two lists of closed intervals: firstList
and secondList
. Each interval is represented as [start, end]
where start
is the beginning of the interval and end
is the ending of the interval.
Key properties of the input:
- Each list contains intervals that are pairwise disjoint (no two intervals in the same list overlap)
- Each list is sorted in order
- A closed interval
[a, b]
includes all valuesx
wherea โค x โค b
Your task is to find all the intersections between intervals from firstList
and intervals from secondList
.
An intersection between two intervals occurs when they share common values. For example:
- The intersection of
[1, 3]
and[2, 4]
is[2, 3]
(the overlapping portion) - If two intervals don't overlap, they have no intersection
You need to return a list containing all the intersection intervals.
Example visualization:
If firstList = [[0,2], [5,10], [13,23], [24,25]]
and secondList = [[1,5], [8,12], [15,24], [25,26]]
, the intersections would be:
[0,2]
and[1,5]
โ intersection is[1,2]
[5,10]
and[1,5]
โ intersection is[5,5]
[5,10]
and[8,12]
โ intersection is[8,10]
[13,23]
and[15,24]
โ intersection is[15,23]
[24,25]
and[15,24]
โ intersection is[24,24]
[24,25]
and[25,26]
โ intersection is[25,25]
The output would be: [[1,2], [5,5], [8,10], [15,23], [24,24], [25,25]]
Intuition
Since both lists are sorted and contain non-overlapping intervals within each list, we can use a two-pointer approach to efficiently find all intersections.
The key insight is that we don't need to check every interval in firstList
against every interval in secondList
. Instead, we can traverse both lists simultaneously using two pointers, i
and j
.
For any two intervals to intersect, they must overlap. The intersection of two intervals [s1, e1]
and [s2, e2]
can be calculated as:
- The start of the intersection is the maximum of the two start points:
max(s1, s2)
- The end of the intersection is the minimum of the two end points:
min(e1, e2)
- If
max(s1, s2) <= min(e1, e2)
, then the intervals intersect
The crucial decision is which pointer to advance after processing a pair of intervals. We should advance the pointer pointing to the interval that ends earlier because:
- If interval A ends before interval B, then A cannot possibly intersect with any intervals that come after B in the other list
- However, B might still intersect with the next interval after A
For example, if we have firstList[i] = [1, 3]
and secondList[j] = [2, 5]
:
- Their intersection is
[2, 3]
- Since
firstList[i]
ends at 3 (earlier than 5), we advancei
- We keep
j
the same because[2, 5]
might still intersect with the next interval infirstList
This greedy approach ensures we find all intersections while only visiting each interval once, giving us an optimal O(m + n)
time complexity where m
and n
are the lengths of the two lists.
Learn more about Two Pointers and Line Sweep patterns.
Solution Approach
The implementation uses a two-pointer technique to traverse both interval lists simultaneously:
Initialization:
- Set two pointers
i = 0
andj = 0
to track positions infirstList
andsecondList
respectively - Create an empty list
ans
to store the intersection results
Main Loop:
The algorithm continues while both pointers are within bounds: while i < len(firstList) and j < len(secondList)
For each iteration:
-
Extract interval boundaries:
s1, e1, s2, e2 = *firstList[i], *secondList[j]
This unpacks the current intervals into their start and end points using Python's unpacking operator.
-
Calculate potential intersection:
l, r = max(s1, s2), min(e1, e2)
l
(left boundary) = maximum of the two start pointsr
(right boundary) = minimum of the two end points
-
Check if intersection exists:
if l <= r: ans.append([l, r])
If the left boundary is less than or equal to the right boundary, we have a valid intersection interval
[l, r]
. -
Advance the appropriate pointer:
if e1 < e2: i += 1 else: j += 1
- If
firstList[i]
ends beforesecondList[j]
, advance pointeri
- Otherwise, advance pointer
j
- This ensures we don't miss any potential intersections with the interval that extends further
- If
Example walkthrough:
For firstList = [[0,2], [5,10]]
and secondList = [[1,5], [8,12]]
:
- Step 1: Compare
[0,2]
and[1,5]
โ intersection[1,2]
, advancei
(since 2 < 5) - Step 2: Compare
[5,10]
and[1,5]
โ intersection[5,5]
, advancej
(since 5 < 10) - Step 3: Compare
[5,10]
and[8,12]
โ intersection[8,10]
, advancei
(since 10 < 12) - Step 4:
i
is out of bounds, loop ends
The algorithm efficiently finds all intersections in O(m + n)
time with O(1)
extra space (excluding the output array).
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Input:
firstList = [[1,4], [6,8]]
secondList = [[2,5], [7,10]]
Goal: Find all intersections between intervals from both lists.
Initialization:
i = 0
,j = 0
(pointers for firstList and secondList)ans = []
(result array)
Step 1: Compare firstList[0] = [1,4]
with secondList[0] = [2,5]
- Extract boundaries:
s1=1, e1=4, s2=2, e2=5
- Calculate intersection:
l = max(1,2) = 2
,r = min(4,5) = 4
- Since
l โค r
(2 โค 4), we have intersection[2,4]
- Add
[2,4]
to result:ans = [[2,4]]
- Compare endpoints:
e1=4 < e2=5
, so advancei
to 1 - Current state:
i=1, j=0
Step 2: Compare firstList[1] = [6,8]
with secondList[0] = [2,5]
- Extract boundaries:
s1=6, e1=8, s2=2, e2=5
- Calculate intersection:
l = max(6,2) = 6
,r = min(8,5) = 5
- Since
l > r
(6 > 5), no intersection exists - Compare endpoints:
e1=8 > e2=5
, so advancej
to 1 - Current state:
i=1, j=1
Step 3: Compare firstList[1] = [6,8]
with secondList[1] = [7,10]
- Extract boundaries:
s1=6, e1=8, s2=7, e2=10
- Calculate intersection:
l = max(6,7) = 7
,r = min(8,10) = 8
- Since
l โค r
(7 โค 8), we have intersection[7,8]
- Add
[7,8]
to result:ans = [[2,4], [7,8]]
- Compare endpoints:
e1=8 < e2=10
, so advancei
to 2 - Current state:
i=2, j=1
Step 4: i=2
is out of bounds (firstList has only 2 elements)
- Loop terminates
Final Result: [[2,4], [7,8]]
The algorithm efficiently found both intersections by advancing the pointer of the interval that ended earlier, ensuring no potential intersections were missed while visiting each interval exactly once.
Solution Implementation
1class Solution:
2 def intervalIntersection(
3 self, firstList: List[List[int]], secondList: List[List[int]]
4 ) -> List[List[int]]:
5 # Initialize two pointers for traversing both lists
6 i = 0 # Pointer for firstList
7 j = 0 # Pointer for secondList
8 result = [] # Store the intersection intervals
9
10 # Process intervals while both lists have remaining elements
11 while i < len(firstList) and j < len(secondList):
12 # Extract start and end points of current intervals
13 start1, end1 = firstList[i]
14 start2, end2 = secondList[j]
15
16 # Calculate the intersection boundaries
17 # The intersection starts at the maximum of both start points
18 intersection_start = max(start1, start2)
19 # The intersection ends at the minimum of both end points
20 intersection_end = min(end1, end2)
21
22 # Check if there's a valid intersection
23 # Valid when start is less than or equal to end
24 if intersection_start <= intersection_end:
25 result.append([intersection_start, intersection_end])
26
27 # Move the pointer of the interval that ends first
28 # This ensures we don't miss any potential intersections
29 if end1 < end2:
30 i += 1 # Move to next interval in firstList
31 else:
32 j += 1 # Move to next interval in secondList
33
34 return result
35
1class Solution {
2 public int[][] intervalIntersection(int[][] firstList, int[][] secondList) {
3 // List to store the intersection intervals
4 List<int[]> intersections = new ArrayList<>();
5
6 // Get the lengths of both interval lists
7 int firstLength = firstList.length;
8 int secondLength = secondList.length;
9
10 // Two pointers to traverse both lists
11 int firstIndex = 0;
12 int secondIndex = 0;
13
14 // Process intervals while both lists have remaining elements
15 while (firstIndex < firstLength && secondIndex < secondLength) {
16 // Find the intersection by taking the maximum start point
17 int intersectionStart = Math.max(firstList[firstIndex][0], secondList[secondIndex][0]);
18
19 // Find the intersection by taking the minimum end point
20 int intersectionEnd = Math.min(firstList[firstIndex][1], secondList[secondIndex][1]);
21
22 // If valid intersection exists (start <= end), add it to the result
23 if (intersectionStart <= intersectionEnd) {
24 intersections.add(new int[] {intersectionStart, intersectionEnd});
25 }
26
27 // Move the pointer of the interval that ends first
28 // This ensures we don't miss any potential intersections
29 if (firstList[firstIndex][1] < secondList[secondIndex][1]) {
30 firstIndex++;
31 } else {
32 secondIndex++;
33 }
34 }
35
36 // Convert the list to a 2D array and return
37 return intersections.toArray(new int[intersections.size()][]);
38 }
39}
40
1class Solution {
2public:
3 vector<vector<int>> intervalIntersection(vector<vector<int>>& firstList, vector<vector<int>>& secondList) {
4 vector<vector<int>> result;
5
6 // Get the sizes of both interval lists
7 int firstSize = firstList.size();
8 int secondSize = secondList.size();
9
10 // Use two pointers to traverse both lists simultaneously
11 int firstIndex = 0;
12 int secondIndex = 0;
13
14 while (firstIndex < firstSize && secondIndex < secondSize) {
15 // Find the intersection by taking the maximum of start points
16 // and minimum of end points
17 int intersectionStart = max(firstList[firstIndex][0], secondList[secondIndex][0]);
18 int intersectionEnd = min(firstList[firstIndex][1], secondList[secondIndex][1]);
19
20 // If there's a valid intersection (start <= end), add it to result
21 if (intersectionStart <= intersectionEnd) {
22 result.push_back({intersectionStart, intersectionEnd});
23 }
24
25 // Move the pointer of the interval that ends first
26 // This ensures we check all possible intersections
27 if (firstList[firstIndex][1] < secondList[secondIndex][1]) {
28 firstIndex++;
29 } else {
30 secondIndex++;
31 }
32 }
33
34 return result;
35 }
36};
37
1/**
2 * Finds the intersection of two lists of intervals
3 * @param firstList - First list of intervals where each interval is [start, end]
4 * @param secondList - Second list of intervals where each interval is [start, end]
5 * @returns List of intersecting intervals
6 */
7function intervalIntersection(firstList: number[][], secondList: number[][]): number[][] {
8 const firstListLength: number = firstList.length;
9 const secondListLength: number = secondList.length;
10 const intersections: number[][] = [];
11
12 // Pointers to track current position in each list
13 let firstIndex: number = 0;
14 let secondIndex: number = 0;
15
16 // Process both lists using two pointers technique
17 while (firstIndex < firstListLength && secondIndex < secondListLength) {
18 // Find the overlapping region by taking the maximum start and minimum end
19 const intersectionStart: number = Math.max(firstList[firstIndex][0], secondList[secondIndex][0]);
20 const intersectionEnd: number = Math.min(firstList[firstIndex][1], secondList[secondIndex][1]);
21
22 // If there's a valid intersection (start <= end), add it to results
23 if (intersectionStart <= intersectionEnd) {
24 intersections.push([intersectionStart, intersectionEnd]);
25 }
26
27 // Move the pointer of the interval that ends first
28 // This ensures we don't miss any potential intersections
29 if (firstList[firstIndex][1] < secondList[secondIndex][1]) {
30 firstIndex++;
31 } else {
32 secondIndex++;
33 }
34 }
35
36 return intersections;
37}
38
Time and Space Complexity
Time Complexity: O(m + n)
where m
is the length of firstList
and n
is the length of secondList
.
The algorithm uses a two-pointer approach, traversing through both lists simultaneously. In each iteration, we either increment pointer i
or pointer j
(never both), moving through the intervals linearly. Since each interval is visited at most once, and we process at most m + n
intervals total, the time complexity is linear in the sum of the input sizes.
Space Complexity: O(1)
for auxiliary space, or O(k)
for total space where k
is the number of intersections.
The algorithm uses only a constant amount of extra space for variables i
, j
, s1
, e1
, s2
, e2
, l
, and r
. However, if we consider the output array ans
as part of the space complexity, it would be O(k)
where k
is the number of intersecting intervals. In the worst case, k
could be min(m, n)
when all intervals from the shorter list intersect with intervals from the longer list.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Pointer Advancement Logic
The Pitfall: A common mistake is advancing both pointers when an intersection is found, or using incorrect logic to decide which pointer to advance:
# WRONG: Advancing both pointers after finding intersection if intersection_start <= intersection_end: result.append([intersection_start, intersection_end]) i += 1 j += 1 # This will miss potential intersections!
Why it's wrong: An interval from one list can intersect with multiple intervals from the other list. By advancing both pointers, you'll miss these additional intersections.
Example where this fails:
firstList = [[1,10]]
,secondList = [[2,3], [4,5], [6,7]]
- The interval
[1,10]
should intersect with all three intervals insecondList
- If you advance both pointers after finding
[2,3]
, you'll miss[4,5]
and[6,7]
The Solution: Always advance the pointer of the interval that ends first:
if end1 < end2: i += 1 else: j += 1
2. Edge Case: Single-Point Intersections
The Pitfall:
Using strict inequality (<
) instead of <=
when checking for valid intersections:
# WRONG: This misses single-point intersections if intersection_start < intersection_end: # Should be <= result.append([intersection_start, intersection_end])
Example where this fails:
firstList = [[1,3]]
,secondList = [[3,5]]
- The intersection should be
[3,3]
(a single point) - With
<
, this valid intersection would be missed
The Solution:
Always use <=
to include single-point intersections:
if intersection_start <= intersection_end: result.append([intersection_start, intersection_end])
3. Forgetting to Handle Empty Lists
The Pitfall: Not considering that one or both input lists might be empty. While the two-pointer approach naturally handles this (the while loop condition fails immediately), some might try to optimize or add unnecessary checks that break this natural handling.
The Solution:
The standard two-pointer approach with while i < len(firstList) and j < len(secondList)
naturally handles empty lists without additional checks.
How would you design a stack which has a function min
that returns the minimum element in the stack, in addition to push
and pop
? All push
, pop
, min
should have running time O(1)
.
Recommended Readings
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
Line Sweep Introduction The primary motivation for this algorithm is for geometry related problems that we encounter in computer science Deeply rooted in geometric problem solving it has become an essential tool for tackling questions related to shapes lines or points on a Cartesian plane At its heart the line
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Donโt Miss This!