Builds on Lower Bound and Upper Bound, which you haven’t finished yet.
Ordered Set (Floor/Ceiling Lookups)
My Calendar I (LC 729)(opens leetcode.com)
Build a calendar, MyCalendar, that takes events one at a time and keeps only those that fit.
An event is a half-open range of time [startTime, endTime): it covers every moment x with startTime <= x < endTime. Two events double book when some moment belongs to both. So an event that ends at 20 and one that starts at 20 share no moment and do not double book.
MyCalendar()creates an empty calendar.boolean book(int startTime, int endTime)adds the event and returnstruewhen it double books with no event already in the calendar. Otherwise it returnsfalseand the calendar stays as it was.
[10, 20) is stored. [15, 25) shares moments 15 to 19 with it, so it is rejected. [20, 30) starts exactly where [10, 20) stops, so they share no moment and it is stored.Ordered Set
sorted keys ➔ bisect position ➔ floor and ceiling neighbours| 1 | import bisect |
| 2 | |
| 3 | |
| 4 | class MyCalendar: |
| 5 | """My Calendar I: keep booked events sorted by start; a new event can only clash with its two neighbours. |
| 6 | Time: O(log N) search + O(N) insert per book, N = bookings so far | Space: O(N) |
| 7 | """ |
| 8 | def __init__(self) -> None: |
| 9 | self.starts: list[int] = [] # Start times of booked events, kept sorted |
| 10 | self.ends: list[int] = [] # ends[j] is the end of the event that starts at starts[j] |
| 11 | |
| 12 | def book(self, startTime: int, endTime: int) -> bool: |
| 13 | i = bisect.bisect_right(self.starts, startTime) # Events before i start at or before startTime |
| 14 | |
| 15 | # 1. Reject An Overlap With Either Neighbour, Else Insert Between Them: |
| 16 | # The floor and ceiling are the only stored events that could possibly overlap the new one, |
| 17 | # since every other event sits sorted further away; both half-open checks must stay strict. |
| 18 | if i > 0 and self.ends[i - 1] > startTime: |
| 19 | return False # Floor neighbour is still running at startTime |
| 20 | if i < len(self.starts) and self.starts[i] < endTime: |
| 21 | return False # Ceiling neighbour starts before endTime |
| 22 | self.starts.insert(i, startTime) # Insert in place: starts stays sorted |
| 23 | self.ends.insert(i, endTime) |
| 24 | return True |
starts holds every stored key in sorted order however the inserts arrive, so i = bisect.bisect_right(self.starts, startTime) puts the floor at i - 1 and the ceiling at i.Keep the stored keys sorted however they arrive, so one bisect_right finds both neighbours of a new value: the floor at i - 1 and the ceiling at i. For a calendar that is all you need: stored events never overlap, so sorted by start they are sorted by end too, and every other event either ends before the floor starts or starts after the ceiling. A new event fits exactly when the floor has ended by startTime and the ceiling starts no earlier than endTime; then it is inserted at i in both starts and ends. The trap is the half-open ends: the checks are self.ends[i - 1] > startTime and self.starts[i] < endTime, strict, because an event ending at 20 and one starting at 20 share no moment.
See it run
An ordered list of starts: floor and ceiling checks
book -i -last result -
Each column is one slot of the sorted lists. The top row is starts, and the bottom row is ends. An empty cell is a slot not used yet.
floorthe stored event at or before the new startceilingthe stored event after the new startnewthe event just inserted
No event is booked yet, so starts and ends are empty. Each book call finds its two neighbours with one bisect_right.
Gotcha 1 of 5
- The trap
Gotcha 1: Writing
>=instead of>in the floor checkMistakeif i > 0 and self.ends[i - 1] >= startTime:Fixif i > 0 and self.ends[i - 1] > startTime:What happens: Events are half-open, so
[10, 20)and[20, 30)share no moment. With>=, an event that ends at 20 blocks one that starts at 20. The ceiling checkself.starts[i] < endTimemust stay strict for the same reason.Example: book(10, 20), then book(20, 30)Mistake: returns FalseFix: returns True
- The keys arrive already sorted, as the timestamps in Time Based Key-Value Store (LC 981) do: appending keeps the list in order, so a binary search for the last stored key at or below the query is enough and nothing is ever inserted in the middle. See Upper Bound
- The question is how many intervals overlap at the busiest moment, as in Meeting Rooms II (LC 253): every event matters at once, not which single stored key sits closest to one new point. See Sweep Line
Deep Dive
One search gives both neighbours
starts stays sorted after every insert. i = bisect.bisect_right(self.starts, startTime) returns the first index whose value is greater than startTime. So every value at index i - 1 or below is at most startTime, and every value at index i or above is greater.
The value at i - 1 is the floor: the largest stored start that is at most startTime. The value at i is the ceiling: the smallest stored start that is greater than startTime. One search, which takes O(log N) time, finds both.
Example: starts = [10, 20] and book(15, 25). The search returns i = 1. The floor is starts[0] = 10, and the ceiling is starts[1] = 20.
Why only the floor and the ceiling can overlap
The calendar holds no overlaps. So when the events are sorted by start, each event ends before the next one starts: ends[j] <= starts[j + 1]. The events are sorted by end too.
Take an event before the floor. It ends no later than the floor starts, and the floor starts at or before startTime. So it ends before the new event starts, and it cannot overlap.
Take an event after the ceiling. It starts no earlier than the ceiling ends, and the ceiling ends after it starts. If the ceiling starts at or after endTime, then so does every later event. So the new event overlaps nothing.
This leaves two checks. The floor must end by startTime, which is self.ends[i - 1] <= startTime. The ceiling must start no earlier than endTime, which is self.starts[i] >= endTime. For the five calls book(10, 20), book(15, 25), book(20, 30), book(5, 10) and book(25, 35), the results are true, false, true, true and false.
Half-open ends: both comparisons are strict
An event [10, 20) includes 10 and excludes 20. The event [20, 30) starts at 20. The two share no moment, so both can be booked.
In the code, a floor that ends at 20 and a new start of 20 must pass. The check self.ends[i - 1] > startTime is 20 > 20, which is false, so the booking continues. With >=, the check would be true and the booking would be rejected by mistake.
The same holds for the ceiling. book(5, 10) when starts = [10, 20]: the check self.starts[i] < endTime is 10 < 10, which is false, so the booking passes.
Equal starts are handled by bisect_right. After book(10, 20), the call book(10, 15) returns i = 1. The floor is [10, 20), and 20 > 10 rejects the booking.
Cost, and a faster structure
bisect_right takes O(log N) time. list.insert shifts every later entry, so it takes O(N) time. One book call costs O(N) time, and the space is O(N).
My Calendar I (LC 729) allows at most 1000 calls, so the lists are fast enough. For many more bookings, use a balanced search tree. Java's TreeMap offers floorKey and ceilingKey. The checks stay the same.
The same floor and ceiling idea solves Data Stream as Disjoint Intervals (LC 352): the new number is compared with the interval before it and the interval after it, and it merges with them when they touch.
Insert only after both checks pass. A rejected event that was inserted first would stay in starts and ends and wrongly block later events.
60-Second Pitch
"The calendar never holds overlapping events, so sorted by start it is also sorted by end. A new event can overlap only the stored event just before it or just after it. I keep starts and ends sorted. One bisect_right gives i. The floor is at i - 1, and the ceiling is at i. I reject the booking if the floor ends after startTime, or if the ceiling starts before endTime. Both comparisons are strict, because the events are half-open. Otherwise I insert at i in both lists. The search is O(log N) and the insert is O(N), which is fine for 1000 calls. For more, I use a balanced tree with floor and ceiling lookups."
Common Questions
- Why is it enough to check only two neighbours?
Stored events do not overlap, so sorted by start they are also sorted by end. Every event before the floor ends before the floor starts. Every event after the ceiling starts after the ceiling ends. If the new event clears the floor and the ceiling, it clears all of them.
- Why
bisect_rightand notbisect_left? bisect_rightputs an event with an equal start before indexi, so that event becomes the floor. Its end is greater thanstartTime, so the floor check rejects the booking. The floor and ceiling have a clear meaning: at moststartTime, and greater thanstartTime.- How would you make the insert fast?
Use a balanced search tree or a skip list, which insert in
O(log N)time and offer floor and ceiling lookups. Java'sTreeMaphasfloorKeyandceilingKey. In Python, use a sorted container from a library when it is allowed. Plain lists areO(N)per insert.- What changes if events may overlap, as in My Calendar II (LC 731)?
Two neighbours are no longer enough, because several stored events can overlap the new one. A new event is rejected only when it overlaps a moment that two stored events already cover. Keep the booked events in one list and the intervals that two events cover in a second list, and check the new event against both lists.
- Why keep
endsas a second list? The floor check needs the end of the floor event,
ends[i - 1]. The two lists share one index, so the end of the event atstarts[j]isends[j]. Both inserts use the same indexi.
SWE Reasoning
Spot it
- Keys arrive in any order, and between inserts a question asks for the closest stored key at or below a value, or above it.
- A new item can conflict only with its nearest neighbours, because the stored items never conflict with each other.
- The structure must stay sorted. Sorting again for every query costs
O(N log N)time each. - The intervals are half-open,
[start, end), so touching ends are allowed.
The invariant, step by step
startsis sorted, andends[j]is the end of the event that starts atstarts[j].i = bisect.bisect_right(self.starts, startTime)splitsstartsinto values at moststartTimeand values greater thanstartTime.- The floor is at
i - 1. Ifself.ends[i - 1] > startTime, the floor event is still running atstartTime, and the function returns False. - The ceiling is at
i. Ifself.starts[i] < endTime, the ceiling event starts before the new event ends, and the function returns False. - Stored events do not overlap, so no other stored event can overlap the new one.
- Both checks passed, so the function inserts
startTimeintostartsandendTimeintoendsat the same indexi. Both lists stay sorted and aligned.
Traps
>=in the floor check. An event that ends at 20 then blocks one that starts at 20.book(10, 20)followed bybook(20, 30)returns False and should return True. The ceiling check<must stay strict for the same reason.- Only one neighbour. For
book(10, 20)followed bybook(5, 11), there is no floor, and only the ceiling check rejects it. Checking only the floor returns True. appendforends.ends[j]must pair withstarts[j].appendputs the end in the last slot while the start went to indexi, so later checks read the wrong end.- Insert before the checks. A rejected event that stays in the lists blocks later events that overlap only the rejected one.
- Assuming
O(log N)insert.list.insertisO(N). Say this in an interview, and name a balanced tree as the fix.
Practice
Builds on Lower Bound and Upper Bound, which you haven’t finished yet.
Ordered Set (Floor/Ceiling Lookups)
My Calendar I (LC 729)(opens leetcode.com)
Build a calendar, MyCalendar, that takes events one at a time and keeps only those that fit.
An event is a half-open range of time [startTime, endTime): it covers every moment x with startTime <= x < endTime. Two events double book when some moment belongs to both. So an event that ends at 20 and one that starts at 20 share no moment and do not double book.
MyCalendar()creates an empty calendar.boolean book(int startTime, int endTime)adds the event and returnstruewhen it double books with no event already in the calendar. Otherwise it returnsfalseand the calendar stays as it was.
[10, 20) is stored. [15, 25) shares moments 15 to 19 with it, so it is rejected. [20, 30) starts exactly where [10, 20) stops, so they share no moment and it is stored.Ordered Set
sorted keys ➔ bisect position ➔ floor and ceiling neighbours| 1 | import bisect |
| 2 | |
| 3 | |
| 4 | class MyCalendar: |
| 5 | """My Calendar I: keep booked events sorted by start; a new event can only clash with its two neighbours. |
| 6 | Time: O(log N) search + O(N) insert per book, N = bookings so far | Space: O(N) |
| 7 | """ |
| 8 | def __init__(self) -> None: |
| 9 | self.starts: list[int] = [] # Start times of booked events, kept sorted |
| 10 | self.ends: list[int] = [] # ends[j] is the end of the event that starts at starts[j] |
| 11 | |
| 12 | def book(self, startTime: int, endTime: int) -> bool: |
| 13 | i = bisect.bisect_right(self.starts, startTime) # Events before i start at or before startTime |
| 14 | |
| 15 | # 1. Reject An Overlap With Either Neighbour, Else Insert Between Them: |
| 16 | # The floor and ceiling are the only stored events that could possibly overlap the new one, |
| 17 | # since every other event sits sorted further away; both half-open checks must stay strict. |
| 18 | if i > 0 and self.ends[i - 1] > startTime: |
| 19 | return False # Floor neighbour is still running at startTime |
| 20 | if i < len(self.starts) and self.starts[i] < endTime: |
| 21 | return False # Ceiling neighbour starts before endTime |
| 22 | self.starts.insert(i, startTime) # Insert in place: starts stays sorted |
| 23 | self.ends.insert(i, endTime) |
| 24 | return True |
starts holds every stored key in sorted order however the inserts arrive, so i = bisect.bisect_right(self.starts, startTime) puts the floor at i - 1 and the ceiling at i.Keep the stored keys sorted however they arrive, so one bisect_right finds both neighbours of a new value: the floor at i - 1 and the ceiling at i. For a calendar that is all you need: stored events never overlap, so sorted by start they are sorted by end too, and every other event either ends before the floor starts or starts after the ceiling. A new event fits exactly when the floor has ended by startTime and the ceiling starts no earlier than endTime; then it is inserted at i in both starts and ends. The trap is the half-open ends: the checks are self.ends[i - 1] > startTime and self.starts[i] < endTime, strict, because an event ending at 20 and one starting at 20 share no moment.
See it run
An ordered list of starts: floor and ceiling checks
book -i -last result -
Each column is one slot of the sorted lists. The top row is starts, and the bottom row is ends. An empty cell is a slot not used yet.
floorthe stored event at or before the new startceilingthe stored event after the new startnewthe event just inserted
No event is booked yet, so starts and ends are empty. Each book call finds its two neighbours with one bisect_right.
Gotcha 1 of 5
- The trap
Gotcha 1: Writing
>=instead of>in the floor checkMistakeif i > 0 and self.ends[i - 1] >= startTime:Fixif i > 0 and self.ends[i - 1] > startTime:What happens: Events are half-open, so
[10, 20)and[20, 30)share no moment. With>=, an event that ends at 20 blocks one that starts at 20. The ceiling checkself.starts[i] < endTimemust stay strict for the same reason.Example: book(10, 20), then book(20, 30)Mistake: returns FalseFix: returns True
- The keys arrive already sorted, as the timestamps in Time Based Key-Value Store (LC 981) do: appending keeps the list in order, so a binary search for the last stored key at or below the query is enough and nothing is ever inserted in the middle. See Upper Bound
- The question is how many intervals overlap at the busiest moment, as in Meeting Rooms II (LC 253): every event matters at once, not which single stored key sits closest to one new point. See Sweep Line
Deep Dive
One search gives both neighbours
starts stays sorted after every insert. i = bisect.bisect_right(self.starts, startTime) returns the first index whose value is greater than startTime. So every value at index i - 1 or below is at most startTime, and every value at index i or above is greater.
The value at i - 1 is the floor: the largest stored start that is at most startTime. The value at i is the ceiling: the smallest stored start that is greater than startTime. One search, which takes O(log N) time, finds both.
Example: starts = [10, 20] and book(15, 25). The search returns i = 1. The floor is starts[0] = 10, and the ceiling is starts[1] = 20.
Why only the floor and the ceiling can overlap
The calendar holds no overlaps. So when the events are sorted by start, each event ends before the next one starts: ends[j] <= starts[j + 1]. The events are sorted by end too.
Take an event before the floor. It ends no later than the floor starts, and the floor starts at or before startTime. So it ends before the new event starts, and it cannot overlap.
Take an event after the ceiling. It starts no earlier than the ceiling ends, and the ceiling ends after it starts. If the ceiling starts at or after endTime, then so does every later event. So the new event overlaps nothing.
This leaves two checks. The floor must end by startTime, which is self.ends[i - 1] <= startTime. The ceiling must start no earlier than endTime, which is self.starts[i] >= endTime. For the five calls book(10, 20), book(15, 25), book(20, 30), book(5, 10) and book(25, 35), the results are true, false, true, true and false.
Half-open ends: both comparisons are strict
An event [10, 20) includes 10 and excludes 20. The event [20, 30) starts at 20. The two share no moment, so both can be booked.
In the code, a floor that ends at 20 and a new start of 20 must pass. The check self.ends[i - 1] > startTime is 20 > 20, which is false, so the booking continues. With >=, the check would be true and the booking would be rejected by mistake.
The same holds for the ceiling. book(5, 10) when starts = [10, 20]: the check self.starts[i] < endTime is 10 < 10, which is false, so the booking passes.
Equal starts are handled by bisect_right. After book(10, 20), the call book(10, 15) returns i = 1. The floor is [10, 20), and 20 > 10 rejects the booking.
Cost, and a faster structure
bisect_right takes O(log N) time. list.insert shifts every later entry, so it takes O(N) time. One book call costs O(N) time, and the space is O(N).
My Calendar I (LC 729) allows at most 1000 calls, so the lists are fast enough. For many more bookings, use a balanced search tree. Java's TreeMap offers floorKey and ceilingKey. The checks stay the same.
The same floor and ceiling idea solves Data Stream as Disjoint Intervals (LC 352): the new number is compared with the interval before it and the interval after it, and it merges with them when they touch.
Insert only after both checks pass. A rejected event that was inserted first would stay in starts and ends and wrongly block later events.
60-Second Pitch
"The calendar never holds overlapping events, so sorted by start it is also sorted by end. A new event can overlap only the stored event just before it or just after it. I keep starts and ends sorted. One bisect_right gives i. The floor is at i - 1, and the ceiling is at i. I reject the booking if the floor ends after startTime, or if the ceiling starts before endTime. Both comparisons are strict, because the events are half-open. Otherwise I insert at i in both lists. The search is O(log N) and the insert is O(N), which is fine for 1000 calls. For more, I use a balanced tree with floor and ceiling lookups."
Common Questions
- Why is it enough to check only two neighbours?
Stored events do not overlap, so sorted by start they are also sorted by end. Every event before the floor ends before the floor starts. Every event after the ceiling starts after the ceiling ends. If the new event clears the floor and the ceiling, it clears all of them.
- Why
bisect_rightand notbisect_left? bisect_rightputs an event with an equal start before indexi, so that event becomes the floor. Its end is greater thanstartTime, so the floor check rejects the booking. The floor and ceiling have a clear meaning: at moststartTime, and greater thanstartTime.- How would you make the insert fast?
Use a balanced search tree or a skip list, which insert in
O(log N)time and offer floor and ceiling lookups. Java'sTreeMaphasfloorKeyandceilingKey. In Python, use a sorted container from a library when it is allowed. Plain lists areO(N)per insert.- What changes if events may overlap, as in My Calendar II (LC 731)?
Two neighbours are no longer enough, because several stored events can overlap the new one. A new event is rejected only when it overlaps a moment that two stored events already cover. Keep the booked events in one list and the intervals that two events cover in a second list, and check the new event against both lists.
- Why keep
endsas a second list? The floor check needs the end of the floor event,
ends[i - 1]. The two lists share one index, so the end of the event atstarts[j]isends[j]. Both inserts use the same indexi.
SWE Reasoning
Spot it
- Keys arrive in any order, and between inserts a question asks for the closest stored key at or below a value, or above it.
- A new item can conflict only with its nearest neighbours, because the stored items never conflict with each other.
- The structure must stay sorted. Sorting again for every query costs
O(N log N)time each. - The intervals are half-open,
[start, end), so touching ends are allowed.
The invariant, step by step
startsis sorted, andends[j]is the end of the event that starts atstarts[j].i = bisect.bisect_right(self.starts, startTime)splitsstartsinto values at moststartTimeand values greater thanstartTime.- The floor is at
i - 1. Ifself.ends[i - 1] > startTime, the floor event is still running atstartTime, and the function returns False. - The ceiling is at
i. Ifself.starts[i] < endTime, the ceiling event starts before the new event ends, and the function returns False. - Stored events do not overlap, so no other stored event can overlap the new one.
- Both checks passed, so the function inserts
startTimeintostartsandendTimeintoendsat the same indexi. Both lists stay sorted and aligned.
Traps
>=in the floor check. An event that ends at 20 then blocks one that starts at 20.book(10, 20)followed bybook(20, 30)returns False and should return True. The ceiling check<must stay strict for the same reason.- Only one neighbour. For
book(10, 20)followed bybook(5, 11), there is no floor, and only the ceiling check rejects it. Checking only the floor returns True. appendforends.ends[j]must pair withstarts[j].appendputs the end in the last slot while the start went to indexi, so later checks read the wrong end.- Insert before the checks. A rejected event that stays in the lists blocks later events that overlap only the rejected one.
- Assuming
O(log N)insert.list.insertisO(N). Say this in an interview, and name a balanced tree as the fix.