The real-world analogy: conference room clipboard timelines
Imagine walking down the hall of an office building. Outside each conference room door hangs a paper clipboard showing a calendar timeline:
- Linear Scan vs. Sorted Registry: If the clipboard contains a disorganized stack of loose post-it notes with meeting times, you have to read every single note (taking O(N) operations) to check if your new meeting overlaps with any of them. If the clipboard has one single paper sheet pre-ruled with hours sorted from 9:00 AM to 5:00 PM, you immediately slide your eyes to your start time and look only at the slot immediately before and after (taking O(1) focus operations).
- Double-booking Prevention (Lock): Two people walk up to the clipboard simultaneously, pencils in hand. They both notice the 2:00 PM slot is empty. If they both start writing without acknowledging the other, they will overwrite each other's text. To prevent this, they must agree that only one person holds the clipboard pencil at a time (Mutex Lock).
In software, we model this by sorting room timelines inside self-balancing binary search trees (like TreeMap or std::map) to perform O(log N) conflict lookups, and applying locks to prevent concurrent race conditions.
The ask
Book meetings with a start and end time across a set of rooms: reject overlaps in the same room, assign a free room when one's available, and answer "how many rooms do we need?" (This is My Calendar I/II and Meeting Rooms II / LeetCode 253 as a design problem.)
UML Class Diagram
The two core algorithms
- Conflict detection (one room): two intervals
[s1,e1)and[s2,e2)overlap iffs1 < e2 and s2 < e1. Keep each room's bookings sorted (aTreeMap/balanced BST keyed on start) so you can binary-search the neighbours and check only those — O(log n) per booking instead of scanning all. - Minimum rooms (sweep line + heap): sort meetings by start; keep a min-heap of end times of in-progress meetings. For each meeting, if the earliest end ≤ its start, a room freed up (pop and reuse); else allocate a new room (push). The heap's max size is the answer.
Think it through like the interview
PROBLEMBook meetings across rooms with no overlaps; assign a free room; report the minimum rooms needed.
- 1
Spec → entities
“What are the objects and what does each own?”
- 2
Conflict check without scanning everything
“How do I know a new [s,e) fits a room in better than O(n)?”
unlocks after the stage above - 3
Minimum rooms = max concurrency
“How many rooms do N meetings need?”
unlocks after the stage above
Implementation
Below are complete implementations with sorted interval lookups and concurrency protection locks.
Python
import bisect
import threading
from typing import List, Tuple, Optional
class Meeting:
def __init__(self, start: int, end: int, title: str):
self.start = start
self.end = end
self.title = title
class Room:
def __init__(self, room_id: str):
self.room_id = room_id
self.bookings: List[Tuple[int, int, Meeting]] = [] # Keep sorted by start time
self.lock = threading.Lock()
def book(self, meeting: Meeting) -> bool:
with self.lock:
# Binary search neighbor index using bisect
idx = bisect.bisect_left(self.bookings, (meeting.start, meeting.end))
# Check overlap with previous booking (if any)
if idx > 0:
prev_start, prev_end, _ = self.bookings[idx - 1]
if prev_end > meeting.start:
return False # Overlap!
# Check overlap with next booking (if any)
if idx < len(self.bookings):
next_start, next_end, _ = self.bookings[idx]
if meeting.end > next_start:
return False # Overlap!
# Insert into sorted list
self.bookings.insert(idx, (meeting.start, meeting.end, meeting))
return True
class Scheduler:
def __init__(self):
self.rooms: List[Room] = []
self.lock = threading.Lock()
def add_room(self, room_id: str):
with self.lock:
self.rooms.append(Room(room_id))
def book_meeting(self, meeting: Meeting) -> Optional[str]:
# Try booking in rooms
for room in self.rooms:
if room.book(meeting):
return room.room_id
return None
Java
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
class Meeting {
int start;
int end;
String title;
Meeting(int s, int e, String t) {
this.start = s;
this.end = e;
this.title = t;
}
}
class Room {
String roomId;
// TreeMap keeps entries sorted by start time, allowing ceiling/floor log(N) operations
private final TreeMap<Integer, Meeting> bookings = new TreeMap<>();
private final ReentrantLock lock = new ReentrantLock();
Room(String id) { this.roomId = id; }
public boolean book(Meeting meeting) {
lock.lock();
try {
// Find booking starting immediately at or after meeting.start
Integer nextStart = bookings.ceilingKey(meeting.start);
if (nextStart != null && nextStart < meeting.end) {
return false; // Overlaps with next meeting
}
// Find booking starting before meeting.start
Integer prevStart = bookings.floorKey(meeting.start);
if (prevStart != null) {
Meeting prevMeeting = bookings.get(prevStart);
if (prevMeeting.end > meeting.start) {
return false; // Overlaps with previous meeting
}
}
bookings.put(meeting.start, meeting);
return true;
} finally {
lock.unlock();
}
}
}
public class Scheduler {
private final List<Room> rooms = new ArrayList<>();
private final ReentrantLock lock = new ReentrantLock();
public void addRoom(String roomId) {
lock.lock();
try {
rooms.add(new Room(roomId));
} finally {
lock.unlock();
}
}
public String bookMeeting(Meeting meeting) {
for (Room room : rooms) {
if (room.book(meeting)) {
return room.roomId;
}
}
return null; // No available room fits this interval
}
}
C++
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <mutex>
#include <memory>
class Meeting {
public:
int start;
int end;
std::string title;
Meeting(int s, int e, std::string t) : start(s), end(e), title(t) {}
};
class Room {
private:
std::string roomId;
// Map in C++ is a red-black tree, sorted by key (start time)
std::map<int, std::shared_ptr<Meeting>> bookings;
std::mutex mtx;
public:
Room(std::string id) : roomId(id) {}
std::string getRoomId() const { return roomId; }
bool book(std::shared_ptr<Meeting> meeting) {
std::lock_guard<std::mutex> lock(mtx);
// Find iterator pointing to first element >= meeting->start
auto it = bookings.lower_bound(meeting->start);
if (it != bookings.end() && it->first < meeting->end) {
return false; // Overlap with next meeting
}
if (it != bookings.begin()) {
auto prevIt = std::prev(it);
if (prevIt->second->end > meeting->start) {
return false; // Overlap with previous meeting
}
}
bookings[meeting->start] = meeting;
return true;
}
};
class Scheduler {
private:
std::vector<std::shared_ptr<Room>> rooms;
std::mutex mtx;
public:
void addRoom(const std::string& id) {
std::lock_guard<std::mutex> lock(mtx);
rooms.push_back(std::make_shared<Room>(id));
}
std::string bookMeeting(std::shared_ptr<Meeting> meeting) {
for (auto& room : rooms) {
if (room->book(meeting)) {
return room->getRoomId();
}
}
return "";
}
};
TypeScript
type Meeting = { start: number; end: number };
class Scheduler {
// Each room holds bookings sorted by start time.
private rooms: Meeting[][] = [];
private overlaps(a: Meeting, b: Meeting) {
return a.start < b.end && b.start < a.end; // half-open intervals
}
// Book into the first room with no conflict; open a new room if none fits.
book(m: Meeting): number {
for (let i = 0; i < this.rooms.length; i++) {
if (!this.rooms[i].some((b) => this.overlaps(b, m))) {
this.rooms[i].push(m);
return i; // assigned room index
}
}
this.rooms.push([m]); // all busy → new room
return this.rooms.length - 1;
}
}
Treat times as [start, end). A meeting ending at 10:00 and one starting at 10:00 then don't conflict (s1 < e2 and s2 < e1 is false), which matches how calendars actually behave. State the convention up front — it kills a whole class of boundary bugs.
Interactive Quiz
1.
2.
3.
Complexity & follow-ups
- Booking: O(log n) per room with a TreeMap; the naive
some()scan above is O(n) per room — fine to start, then optimize when asked. - Min rooms: O(n log n) via the heap.
- Recurring meetings: store a rule (RRULE) + expand lazily into instances within a query window — don't materialize infinite occurrences.
- Time zones: store everything in UTC, convert at the edges; mind DST transitions.
- Find a common slot across attendees: merge each person's busy intervals, then intersect the free gaps (interval intersection).
- Concurrency / double-booking: two requests for the same room+time race — guard with a per-room lock or an optimistic check-and-insert (unique constraint on room+overlap) so the second booking fails cleanly.
Design drills
Whiteboard each one out loud for 5–10 minutes before you reveal what a strong answer covers — the gap between your sketch and the checklist is your study list. Progress is saved on this device.
Find the earliest common free slot of duration D across K attendees.
Two users book the same room for the same time simultaneously. Prevent the double-booking.
Support recurring meetings (every weekday for a quarter) without storing infinite rows.