Binary Search Trees · 18 of 22
Given n appointments, find the conflicting appointments
Go through appointments in order and say which earlier one each clashes with, using a sorted tree where every spot also remembers the latest finishing time below it.
- Simple way
- time O(n^2)
room O(1) - Clever way
- time O(n h)
room O(n)
The problem, simply
You get n appointments in the order they were booked. Each has a start time and an end time. For every appointment, find an earlier booked appointment that it clashes with, if there is one.
Two appointments clash when they overlap: a.start < b.end and b.start < a.end. One ending exactly when the other starts is not a clash.
A story to remember it
Picture it
The six bookings on a time ruler, and the notebook tree filed by start time. Each note says the booking and the latest finish below that page.
Examples
The classic list
- In
[1,5], [3,7], [2,6], [10,15], [5,6], [4,100]- Out
[3,7] with [1,5]; [2,6] with [1,5]; [5,6] with [3,7]; [4,100] with [1,5]
[10,15] clashes with nothing booked before it. [5,6] does not clash with [1,5], because 5 to 5 is only touching.
No clashes
- In
[1,2], [2,3], [3,4]- Out
none
Each one starts exactly when the one before ends.
One long booking
- In
[0,10], [1,2], [3,4], [5,6]- Out
[1,2], [3,4] and [5,6] each with [0,10]
The first booking covers all the others.
Only one booking
- In
[4,8]- Out
none
There is nothing earlier to clash with.
Way 1: the simple way
For each appointment, look at every earlier one.
- For
ifrom 1 ton - 1, and for everyj < i: - If
a[j]anda[i]overlap, reporta[i]clashes witha[j]and stop looking for thisi.
import java.util.*;
class Solution {
// returns, for each booking, the index of an earlier clashing booking, or -1
public int[] findConflicts(int[][] appts) {
int n = appts.length;
int[] clash = new int[n];
Arrays.fill(clash, -1);
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
if (appts[j][0] < appts[i][1] && appts[i][0] < appts[j][1]) {
clash[i] = j; // first earlier booking that overlaps
break;
}
}
}
return clash;
}
}How long it takes: up to n * n / 2 checks, O(n^2). Extra room besides the answer: O(1).
Way 2: the clever way
Keep the earlier bookings in a BST filed by start time, where every spot also stores max: the latest end time in its part of the tree. This is called an interval tree.
To check a new booking [s, e], walk down from the top:
- If the current spot's booking overlaps
[s, e], report it. - Else, if the left child exists and its
max > s, go left. Something on the left still runs aftersstarts. - Else go right. Everything on the left ends by
s, so none of it can clash.
Then insert [s, e] like a normal BST insert by start, and raise max on every spot along the way.
Why going left is safe: if the left side's max > s but nothing there overlaps, then the booking with that late end must start at or after e. Everything on the right starts even later, so the right side cannot overlap either.
Check each booking, then file it
01 / 06
Booking 1-5. The notebook is empty, so no clash. File it as the top page. max is 5.
- clash
- none
- looking now
- seen
- rejected
- path
- just added
- ignored
import java.util.*;
class Solution {
static class Spot {
int start, end, max, index; // max: latest end in my part of the tree
Spot left, right;
Spot(int s, int e, int i) { start = s; end = e; max = e; index = i; }
}
public int[] findConflicts(int[][] appts) {
int[] clash = new int[appts.length];
Spot root = null;
for (int i = 0; i < appts.length; i++) {
Spot hit = overlap(root, appts[i][0], appts[i][1]);
clash[i] = (hit == null) ? -1 : hit.index;
root = insert(root, appts[i][0], appts[i][1], i);
}
return clash;
}
private Spot overlap(Spot node, int s, int e) {
while (node != null) {
if (node.start < e && s < node.end) return node; // clash here
if (node.left != null && node.left.max > s) node = node.left; // left still runs after s
else node = node.right; // left is all over by s
}
return null;
}
private Spot insert(Spot node, int s, int e, int i) {
if (node == null) return new Spot(s, e, i);
if (s < node.start) node.left = insert(node.left, s, e, i);
else node.right = insert(node.right, s, e, i);
node.max = Math.max(node.max, e); // keep max up to date
return node;
}
}How long it takes: each booking walks one road to check and one road to insert, O(h), so O(n h) in total. With a self balancing tree, h stays about log n, which gives O(n log n). With a plain BST and bookings that arrive sorted by start, the tree becomes a stick and it drops to O(n^2). Extra room: one spot per booking, O(n).
Watch out for
- Touching is not overlapping here. Use strict
<in both parts of the check. If the problem says touching counts, change both to<=. - Forgetting to raise
maxon the way down during insert. Then the left or right choice is made with stale numbers. - This finds one clash per booking, not all of them. Listing all clashes needs to search both sides when both can hold overlaps, which costs
O(h + k)forkanswers.
What interviewers ask next
- Can a person attend all meetings (LeetCode 252)? Sort by start and check each pair of neighbors.
O(n log n). - Book a calendar online, refusing clashes (LeetCode 729). Use a
TreeMapfrom start to end: check the booking just before (floorKey) and just after (ceilingKey).O(log n)per booking. - List every clashing pair. Sort by start, then sweep, keeping a min heap of end times of bookings still running. Each new booking clashes with everyone in the heap whose end is after its start.
O(n log n + k). - How many rooms are needed (LeetCode 253)? Sweep starts and ends in time order. The most bookings running at once is the answer.
Remember this
Grown-up words: the notebook is an interval tree (a BST augmented with the max end of each subtree), a clash is an interval overlap, walking one road is an O(h) search, and keeping max correct during insert is maintaining the augmentation.