You are given two lists of intervals, A
and B
.
In A
, the intervals are sorted by their starting points. None of the intervals within A
overlap.
Likewise, in B
, the intervals are sorted by their starting points. None of the intervals within B
overlap.
Return the intervals that overlap between the two lists.
Example:
A: {[0,4], [7,12]}
B: {[1,3], [5,8], [9,11]}
Return:
{[1,3], [7,8], [9,11]}
I got this in an interview and was stumped.
I thought of comparing intervals between the two lists. If there is an overlap between the two, add the overlap to a results list. I then advance the pointer of the list with the smaller starting interval but was unable to get to a working solution by the end of the interview.
What is the best way to solve this problem?
Here is an implementation, following the roman principle divide-et-impera:
First, find a method, which, for a given pair of Intervals, finds the overlap, if any.
That was the method with a limited responsibility, to decide for two Intervals.
If you're not familiar with Scala: Interval is a class, and the first line can be read as constructor. Lower and upper should be self explaining (of type Int). The class has a method overlap, which takes a second instance of the class (other) and returns, as a result, a new Interval of the overlapping. But wrapped into an Option, which means: If no overlap is found, we return None. If we find one, it is Some (Interval). You can help yourself to understand this construct as a List, which is either empty, or contains exactly one element. It's a technique to avoid NullpointerException, by signalling, that there might be no result, with a Type.
If the upper of the one Interval is lower than the lower of the other interval, there can't be an overlap, so we return None.
For the overlap, we take the maximum of the two lower bounds as lower bound, and the minimum of the two upper bounds, as new upper bound.
Data:
Naive approach: Bubble overlap (first make it work, then make it fast):
The core, if you're not used to Option/Maybe with Some(T) and None, which might help to understand, is:
The first flatten combined the two inner Lists into one List, and the second flatten removed the Nones and left us with Intervals, instead of the wrapper Some(Interval).
Maybe I can come up with an iterative solution, which takes no interval more than 2 times more often, than it matches. ...(10 min later)... Here it is:
The first two inner lines check, if either of the Lists is empty - then no more overlap can be expected.
(a :: as, b :: bs) is a match of (l1, l2) a is the head of l1, as the tail of l1 (might be Nil) and analog b is the head of l2, bs its tail.
If a.lower is > b.upper, we take the tail of the b list and repeat recursively with the whole l1-list and similar, with the whole l2-List but only the tail of the l1 list, if b.lower > a.upper.
Else we should have an overlap so we take a.overlap (b) in either case, with the whole list of the one with higher upper bound, and the tail of the other list.
You see, no None was generated, and for findOverlaps (b, a) the same result.
Here is an implementation of the algorithm that I used as a component for a complicated reduce-step in an apache-spark program: link to another related answer. Curiously, it's also in Scala.
Here is the algorithm in isolation:
It works very similar to the
merge
step of a merge sort, but instead of looking at single numbers, you have to look at entire intervals. The principle remains the same, only the case-distinctions become quite nasty.EDIT: It's not quite that. You want intersection, the algorithm here produces the union. You would either have to flip quite a few
if
-else
-conditions andmin
-max
-functions, or you would have to preprocess / postprocess using the de-Morgan laws. The principle is still the same, but I definitely don't want to repeat this whole exercise for intersections. Regard it not as a shortcoming, but as a feature of the answer: no spoilers ;)So you have two lists with events - entering interval and leaving interval. Merge these lists keeping current state as integer 0, 1, 2 (active interval count)
Treat ties (when values are equal [0..1] and [1..2]) corresponding to chosen rules - treat closing event before opening one if such intervals should not give an intersection