Interval modeling
Modelling intervals precisely: closed versus open and half-open endpoints, what overlap means, normalising input and sorting endpoints.
Learning goals
- Choose between closed, open and half-open intervals and state what each implies for touching endpoints
- Write the overlap test and the intersection of two intervals correctly
- Normalise input into one convention before running an algorithm
- Pick a sort key and tie-breaker that suits the algorithm
Prerequisites
Almost every interval bug is an off-by-one at an endpoint. Deciding the convention first, and writing it down, removes most of them before any algorithm runs.
Closed vs open intervals
A **closed** interval [a, b] contains both endpoints; an **open** interval (a, b) contains neither. Two closed intervals that merely touch, such as [1, 4] and [4, 7], share the single point 4, so they overlap. Two open intervals (1, 4) and (4, 7) share nothing.
Run the first two presets of the animation on the same data, 1-4 and 4-7. Under the closed definition they overlap and the shared part is the single point [4, 4]; under the half-open definition they do not. The data did not change, only the meaning of the endpoints, and the answer flipped.
Closed intervals suit integer or discrete data (days 3 through 5 inclusive, line numbers). Open intervals mostly appear in mathematics; programs almost never need them.
1static boolean overlapsClosed(int[] a, int[] b) {2return a[0] <= b[1] && b[0] <= a[1]; // touching endpoints share a point3}45static boolean overlapsOpen(int[] a, int[] b) {6return a[0] < b[1] && b[0] < a[1]; // touching endpoints share nothing7}
Do two intervals overlap? Closed, half-open and open
Interactive · edit the inputI1
I2
Two intervals overlap when they share at least one point (or, for the open conventions, a stretch of positive length). The convention for the endpoints decides what touching ends mean: closed intervals include both ends, so touching counts.
1static boolean overlaps(int a1, int b1, int a2, int b2, int kind) { // kind 0: closed, 1: half-open, 2: open2if (kind == 0) return a1 <= b2 && a2 <= b1; // closed: sharing a single point counts as overlap3return a1 < b2 && a2 < b1; // half-open and open: touching ends do not overlap4}
Live state
convention
Closed [a, b]
Execution trace (dry run table)Show 4 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | convention=Closed [a, b] | Two intervals overlap when they share at least one point (or, for the open conventions, a stretch of positive length). The convention for the endpoints decides what touching ends mean: closed intervals include both ends, so touching counts. |
| 2 | L2 | a1=1 b2=7 | The first interval starts before the second one ends. |
| 3 | L2 | a2=4 b1=4 | The second interval starts before the first one ends. |
| 4 | L2 | return=true intersection=[4, 4] | They overlap; the shared part is [4, 4] (a single point). |
Half-open intervals
A **half-open** interval [a, b) contains a but not b. It is the convention of most programming: substring(from, to), Arrays.copyOfRange(a, from, to), List.subList(from, to) and for (int i = from; i < to; i++) are all half-open. Its advantages are exact: the length is b - a, an empty interval is a == b, adjacent intervals [1, 4) and [4, 7) tile without a gap or overlap, and splitting [a, c) at b gives [a, b) and [b, c) with no endpoint counted twice.
Meeting schedules are naturally half-open: a meeting from 9:00 to 10:00 and another from 10:00 to 11:00 may share a room. That is why the “Touching: half-open does not” preset reports no overlap, and why the meeting-room algorithms in this chapter treat an end as happening before a start at the same time.
1static void halfOpenInJava() {2String s = "abcdefgh";3String part = s.substring(2, 5); // half-open [2, 5): characters at 2, 3 and 44System.out.println(part); // cde5System.out.println(part.length() == 5 - 2); // length is end - start6// adjacent half-open ranges tile exactly7System.out.println(s.substring(0, 2) + s.substring(2, 5) + s.substring(5, 8)); // abcdefgh8}
Do two intervals overlap? Closed, half-open and open
Interactive · edit the inputI1
I2
Two intervals overlap when they share at least one point (or, for the open conventions, a stretch of positive length). The convention for the endpoints decides what touching ends mean: closed intervals include both ends, so touching counts.
1static boolean overlaps(int a1, int b1, int a2, int b2, int kind) { // kind 0: closed, 1: half-open, 2: open2if (kind == 0) return a1 <= b2 && a2 <= b1; // closed: sharing a single point counts as overlap3return a1 < b2 && a2 < b1; // half-open and open: touching ends do not overlap4}
Live state
convention
Closed [a, b]
Execution trace (dry run table)Show 4 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | convention=Closed [a, b] | Two intervals overlap when they share at least one point (or, for the open conventions, a stretch of positive length). The convention for the endpoints decides what touching ends mean: closed intervals include both ends, so touching counts. |
| 2 | L2 | a1=1 b2=7 | The first interval starts before the second one ends. |
| 3 | L2 | a2=4 b1=4 | The second interval starts before the first one ends. |
| 4 | L2 | return=true intersection=[4, 4] | They overlap; the shared part is [4, 4] (a single point). |
Overlap definitions
Two intervals relate in one of a few ways. They are **disjoint** when one ends before the other starts, they **touch** when one's end equals the other's start, they **partially overlap**, one can be **nested** inside the other, or they are **equal**. One formula covers every case: two intervals overlap exactly when max(starts) < min(ends) for half-open intervals, or max(starts) <= min(ends) for closed ones, and in that case the intersection is [max(starts), min(ends)].
The negation is easier to remember: two intervals are disjoint exactly when one ends before the other starts. The animation's “Nested” preset shows the shared part [3, 5] being the whole inner interval, and “Clear overlap” shows [4, 6].
| Case | Example | Closed overlap? | Half-open overlap? |
|---|---|---|---|
| Disjoint | [1, 2] and [3, 4] | No | No |
| Touching | [1, 4] and [4, 7] | Yes (the point 4) | No |
| Partial overlap | [1, 6] and [4, 9] | Yes, [4, 6] | Yes, [4, 6) |
| Nested | [0, 10] and [3, 5] | Yes, [3, 5] | Yes, [3, 5) |
| Equal | [2, 5] and [2, 5] | Yes | Yes |
1// half-open intersection; returns null when the intervals do not overlap2static int[] intersect(int[] a, int[] b) {3int start = Math.max(a[0], b[0]);4int end = Math.min(a[1], b[1]);5return start < end ? new int[]{start, end} : null;6}
Do two intervals overlap? Closed, half-open and open
Interactive · edit the inputI1
I2
Two intervals overlap when they share at least one point (or, for the open conventions, a stretch of positive length). The convention for the endpoints decides what touching ends mean: closed intervals include both ends, so touching counts.
1static boolean overlaps(int a1, int b1, int a2, int b2, int kind) { // kind 0: closed, 1: half-open, 2: open2if (kind == 0) return a1 <= b2 && a2 <= b1; // closed: sharing a single point counts as overlap3return a1 < b2 && a2 < b1; // half-open and open: touching ends do not overlap4}
Live state
convention
Closed [a, b]
Execution trace (dry run table)Show 4 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | convention=Closed [a, b] | Two intervals overlap when they share at least one point (or, for the open conventions, a stretch of positive length). The convention for the endpoints decides what touching ends mean: closed intervals include both ends, so touching counts. |
| 2 | L2 | a1=1 b2=7 | The first interval starts before the second one ends. |
| 3 | L2 | a2=4 b1=4 | The second interval starts before the first one ends. |
| 4 | L2 | return=true intersection=[4, 4] | They overlap; the shared part is [4, 4] (a single point). |
Interval normalization
Real input is messy: endpoints may come reversed (start after end), empty intervals appear, and the same data may be given as inclusive ranges while the algorithm expects half-open. **Normalisation** converts every input into one clean convention before anything else runs: swap reversed endpoints (or reject them), convert inclusive [a, b] to half-open [a, b + 1), drop empty intervals, and optionally sort.
Do it at the boundary of your code, once, and write the rest of the program for half-open data only. Mixing conventions is where < and <= bugs come from.
1// inclusive [a, b] pairs in any order -> sorted, non-empty half-open [a, b+1) pairs2static int[][] normalize(int[][] inclusive) {3List<int[]> out = new ArrayList<>();4for (int[] r : inclusive) {5int lo = Math.min(r[0], r[1]);6int hi = Math.max(r[0], r[1]);7out.add(new int[]{lo, hi + 1}); // half-open: every integer range now has length >= 18}9out.sort(Comparator.comparingInt(x -> x[0]));10return out.toArray(new int[0][]);11}
Sorting endpoints
Almost every interval algorithm starts by sorting, and the sort key follows from the question. **Sort by start** when you sweep left to right and build things up (merging, inserting, intersections). **Sort by end** when you want to keep the interval that frees the line soonest (activity selection, minimum arrows). **Sort the endpoints themselves** as separate events when you only need counts (maximum overlap).
A tie-breaker is needed when keys are equal. For merging, ties on start do not change the result. For event sorting, the order of an end and a start at the same time decides whether touching intervals overlap, and the next lesson group builds on exactly that point. When intervals have equal starts and you want the longest one first, sort by start ascending and end descending.
1// by start ascending; for equal starts, longer interval first2static void sortByStartThenLongest(int[][] a) {3Arrays.sort(a, (x, y) -> x[0] != y[0] ? Integer.compare(x[0], y[0]) : Integer.compare(y[1], x[1]));4}56// by end ascending; for equal ends, later start first (the shorter interval)7static void sortByEnd(int[][] a) {8Arrays.sort(a, (x, y) -> x[1] != y[1] ? Integer.compare(x[1], y[1]) : Integer.compare(y[0], x[0]));9}
Merge overlapping intervals (sort by start, then scan)
Interactive · edit the inputinput
In input order, overlaps can be far apart. Sorting by start puts every overlap next to its neighbour.
1public static List<int[]> merge(int[][] intervals) {2Arrays.sort(intervals, Comparator.comparingInt(a -> a[0])); // sort by start3List<int[]> merged = new ArrayList<>();4for (int[] cur : intervals) {5int[] last = merged.isEmpty() ? null : merged.get(merged.size() - 1);6if (last != null && cur[0] <= last[1]) {7last[1] = Math.max(last[1], cur[1]); // overlap: extend8} else {9merged.add(cur); // gap: start a new interval10}11}12return merged;13}
Live state
intervals
5
Execution trace (dry run table)Show 13 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | intervals=5 | In input order, overlaps can be far apart. Sorting by start puts every overlap next to its neighbour. |
| 2 | L2 | intervals=5 | Sorted by start: O(n log n). Now one left-to-right scan is enough. |
| 3 | L6 | start=1 end=3 last end=none | Nothing merged yet: this interval starts the list. |
| 4 | L9 | merged=1 | Append [1, 3] as a new merged interval. |
| 5 | L6 | start=2 end=6 last end=3 | [2, 6] starts before the last merged interval ends, so they overlap. |
| 6 | L7 | last end=6 | Extend the last interval: end = max(3, 6) = 6. |
| 7 | L6 | start=8 end=10 last end=6 | [8, 10] starts after 6: there is a gap. |
| 8 | L9 | merged=2 | Append [8, 10] as a new merged interval. |
| 9 | L6 | start=9 end=12 last end=10 | [9, 12] starts before the last merged interval ends, so they overlap. |
| 10 | L7 | last end=12 | Extend the last interval: end = max(10, 12) = 12. |
| 11 | L6 | start=15 end=18 last end=12 | [15, 18] starts after 12: there is a gap. |
| 12 | L9 | merged=3 | Append [15, 18] as a new merged interval. |
| 13 | L12 | return=[1,6] [8,12] [15,18] | 5 intervals became 3. O(n log n) for the sort plus O(n) for the scan. |
Practice ladder
Now try it yourself
Easy
- Meeting Rooms
- Summary Ranges
Medium
- Merge Intervals
- Interval List Intersections
Advanced
- Data Stream as Disjoint Intervals
- Range Module
Key takeaways
- Decide whether endpoints are closed or half-open before writing any comparison.
- Half-open [start, end) is the convention of most programming: length is end - start and neighbours tile exactly.
- Intervals overlap when max(starts) < min(ends) (half-open), and the intersection is [max(starts), min(ends)).
- Normalise to one convention at the input boundary.
- Sort by start to build, by end to select, and use sorted events to count.