My Calendar I (LeetCode 729)
You will see how keeping booked events sorted by start means a new event only has to be checked against the two events next to it.
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.
Worked Examples
["MyCalendar", "book", "book", "book"]
[[], [10, 20], [15, 25], [20, 30]][null, true, false, true]⚖️Formal Constraints & Bounds
0 <= start < end <= 109At most
1000calls will be made tobook.
Why It Works & Core Invariant
Stored events never overlap each other, so once they are sorted by start their ends are sorted too. A new event can then only clash with the stored event that starts just before it (the floor) or the one that starts just after it (the ceiling): every other stored event is further away than one of those two.
Real-World Scenario & Production Applications
A meeting-room or equipment booking system accepts a reservation only if the room is free for the whole slot. Reservations arrive in any order, so the system keeps them sorted by start time and compares a new one with the reservation just before it and the one just after it, never with the whole schedule.
Step-by-Step Execution Trace Table
LeetCode's calls book(10, 20), book(15, 25), book(20, 30):
| Call | starts / ends before | i = bisect_right(starts, startTime) | Floor check ends[i - 1] > startTime | Ceiling check starts[i] < endTime | Returns |
|---|---|---|---|---|---|
book(10, 20) | [] / [] | 0 | no floor | no ceiling | true, insert at 0 |
book(15, 25) | [10] / [20] | 1 | 20 > 15: clash | false | |
book(20, 30) | [10] / [20] | 1 | 20 > 20 is false | no ceiling | true, insert at 1 |
The third call is the trap: the floor event ends exactly at 20, where the new one starts. The check is >, not >=, because the events are half-open and share no moment.
| 1 | Stored events never overlap, so kept sorted by start they are sorted by end too, and a new event can only clash with its two neighbours. |
| 2 | Keep this true: `starts` holds every stored start in sorted order, `ends[j]` belongs to `starts[j]`, and no two stored events overlap. |
| 3 | `book`: `i = bisect.bisect_right(self.starts, startTime)`; reject if the floor `self.ends[i - 1] > startTime` or the ceiling `self.starts[i] < endTime`; else insert at `i` in both lists. |
| 4 | The trap: events are half-open, so the checks are strict. `>=` or `<=` rejects `book(20, 30)` after `book(10, 20)`, which only touches it. |
Target: My Calendar I (LeetCode 729). `starts[j]` and `ends[j]` describe one stored event. Keeping `starts` sorted is what makes a binary search for the neighbours possible.
Trie: root-to-node path encodes common prefix; DSU: find(u) with path compression flattens tree so root is direct parent.
Trie: traverse word char-by-char in O(L); DSU: if find(u) != find(v): union(u, v) in O(alpha(N)).
Conceptual Narrative
🧭 Conceptual Foundation & Pattern Intuition
Comparing a new event with every stored event is O(N) per call. But the stored events never overlap each other, so once they are kept sorted by start their ends are sorted too, and a new event can only clash with two of them: the floor, the last stored event that starts at or before the new start, and the ceiling, the first one that starts after it. Every other stored event either ended before the floor started or starts after the ceiling. This is the Ordered Set technique: keep keys sorted while inserting anywhere, and answer "the closest key at or below x" and "the closest key above x" with one binary search.
📖 The Analogy: A Row of Booked Slots on a Timeline
Picture the day's bookings as blocks laid along a ruler, never overlapping. To see whether a new block fits, you put your finger where it would start and look only at the block to the left of your finger and the block to the right. If the left block ends before your start and the right block begins after your end, the new block fits; nothing further away can be in the way.
🪄 The Mathematical Harmony / Magic Trick
i = bisect.bisect_right(self.starts, startTime)if i > 0 and self.ends[i - 1] > startTime: return Falseif i < len(self.starts) and self.starts[i] < endTime: return False bisect_right splits the stored events into those that start at or before startTime (indexes below i) and those that start after it. Events are half-open, so the comparisons are strict: an event ending at 20 and one starting at 20 touch but do not overlap.
💡 Summary
One bisect_right finds both neighbours in O(log N); two strict comparisons decide; the event is inserted at i in both lists. A Python list makes that insert O(N); a balanced tree makes it O(log N). O(N) total space.
>=instead of>on the floor (or<=on the ceiling): events are half-open, so an event ending at20and one starting at20do not overlap. A non-strict check rejectsbook(20, 30)afterbook(10, 20).Checking only one neighbour: the floor check misses a stored event that starts inside the new one, and the ceiling check misses one that is still running when the new one starts. Both are needed.
Storing a rejected event: insert only after both checks pass; a rejected event left in the lists blocks later events that should fit.
Lists out of step: insert into
startsandendsat the samei; appending to one and inserting into the other pairs the wrong ends with the wrong starts.Re-sorting on every call:
starts.sort()after an append works but costsO(N log N)per call;bisect_rightplusinsertkeeps the order without sorting.
4-Phase Thought Process Model
You will see how a senior engineer hears "accept only if it does not overlap" with events arriving in any order and reaches for a sorted structure with floor and ceiling lookups.
Pattern Recognition Signals
The 10-second spot
"book" events one at a time, accepted only if they do not "double book" with any stored event, and the events arrive in any order: each new event needs the closest stored event at or below its start and the closest one above it. That is a floor and a ceiling lookup on a set that keeps growing: Ordered Set.
Formulating the Predicate & Invariants
Turning intuition into a boolean rule
starts holds the start of every stored event in sorted order and ends[j] belongs to starts[j]; stored events never overlap, so ends is sorted too. With i = bisect.bisect_right(self.starts, startTime), the new event fits exactly when not (i > 0 and self.ends[i - 1] > startTime) and not (i < len(self.starts) and self.starts[i] < endTime).
Silent Failure Traps & Edge Cases
Where confident candidates still lose points
Half-open events: the floor clashes only when
self.ends[i - 1] > startTime, and the ceiling only whenself.starts[i] < endTime. With>=or<=,book(20, 30)afterbook(10, 20)is wrongly rejected.Check both neighbours: the floor alone misses a stored event that starts inside the new one (
book(5, 11)afterbook(10, 20)), and the ceiling alone misses one that is still running when the new one starts.Insert only after both checks pass, and insert into both lists at the same
i, so a rejected event is never stored andends[j]keeps belonging tostarts[j].
The 60-Second Interview Pitch
Say this out loud before you type a single line
"I'd use an Ordered Set. I keep the stored events sorted by start, in two lists,
startsandends. For a new event I findi = bisect_right(starts, startTime): the event ati - 1is the floor, the last one that starts at or before mine, and the event atiis the ceiling, the first one that starts after. Stored events never overlap, so their ends are sorted too, and only those two neighbours can clash: everything earlier ends before the floor starts, everything later starts after the ceiling. The trap is that events are half-open, so the floor clashes only if its end is greater than my start, and the ceiling only if it starts before my end; touching events are fine. If both checks pass I insert ati. Finding the neighbours isO(log N); inserting into a Python list isO(N), and a balanced tree would make itO(log N). Space isO(N)."
So: sorted starts, i = bisect_right(starts, startTime), check the floor ends[i - 1] > startTime and the ceiling starts[i] < endTime, then insert both at i.
Complexity & Mathematical Proof
O(log N) search + O(N) insert per book
Look at the code: bisect.bisect_right(self.starts, startTime) is a binary search over the N stored starts, O(log N). The floor and ceiling checks read self.ends[i - 1] and self.starts[i], O(1) each. When the event fits, self.starts.insert(i, startTime) and self.ends.insert(i, endTime) shift every entry after i one place to the right, O(N) in the worst case. So each book is O(log N) to search plus O(N) to insert.
O(N)
Look at memory allocations: starts and ends hold one entry each per stored event, O(N) for N stored events. book makes no recursive calls and keeps only i, so each call uses O(1) extra space.
book = O(log N) search + O(N) insert, N = events stored so far
Derivation Progression
i = bisect.bisect_right(self.starts, startTime) ⟹ O(log N)
A binary search over the sorted starts: it halves the range each step.
self.ends[i - 1] > startTime, self.starts[i] < endTime ⟹ O(1)
Two index reads and two comparisons, however many events are stored.
self.starts.insert(i, startTime) ⟹ O(N)
A Python list shifts every entry after i; a balanced tree would insert in O(log N).
O(log N) + O(1) + O(N) = O(log N) search + O(N) insert
The search is the part the Ordered Set speeds up; the insert cost depends on the container.
Variable Definitions
Number of events stored so far (at most 1000 book calls)
Memory Architecture & Bounds
O(1): book is not recursive
O(N): starts and ends, one entry each per stored event
O(1) per call: one boolean
Boundary Best / Worst Cases
: the event clashes, so nothing is inserted
search + insert
: the event fits at the front, so the insert shifts every stored entry
Pointer Invariant Transition Progression
Senior SWE Deconstruction & Hardware Caveats
Triggers: "add a new event if it does not cause a double booking", "half-open interval [startTime, endTime)". Events arrive in any order and each one is judged against the stored set, so the store must stay sorted under inserts in the middle: Ordered Set, with a floor and a ceiling lookup per call.
At most calls and times up to . A scan of every stored event is per call, about comparisons in all, which also passes; the ordered set cuts the search to and is the shape that still works when the calendar holds millions of events.
Memory and write cost at scale: a sorted array shifts up to entries on every insert, so a store with millions of bookings needs a balanced tree or a B-tree index instead. Concurrency: two requests for the same slot can both pass the neighbour check before either inserts, so the check and the insert must happen under one lock or in one database transaction.
Core Algorithmic State Invariants
- Sorted by Start Means Sorted by End
Stored events never overlap, so the one that starts first also ends first: starts and ends are both sorted. That is why the floor event (i - 1) and the ceiling event (i) are the only ones that can clash with a new event.
- Floor and Ceiling from One bisect_right
i = bisect_right(starts, startTime) counts the stored events that start at or before startTime: the floor is at i - 1 and the ceiling at i. One binary search gives both neighbours.
O(log N)Search,O(N)List Insert
The search is O(log N), but list.insert shifts every later entry, O(N). A balanced tree (Java TreeMap, C++ std::map, sortedcontainers.SortedList) keeps the same floor and ceiling logic with an O(log N) insert.
My Calendar I (LeetCode 729)
You will see how keeping booked events sorted by start means a new event only has to be checked against the two events next to it.
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.
Worked Examples
["MyCalendar", "book", "book", "book"]
[[], [10, 20], [15, 25], [20, 30]][null, true, false, true]⚖️Formal Constraints & Bounds
0 <= start < end <= 109At most
1000calls will be made tobook.
Why It Works & Core Invariant
Stored events never overlap each other, so once they are sorted by start their ends are sorted too. A new event can then only clash with the stored event that starts just before it (the floor) or the one that starts just after it (the ceiling): every other stored event is further away than one of those two.
Real-World Scenario & Production Applications
A meeting-room or equipment booking system accepts a reservation only if the room is free for the whole slot. Reservations arrive in any order, so the system keeps them sorted by start time and compares a new one with the reservation just before it and the one just after it, never with the whole schedule.
Step-by-Step Execution Trace Table
LeetCode's calls book(10, 20), book(15, 25), book(20, 30):
| Call | starts / ends before | i = bisect_right(starts, startTime) | Floor check ends[i - 1] > startTime | Ceiling check starts[i] < endTime | Returns |
|---|---|---|---|---|---|
book(10, 20) | [] / [] | 0 | no floor | no ceiling | true, insert at 0 |
book(15, 25) | [10] / [20] | 1 | 20 > 15: clash | false | |
book(20, 30) | [10] / [20] | 1 | 20 > 20 is false | no ceiling | true, insert at 1 |
The third call is the trap: the floor event ends exactly at 20, where the new one starts. The check is >, not >=, because the events are half-open and share no moment.
| 1 | Stored events never overlap, so kept sorted by start they are sorted by end too, and a new event can only clash with its two neighbours. |
| 2 | Keep this true: `starts` holds every stored start in sorted order, `ends[j]` belongs to `starts[j]`, and no two stored events overlap. |
| 3 | `book`: `i = bisect.bisect_right(self.starts, startTime)`; reject if the floor `self.ends[i - 1] > startTime` or the ceiling `self.starts[i] < endTime`; else insert at `i` in both lists. |
| 4 | The trap: events are half-open, so the checks are strict. `>=` or `<=` rejects `book(20, 30)` after `book(10, 20)`, which only touches it. |
Target: My Calendar I (LeetCode 729). `starts[j]` and `ends[j]` describe one stored event. Keeping `starts` sorted is what makes a binary search for the neighbours possible.
Trie: root-to-node path encodes common prefix; DSU: find(u) with path compression flattens tree so root is direct parent.
Trie: traverse word char-by-char in O(L); DSU: if find(u) != find(v): union(u, v) in O(alpha(N)).
Conceptual Narrative
🧭 Conceptual Foundation & Pattern Intuition
Comparing a new event with every stored event is O(N) per call. But the stored events never overlap each other, so once they are kept sorted by start their ends are sorted too, and a new event can only clash with two of them: the floor, the last stored event that starts at or before the new start, and the ceiling, the first one that starts after it. Every other stored event either ended before the floor started or starts after the ceiling. This is the Ordered Set technique: keep keys sorted while inserting anywhere, and answer "the closest key at or below x" and "the closest key above x" with one binary search.
📖 The Analogy: A Row of Booked Slots on a Timeline
Picture the day's bookings as blocks laid along a ruler, never overlapping. To see whether a new block fits, you put your finger where it would start and look only at the block to the left of your finger and the block to the right. If the left block ends before your start and the right block begins after your end, the new block fits; nothing further away can be in the way.
🪄 The Mathematical Harmony / Magic Trick
i = bisect.bisect_right(self.starts, startTime)if i > 0 and self.ends[i - 1] > startTime: return Falseif i < len(self.starts) and self.starts[i] < endTime: return False bisect_right splits the stored events into those that start at or before startTime (indexes below i) and those that start after it. Events are half-open, so the comparisons are strict: an event ending at 20 and one starting at 20 touch but do not overlap.
💡 Summary
One bisect_right finds both neighbours in O(log N); two strict comparisons decide; the event is inserted at i in both lists. A Python list makes that insert O(N); a balanced tree makes it O(log N). O(N) total space.
>=instead of>on the floor (or<=on the ceiling): events are half-open, so an event ending at20and one starting at20do not overlap. A non-strict check rejectsbook(20, 30)afterbook(10, 20).Checking only one neighbour: the floor check misses a stored event that starts inside the new one, and the ceiling check misses one that is still running when the new one starts. Both are needed.
Storing a rejected event: insert only after both checks pass; a rejected event left in the lists blocks later events that should fit.
Lists out of step: insert into
startsandendsat the samei; appending to one and inserting into the other pairs the wrong ends with the wrong starts.Re-sorting on every call:
starts.sort()after an append works but costsO(N log N)per call;bisect_rightplusinsertkeeps the order without sorting.
4-Phase Thought Process Model
You will see how a senior engineer hears "accept only if it does not overlap" with events arriving in any order and reaches for a sorted structure with floor and ceiling lookups.
Pattern Recognition Signals
The 10-second spot
"book" events one at a time, accepted only if they do not "double book" with any stored event, and the events arrive in any order: each new event needs the closest stored event at or below its start and the closest one above it. That is a floor and a ceiling lookup on a set that keeps growing: Ordered Set.
Formulating the Predicate & Invariants
Turning intuition into a boolean rule
starts holds the start of every stored event in sorted order and ends[j] belongs to starts[j]; stored events never overlap, so ends is sorted too. With i = bisect.bisect_right(self.starts, startTime), the new event fits exactly when not (i > 0 and self.ends[i - 1] > startTime) and not (i < len(self.starts) and self.starts[i] < endTime).
Silent Failure Traps & Edge Cases
Where confident candidates still lose points
Half-open events: the floor clashes only when
self.ends[i - 1] > startTime, and the ceiling only whenself.starts[i] < endTime. With>=or<=,book(20, 30)afterbook(10, 20)is wrongly rejected.Check both neighbours: the floor alone misses a stored event that starts inside the new one (
book(5, 11)afterbook(10, 20)), and the ceiling alone misses one that is still running when the new one starts.Insert only after both checks pass, and insert into both lists at the same
i, so a rejected event is never stored andends[j]keeps belonging tostarts[j].
The 60-Second Interview Pitch
Say this out loud before you type a single line
"I'd use an Ordered Set. I keep the stored events sorted by start, in two lists,
startsandends. For a new event I findi = bisect_right(starts, startTime): the event ati - 1is the floor, the last one that starts at or before mine, and the event atiis the ceiling, the first one that starts after. Stored events never overlap, so their ends are sorted too, and only those two neighbours can clash: everything earlier ends before the floor starts, everything later starts after the ceiling. The trap is that events are half-open, so the floor clashes only if its end is greater than my start, and the ceiling only if it starts before my end; touching events are fine. If both checks pass I insert ati. Finding the neighbours isO(log N); inserting into a Python list isO(N), and a balanced tree would make itO(log N). Space isO(N)."
So: sorted starts, i = bisect_right(starts, startTime), check the floor ends[i - 1] > startTime and the ceiling starts[i] < endTime, then insert both at i.
Complexity & Mathematical Proof
O(log N) search + O(N) insert per book
Look at the code: bisect.bisect_right(self.starts, startTime) is a binary search over the N stored starts, O(log N). The floor and ceiling checks read self.ends[i - 1] and self.starts[i], O(1) each. When the event fits, self.starts.insert(i, startTime) and self.ends.insert(i, endTime) shift every entry after i one place to the right, O(N) in the worst case. So each book is O(log N) to search plus O(N) to insert.
O(N)
Look at memory allocations: starts and ends hold one entry each per stored event, O(N) for N stored events. book makes no recursive calls and keeps only i, so each call uses O(1) extra space.
book = O(log N) search + O(N) insert, N = events stored so far
Derivation Progression
i = bisect.bisect_right(self.starts, startTime) ⟹ O(log N)
A binary search over the sorted starts: it halves the range each step.
self.ends[i - 1] > startTime, self.starts[i] < endTime ⟹ O(1)
Two index reads and two comparisons, however many events are stored.
self.starts.insert(i, startTime) ⟹ O(N)
A Python list shifts every entry after i; a balanced tree would insert in O(log N).
O(log N) + O(1) + O(N) = O(log N) search + O(N) insert
The search is the part the Ordered Set speeds up; the insert cost depends on the container.
Variable Definitions
Number of events stored so far (at most 1000 book calls)
Memory Architecture & Bounds
O(1): book is not recursive
O(N): starts and ends, one entry each per stored event
O(1) per call: one boolean
Boundary Best / Worst Cases
: the event clashes, so nothing is inserted
search + insert
: the event fits at the front, so the insert shifts every stored entry
Pointer Invariant Transition Progression
Senior SWE Deconstruction & Hardware Caveats
Triggers: "add a new event if it does not cause a double booking", "half-open interval [startTime, endTime)". Events arrive in any order and each one is judged against the stored set, so the store must stay sorted under inserts in the middle: Ordered Set, with a floor and a ceiling lookup per call.
At most calls and times up to . A scan of every stored event is per call, about comparisons in all, which also passes; the ordered set cuts the search to and is the shape that still works when the calendar holds millions of events.
Memory and write cost at scale: a sorted array shifts up to entries on every insert, so a store with millions of bookings needs a balanced tree or a B-tree index instead. Concurrency: two requests for the same slot can both pass the neighbour check before either inserts, so the check and the insert must happen under one lock or in one database transaction.
Core Algorithmic State Invariants
- Sorted by Start Means Sorted by End
Stored events never overlap, so the one that starts first also ends first: starts and ends are both sorted. That is why the floor event (i - 1) and the ceiling event (i) are the only ones that can clash with a new event.
- Floor and Ceiling from One bisect_right
i = bisect_right(starts, startTime) counts the stored events that start at or before startTime: the floor is at i - 1 and the ceiling at i. One binary search gives both neighbours.
O(log N)Search,O(N)List Insert
The search is O(log N), but list.insert shifts every later entry, O(N). A balanced tree (Java TreeMap, C++ std::map, sortedcontainers.SortedList) keeps the same floor and ceiling logic with an O(log N) insert.
| Canonical Invariant | Concrete Code | Engineering Rationale |
|---|---|---|
| Two parallel lists, sorted by start | self.starts: list[int] = []
self.ends: list[int] = [] | `starts[j]` and `ends[j]` describe one stored event. Keeping `starts` sorted is what makes a binary search for the neighbours possible. |
| Find the floor and the ceiling | i = bisect.bisect_right(self.starts, startTime) | `i` counts the stored events that start at or before `startTime`: the floor is at `i - 1`, the ceiling at `i`. |
| Floor check (half-open) | if i > 0 and self.ends[i - 1] > startTime:
return False | The floor clashes only if it is still running at `startTime`. An end equal to `startTime` is fine: the events only touch. |
| Ceiling check (half-open) | if i < len(self.starts) and self.starts[i] < endTime:
return False | The ceiling clashes only if it starts before `endTime`. Every later event starts even later, so it cannot clash if the ceiling does not. |
| Insert in sorted place | self.starts.insert(i, startTime)
self.ends.insert(i, endTime) | Both lists get the new event at the same `i`, so they stay sorted and stay paired. |