Exam Room
MedPrompt
There is an exam room with n seats in a single row labeled from 0 to n - 1.
When a student enters the room, they must sit in the seat that maximizes the distance to the closest person. If there are multiple such seats, they sit in the seat with the lowest number. If no one is in the room, then the student sits at seat number 0.
Design a class that simulates the mentioned exam room.
Implement the ExamRoom class:
ExamRoom(int n)Initializes the object of the exam room with the number of the seatsn.int seat()Returns the label of the seat at which the next student will set.void leave(int p)Indicates that the student sitting at seatpwill leave the room. It is guaranteed that there will be a student sitting at seatp.
Example 1:
Input
["ExamRoom", "seat", "seat", "seat", "seat", "leave", "seat"]
[[10], [], [], [], [], [4], []]
Output
[null, 0, 9, 4, 2, null, 5]
Explanation
ExamRoom examRoom = new ExamRoom(10);
examRoom.seat(); // return 0, no one is in the room, then the student sits at seat number 0.
examRoom.seat(); // return 9, the student sits at the last seat number 9.
examRoom.seat(); // return 4, the student sits at the last seat number 4.
examRoom.seat(); // return 2, the student sits at the last seat number 2.
examRoom.leave(4);
examRoom.seat(); // return 5, the student sits at the last seat number 5.
Constraints:
1 <= n <= 109- It is guaranteed that there is a student sitting at seat
p. - At most
104calls will be made toseatandleave.
Approaches
3 approaches with complexity analysis and trade-offs.
This approach uses a simple ArrayList to store the positions of the students. To maintain the ability to find gaps between adjacent students, the list is kept sorted after every insertion.
Algorithm
-
Constructor
ExamRoom(n):- Initialize an
ArrayList<Integer>calledseatsto store the positions of occupied seats. - Store the total number of seats
n.
- Initialize an
-
seat()method:- If
seatsis empty, add0toseatsand return0. - Initialize
maxDist = -1andseat = -1to track the best option found. - Check the first gap (from seat 0 to the first student): The potential seat is
0. The distance isseats.get(0). If this distance is greater thanmaxDist, updatemaxDistto this distance andseatto0. - Check internal gaps (between adjacent students): Iterate through the
seatslist from the first to the second-to-last student. For each pair of adjacent students atp1andp2, calculate the midpoint(p1 + p2) / 2. The distance is(p2 - p1) / 2. If this distance is greater thanmaxDist, updatemaxDistand setseatto the midpoint. - Check the last gap (from the last student to seat n-1): The potential seat is
n-1. The distance is(n - 1) - seats.get(seats.size() - 1). If this distance is greater thanmaxDist, updatemaxDistand setseatton-1. - After checking all gaps,
seatholds the best position. Insert thisseatinto theseatslist while maintaining its sorted order. A simple way is to add it and then callCollections.sort(). - Return the chosen
seat.
- If
-
leave(p)method:- Remove the integer
pfrom theseatslist. This requires finding the element and shifting subsequent elements.
- Remove the integer
Walkthrough
The core idea is to maintain a sorted list of occupied seat numbers. When a new student needs to be seated, we can iterate through this list to find the largest empty segment (gap) and place the student in the middle of it.
seat() operation:
When seat() is called, we analyze all possible places a student can sit. These places are determined by the gaps between existing students and the gaps at the ends of the row.
- If the room is empty: The first student sits at seat 0.
- If the room has students: We calculate the largest possible distance a new student can have from their nearest neighbor. This can be:
- At seat 0, with a distance of
seats.get(0)from the first student. - At seat
n-1, with a distance of(n-1) - seats.get(seats.size()-1)from the last student. - In the middle of any two adjacent students
p1andp2. The best seat is(p1+p2)/2, with a distance of(p2-p1)/2.
- At seat 0, with a distance of
We iterate through all these possibilities, find the one that maximizes the distance, and handle ties by choosing the lower seat number. After finding the best seat, we add it to our list and re-sort the list to prepare for the next call.
leave() operation:
When leave(p) is called, we simply find and remove the seat p from our list.
import java.util.ArrayList;import java.util.Collections;import java.util.List; class ExamRoom { private List<Integer> seats; private int n; public ExamRoom(int n) { this.n = n; this.seats = new ArrayList<>(); } public int seat() { if (seats.isEmpty()) { seats.add(0); return 0; } int maxDist = seats.get(0); int seat = 0; for (int i = 0; i < seats.size() - 1; ++i) { int p1 = seats.get(i); int p2 = seats.get(i + 1); int dist = (p2 - p1) / 2; if (dist > maxDist) { maxDist = dist; seat = p1 + dist; } } if (n - 1 - seats.get(seats.size() - 1) > maxDist) { seat = n - 1; } seats.add(seat); Collections.sort(seats); return seat; } public void leave(int p) { seats.remove(Integer.valueOf(p)); }}Complexity
Time
* `seat()`: O(k log k). The loop to find the best gap takes O(k) time, and sorting the list after insertion takes O(k log k). * `leave(p)`: O(k). Removing an element from an `ArrayList` requires a linear scan to find the element and shift subsequent elements.
Space
O(k), where k is the number of occupied seats (students in the room). We need to store the position of each student.
Trade-offs
Pros
The logic is straightforward and relatively easy to implement and understand.
It works correctly for the given problem constraints on the number of calls, although it's not the most performant.
Cons
The
seat()operation is inefficient because finding the best gap requires iterating through all occupied seats, which takes O(k) time. Inserting into a sorted list and re-sorting also takes O(k) time.The
leave(p)operation is also inefficient, as removing an element from anArrayListtakes O(k) time on average due to the need to shift elements.
This approach improves upon the ArrayList solution by using a TreeSet. A TreeSet is a balanced binary search tree that keeps elements sorted automatically and provides logarithmic time complexity for insertions and deletions. This significantly speeds up the leave operation.
Algorithm
-
Constructor
ExamRoom(n):- Initialize a
TreeSet<Integer>calledseats. - Store the total number of seats
n.
- Initialize a
-
seat()method:- If
seatsis empty, add0toseatsand return0. - The logic for finding the best seat is identical to the
ArrayListapproach. We check the gap at the start, the gaps in the middle, and the gap at the end. - Start gap: Distance is
seats.first(), seat is0. - Middle gaps: Iterate through the
TreeSet. For each adjacent pair of seatsp1andp2, the distance is(p2 - p1) / 2and the seat isp1 + (p2 - p1) / 2. - End gap: Distance is
n - 1 - seats.last(), seat isn-1. - Keep track of the maximum distance and the corresponding seat with the lowest index.
- After finding the best
seat, add it to theTreeSetusingseats.add(seat). - Return the
seat.
- If
-
leave(p)method:- Call
seats.remove(p)to remove the student from theTreeSet.
- Call
Walkthrough
By replacing the ArrayList with a TreeSet, we gain efficiency for operations that benefit from a sorted, tree-based structure.
seat() operation:
The algorithm to find the best seat remains the same. We still need to check every gap between adjacent students to find the largest one. A TreeSet allows us to get the first (seats.first()) and last (seats.last()) students in O(log k) time, but iterating through all adjacent pairs still takes O(k) time. However, once the best seat is determined, adding it to the TreeSet is an efficient O(log k) operation, as the tree automatically rebalances itself.
leave() operation:
This is where the TreeSet shines. Removing a student at seat p is a standard tree deletion operation, which takes O(log k) time. This is a major improvement over the O(k) time required for an ArrayList.
While seat() is still linear, the overall performance is better due to the much faster leave() calls.
import java.util.TreeSet; class ExamRoom { private TreeSet<Integer> seats; private int n; public ExamRoom(int n) { this.n = n; this.seats = new TreeSet<>(); } public int seat() { if (seats.isEmpty()) { seats.add(0); return 0; } int maxDist = seats.first(); int seat = 0; Integer prev = null; for (Integer current : seats) { if (prev != null) { int dist = (current - prev) / 2; if (dist > maxDist) { maxDist = dist; seat = prev + dist; } } prev = current; } if (n - 1 - seats.last() > maxDist) { seat = n - 1; } seats.add(seat); return seat; } public void leave(int p) { seats.remove(p); }}Complexity
Time
* `seat()`: O(k). The iteration to find the best gap takes O(k) time, which dominates the O(log k) insertion time. * `leave(p)`: O(log k). Removing an element from a `TreeSet` is a logarithmic operation.
Space
O(k), where k is the number of occupied seats. The `TreeSet` stores one entry per student.
Trade-offs
Pros
The
leave(p)operation is very efficient, with a time complexity of O(log k).The
seat()operation benefits from a fast O(log k) insertion, although the overall complexity is still linear.Maintains a clean, sorted representation of students without manual sorting.
Cons
The
seat()operation remains the bottleneck. Although adding the new seat is fast (O(log k)), finding where to place it still requires iterating through all existing students, resulting in an O(k) time complexity.
This is the most optimal solution, achieving logarithmic time complexity for both seat and leave operations. It cleverly combines a TreeSet to keep track of occupied seats with a PriorityQueue (acting as a max-heap) to efficiently find the largest available gap at any time.
Algorithm
-
Data Structures:
TreeSet<Integer> seats: To store occupied seats and efficiently find neighbors (lower,higher).PriorityQueue<int[]> pq: A max-heap to store available gaps[start, end]and quickly retrieve the best one.
-
Comparator for
pq:- The
PriorityQueueis ordered by a custom comparator. - For a gap
[start, end], calculate its effective distance. This isend - startfor edge gaps (wherestartis 0 orendisn-1) and(end - start) / 2for internal gaps. - The comparator prioritizes the gap with the largest distance. Ties are broken by the smaller seat index.
- The
-
seat()method:- Poll the
pqto get the best available gap[start, end]. - This gap might be stale (invalidated by a previous
leaveorseatoperation). We must loop, polling from thepq, until we find a valid gap. A gap[s, e]is valid ifseats.higher(s)equalse(with special logic for boundaries). - Once a valid gap is found, calculate the new seat
p. - Add
pto theseatsTreeSet. - The old gap
[start, end]is now split. Add the two new gaps,[start, p]and[p, end], back into thepq.
- Poll the
-
leave(p)method:- Use the
seatsTreeSetto find the neighbors ofp:prev = seats.lower(p)andnext = seats.higher(p). - Remove
pfromseats. - The two gaps adjacent to
p,[prev, p]and[p, next], are now obsolete. They are merged into a new, larger gap[prev, next]. - Add this new merged gap
[prev, next]to thepq. The old, smaller gaps are not explicitly removed (lazy deletion); they will be discarded as stale when polled by theseat()method.
- Use the
Walkthrough
The bottleneck in the previous approach was the linear scan to find the best gap. We can eliminate this scan by using a PriorityQueue to maintain the gaps, ordered by their potential to place a student with the maximum distance.
Data Structures
TreeSet<Integer> seats: As before, this stores the sorted positions of students. Its key role here is to quickly find the neighbors of a student who is leaving, usinglower()andhigher()in O(log k) time.PriorityQueue<int[]> pq: This max-heap stores all the available gaps, represented as[start_seat, end_seat]. A custom comparator ensures that the gap at the top of the heap is always the one that provides the best possible seat placement (i.e., maximizes the distance to the nearest person, with ties broken by lower seat index).
seat() Operation
Instead of scanning, we simply ask the PriorityQueue for the best gap, which is an O(log k) operation. We take that gap, place the new student p, add p to our TreeSet, and then add the two new, smaller gaps created by the placement ([start, p] and [p, end]) back into the PriorityQueue.
leave() Operation and Lazy Deletion
When a student at p leaves, we use the TreeSet to find their neighbors, prev and next. This tells us that the two gaps [prev, p] and [p, next] are now gone, and a new, larger gap [prev, next] has been created. We add this new gap to the PriorityQueue. The crucial insight is that we don't need to remove the old, now-invalid gaps from the PriorityQueue. This is called lazy deletion. We simply leave them there. When the seat() method polls a gap from the queue, it performs a quick check to see if it's still valid (by checking if the start and end points are still adjacent in the TreeSet). If not, it discards the stale gap and polls the next one. This makes both seat() and leave() operations efficient.
import java.util.PriorityQueue;import java.util.TreeSet; class ExamRoom { private int n; private TreeSet<Integer> seats; private PriorityQueue<int[]> pq; public ExamRoom(int n) { this.n = n; this.seats = new TreeSet<>(); // The PQ stores intervals [start, end]. // The comparator prioritizes the interval with the largest distance. // Distance for [x, y] is (y - x) / 2. // For edge cases [0, x] or [y, n-1], the distance is x or n-1-y. // Tie is broken by the smaller start index. this.pq = new PriorityQueue<>((a, b) -> { int distA = getDistance(a); int distB = getDistance(b); if (distA != distB) { return distB - distA; // Max-heap } return a[0] - b[0]; // Smaller start index }); pq.offer(new int[]{-1, n}); // Initial gap for the whole room } public int seat() { int seat = 0; int[] bestGap = pq.poll(); // Lazy deletion: keep polling until we find a valid gap while (seats.contains(bestGap[0]) || seats.contains(bestGap[1]) || (bestGap[0] != -1 && seats.higher(bestGap[0]) != null && seats.higher(bestGap[0]) != bestGap[1]) || (bestGap[1] != n && seats.lower(bestGap[1]) != null && seats.lower(bestGap[1]) != bestGap[0])) { if(bestGap[0] != -1 && !seats.contains(bestGap[0])){ Integer higher = seats.higher(bestGap[0]); if(higher != null) pq.offer(new int[]{bestGap[0], higher}); } if(bestGap[1] != n && !seats.contains(bestGap[1])){ Integer lower = seats.lower(bestGap[1]); if(lower != null) pq.offer(new int[]{lower, bestGap[1]}); } bestGap = pq.poll(); } if (bestGap[0] == -1) { seat = 0; } else if (bestGap[1] == n) { seat = n - 1; } else { seat = bestGap[0] + (bestGap[1] - bestGap[0]) / 2; } seats.add(seat); pq.offer(new int[]{bestGap[0], seat}); pq.offer(new int[]{seat, bestGap[1]}); return seat; } public void leave(int p) { Integer prev = seats.lower(p); Integer next = seats.higher(p); seats.remove(p); if (prev == null) prev = -1; if (next == null) next = n; pq.offer(new int[]{prev, next}); } private int getDistance(int[] interval) { if (interval[0] == -1) { return interval[1]; } if (interval[1] == n) { return n - 1 - interval[0]; } return (interval[1] - interval[0]) / 2; }}Note: The lazy deletion logic in the provided snippet is complex and may need refinement for all edge cases. It serves to illustrate the concept.
Complexity
Time
* `seat()`: O(log k) amortized. Polling the `PriorityQueue` is O(log k). While there's a loop for lazy deletion, the total work is amortized over all operations. Adding to the `TreeSet` and `PriorityQueue` are also O(log k). * `leave(p)`: O(log k). Finding neighbors in the `TreeSet` is O(log k), removing from it is O(log k), and adding the new merged gap to the `PriorityQueue` is O(log k).
Space
O(k), where k is the number of occupied seats. We store each student in the `TreeSet` and each gap in the `PriorityQueue`.
Trade-offs
Pros
Optimal time complexity: Both
seat()andleave()operations run in O(log k) time (amortized forseat).Highly scalable for a large number of calls to
seatandleave.
Cons
This approach is significantly more complex to implement correctly.
The logic for the priority queue's comparator must carefully handle the different distance calculations for edge gaps vs. internal gaps and the tie-breaking rule.
Managing stale entries in the priority queue (lazy deletion) adds another layer of complexity to the
seat()method.
Solutions
Solution
class ExamRoom { private TreeSet < int []> ts = new TreeSet <>(( a , b ) -> { int d1 = dist ( a ), d2 = dist ( b ); return d1 == d2 ? a [ 0 ] - b [ 0 ] : d2 - d1 ; }); private Map < Integer , Integer > left = new HashMap <>(); private Map < Integer , Integer > right = new HashMap <>(); private int n ; public ExamRoom ( int n ) { this . n = n ; add ( new int [] {- 1 , n }); } public int seat () { int [] s = ts . first (); int p = ( s [ 0 ] + s [ 1 ]) >> 1 ; if ( s [ 0 ] == - 1 ) { p = 0 ; } else if ( s [ 1 ] == n ) { p = n - 1 ; } del ( s ); add ( new int [] { s [ 0 ], p }); add ( new int [] { p , s [ 1 ]}); return p ; } public void leave ( int p ) { int l = left . get ( p ), r = right . get ( p ); del ( new int [] { l , p }); del ( new int [] { p , r }); add ( new int [] { l , r }); } private int dist ( int [] s ) { int l = s [ 0 ], r = s [ 1 ]; return l == - 1 || r == n ? r - l - 1 : ( r - l ) >> 1 ; } private void add ( int [] s ) { ts . add ( s ); left . put ( s [ 1 ], s [ 0 ]); right . put ( s [ 0 ], s [ 1 ]); } private void del ( int [] s ) { ts . remove ( s ); left . remove ( s [ 1 ]); right . remove ( s [ 0 ]); } } /** * Your ExamRoom object will be instantiated and called as such: * ExamRoom obj = new ExamRoom(n); * int param_1 = obj.seat(); * obj.leave(p); */int N ; int dist ( const pair < int , int >& p ) { auto [ l , r ] = p ; if ( l == - 1 || r == N ) return r - l - 1 ; return ( r - l ) >> 1 ; } struct cmp { bool operator ()( const pair < int , int >& a , const pair < int , int >& b ) const { int d1 = dist ( a ), d2 = dist ( b ); return d1 == d2 ? a . first < b . first : d1 > d2 ; }; }; class ExamRoom { public: ExamRoom ( int n ) { N = n ; this -> n = n ; add ({ - 1 , n }); } int seat () { auto s = * ts . begin (); int p = ( s . first + s . second ) >> 1 ; if ( s . first == - 1 ) { p = 0 ; } else if ( s . second == n ) { p = n - 1 ; } del ( s ); add ({ s . first , p }); add ({ p , s . second }); return p ; } void leave ( int p ) { int l = left [ p ], r = right [ p ]; del ({ l , p }); del ({ p , r }); add ({ l , r }); } private: set < pair < int , int > , cmp > ts ; unordered_map < int , int > left ; unordered_map < int , int > right ; int n ; void add ( pair < int , int > s ) { ts . insert ( s ); left [ s . second ] = s . first ; right [ s . first ] = s . second ; } void del ( pair < int , int > s ) { ts . erase ( s ); left . erase ( s . second ); right . erase ( s . first ); } }; /** * Your ExamRoom object will be instantiated and called as such: * ExamRoom* obj = new ExamRoom(n); * int param_1 = obj->seat(); * obj->leave(p); */from sortedcontainers import SortedList class ExamRoom : def __init__ ( self , n : int ): def dist ( x ): l , r = x return r - l - 1 if l == - 1 or r == n else ( r - l ) >> 1 self . n = n self . ts = SortedList ( key = lambda x : ( - dist ( x ), x [ 0 ])) self . left = {} self . right = {} self . add (( - 1 , n )) def seat ( self ) -> int : s = self . ts [ 0 ] p = ( s [ 0 ] + s [ 1 ]) >> 1 if s [ 0 ] == - 1 : p = 0 elif s [ 1 ] == self . n : p = self . n - 1 self . delete ( s ) self . add (( s [ 0 ], p )) self . add (( p , s [ 1 ])) return p def leave ( self , p : int ) -> None : l , r = self . left [ p ], self . right [ p ] self . delete (( l , p )) self . delete (( p , r )) self . add (( l , r )) def add ( self , s ): self . ts . add ( s ) self . left [ s [ 1 ]] = s [ 0 ] self . right [ s [ 0 ]] = s [ 1 ] def delete ( self , s ): self . ts . remove ( s ) self . left . pop ( s [ 1 ]) self . right . pop ( s [ 0 ]) # Your ExamRoom object will be instantiated and called as such: # obj = ExamRoom(n) # param_1 = obj.seat() # obj.leave(p)Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.