Problem overview
A closed interval [a, b] contains every real number x with a <= x <= b, ends included. The function intersect(a1: Int, b1: Int, a2: Int, b2: Int): Boolean in class Solution receives two intervals, [a1, b1] and [a2, b2], and returns true when they share at least one number.
The examples:
[1, 6]and[3, 8]share the numbers 3 to 6, so the answer istrue.[1, 4]and[4, 6]share exactly the number 4, so the answer istrue.[1, 8]and[4, 6]overlap because the second lies inside the first, so the answer istrue.[4, 6]and[1, 3]share nothing, so the answer isfalse.
The statement asks for O(1) time and O(1) extra space: a fixed number of comparisons, whatever the size of the numbers.
Approach: describe the misses, then negate
There are many ways two intervals can overlap: partly from the left, partly from the right, one inside the other, or touching at a point. Listing them all is tedious. There are only two ways they can fail to overlap, and they are easy to state:
- The first interval ends before the second begins:
b1 < a2. - The second interval ends before the first begins:
b2 < a1.
If neither is true, the intervals must overlap. So the answer is the negation of "one of the two misses holds". This works because every interval has its start at most its end, so if neither interval lies entirely to the left of the other, they must share a number.
Step by step
- Write the two miss conditions:
b1 < a2andb2 < a1. - Join them with
||. Either miss is enough to separate the intervals, so the combined condition is true whenever they are disjoint. - Wrap the pair in parentheses and negate it with
!. The parentheses make the!apply to the whole combination. Without them, it would try to negate only the first comparison. - Return the result directly. A condition is already a
Boolean, so a singlereturnstatement is enough, with noifand noelse.
The solution in Kotlin
class Solution {
fun intersect(a1: Int, b1: Int, a2: Int, b2: Int): Boolean {
return !(b1 < a2 || b2 < a1)
}
}
Kotlin details worth knowing:
- Returning a Boolean expression. Writing
if (cond) { return true } else { return false }works but is wordy. The comparison already is theBooleanthat the function returns. - Strict
<on purpose. Intervals that only touch, such as[1, 4]and[4, 6], share the number 4. For them,b1 < a2is4 < 4, which isfalse, so they count as intersecting. Using<=would wrongly report a miss. - Short-circuiting. If
b1 < a2istrue, Kotlin skips the second comparison. The result is the same either way. - Same result by De Morgan. The expression is equivalent to
b1 >= a2 && b2 >= a1, which says "each interval ends at or after the other one starts".
Complexity
Time: O(1). The function does at most two comparisons and one negation.
Space: O(1). It creates no collections and keeps no extra data.
Edge cases and common mistakes
- Touching ends.
[1, 3]and[3, 5]intersect at 3, so the answer istrue. This is the case most likely to be wrong with<=. - One interval inside the other.
[1, 8]and[4, 6]returnstrue. A check that only compares the ends of one interval against the other would miss this. - Order of the intervals. Swapping the two intervals does not change the answer, and the condition is symmetric.
- Single points.
[5, 5]and[5, 5]returnstrue, and[5, 5]and[6, 6]returnsfalse. - Using
&&for the misses.b1 < a2 && b2 < a1can never be true for valid intervals, so it would make every pair look like an overlap. The two misses are alternatives, so join them with||. - Forgetting the parentheses.
!b1 < a2 || b2 < a1does not compile, since!would apply tob1, anInt.
Testing
intersect(1, 6, 3, 8)returnstrue.intersect(1, 4, 4, 6)returnstrue.intersect(1, 8, 4, 6)returnstrue.intersect(4, 6, 1, 3)returnsfalse.intersect(1, 3, 5, 9)returnsfalse.