Implement rate limiter, top-K, scheduler algorithms
Company: Uber
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: Medium
Interview Round: Onsite
Quick Answer: This question evaluates proficiency in concurrent programming and thread-safe design, core algorithm implementation (binary search and top-K logic), data structure usage (including doubly-linked-list techniques), and scheduling and ranking reasoning for meeting and resource allocation.
Part 1: Sliding Window Rate Limiter
Constraints
- 0 <= len(request_times) <= 200000
- 1 <= window_size <= 10^9
- 1 <= limit <= 10^5
- request_times is sorted in non-decreasing order
Examples
Input: (3, 10, [1, 2, 3, 4, 14])
Expected Output: [True, True, True, False, True]
Explanation: The fourth request arrives while three accepted requests are still inside the last 10 seconds. By time 14, the old accepted requests have expired.
Input: (1, 10, [5, 5])
Expected Output: [True, False]
Explanation: Only one request is allowed in the window, so the second request at the same time is denied.
Input: (1, 10, [1, 11])
Expected Output: [True, True]
Explanation: At time 11, the request from time 1 has just expired because the window is `(1, 11]`.
Input: (2, 5, [])
Expected Output: []
Explanation: Edge case: no requests.
Solution
def solution(limit, window_size, request_times):
from collections import deque
if limit <= 0:
return [False] * len(request_times)
window = deque()
result = []
for t in request_times:
while window and window[0] <= t - window_size:
window.popleft()
if len(window) < limit:
window.append(t)
result.append(True)
else:
result.append(False)
return resultTime complexity: O(n), where n is the number of requests. Space complexity: O(limit) in the worst case for the active window.
Hints
- Only accepted requests inside the current time window matter.
- A queue or deque is enough because timestamps arrive in sorted order.
Part 2: Shared Rate Limiter Across Multiple Threads
Constraints
- 0 <= total number of requests across all threads <= 200000
- 1 <= number of threads <= 10000
- 1 <= window_size <= 10^9
- 1 <= limit <= 10^5
- Each thread's request list is sorted in non-decreasing order
Examples
Input: (2, 5, [[1, 6], [1, 2], [7]])
Expected Output: [[True, True], [True, False], [True]]
Explanation: The requests at time 1 from threads 0 and 1 are both allowed. The request at time 2 is denied because the limiter is already full.
Input: (1, 3, [[], [1, 1], [4]])
Expected Output: [[], [True, False], [True]]
Explanation: Edge case: one thread has no requests.
Input: (2, 4, [[1, 5], [2], [5]])
Expected Output: [[True, True], [True], [False]]
Explanation: At timestamp 5, thread 0 is processed before thread 2, so thread 0 gets the last available slot.
Input: (1, 10, [[3], [3], [3]])
Expected Output: [[True], [False], [False]]
Explanation: With identical timestamps, smaller thread index wins.
Solution
def solution(limit, window_size, thread_requests):
import heapq
from collections import deque
if limit <= 0:
return [[False] * len(reqs) for reqs in thread_requests]
result = [[False] * len(reqs) for reqs in thread_requests]
heap = []
for tid, reqs in enumerate(thread_requests):
if reqs:
heapq.heappush(heap, (reqs[0], tid, 0))
window = deque()
while heap:
t, tid, idx = heapq.heappop(heap)
while window and window[0] <= t - window_size:
window.popleft()
if len(window) < limit:
result[tid][idx] = True
window.append(t)
next_idx = idx + 1
if next_idx < len(thread_requests[tid]):
heapq.heappush(heap, (thread_requests[tid][next_idx], tid, next_idx))
return resultTime complexity: O(N log T), where N is the total number of requests and T is the number of threads. Space complexity: O(T + limit) excluding the output.
Hints
- This is like merging k sorted lists while maintaining one shared sliding window.
- A min-heap can give you the next global request in timestamp order.
Part 3: Rate Limiter With Internal Clock
Constraints
- 0 <= len(operations) <= 200000
- 1 <= window_size <= 10^9
- 1 <= limit <= 10^5
- Advance values are non-negative integers
Examples
Input: (2, 5, [('allow',), ('allow',), ('allow',), ('advance', 5), ('allow',)])
Expected Output: [True, True, False, True]
Explanation: Three immediate requests hit the limit; after advancing 5 seconds, the earlier accepted requests expire.
Input: (1, 10, [('advance', 3), ('advance', 2)])
Expected Output: []
Explanation: Edge case: no `allow` operations.
Input: (1, 3, [('allow',), ('advance', 3), ('allow',), ('allow',)])
Expected Output: [True, True, False]
Explanation: At time 3, the request from time 0 has expired, but the second request at time 3 fills the window.
Input: (3, 2, [('allow',), ('advance', 1), ('allow',), ('advance', 1), ('allow',), ('allow',)])
Expected Output: [True, True, True, True]
Explanation: The request at time 0 expires before the two requests at time 2 are checked.
Solution
def solution(limit, window_size, operations):
from collections import deque
current_time = 0
window = deque()
result = []
for op in operations:
if not op:
continue
if op[0] == 'advance':
current_time += op[1]
elif op[0] == 'allow':
while window and window[0] <= current_time - window_size:
window.popleft()
if limit > 0 and len(window) < limit:
window.append(current_time)
result.append(True)
else:
result.append(False)
else:
raise ValueError('Unknown operation')
return resultTime complexity: O(m), where m is the number of operations. Space complexity: O(limit) in the worst case for the active window.
Hints
- Treat `allow` as reading from stored state, not from a parameter.
- Only accepted request times need to be stored.
Part 4: Binary Search for First Occurrence
Constraints
- 0 <= len(nums) <= 200000
- nums is sorted in non-decreasing order
- -10^9 <= nums[i], target <= 10^9
Examples
Input: ([1, 2, 2, 2, 5], 2)
Expected Output: 1
Explanation: The first 2 appears at index 1.
Input: ([1, 3, 5, 7], 4)
Expected Output: -1
Explanation: The target is not present.
Input: ([], 10)
Expected Output: -1
Explanation: Edge case: empty array.
Input: ([5], 5)
Expected Output: 0
Explanation: Single-element match.
Input: ([1, 1, 1], 1)
Expected Output: 0
Explanation: All elements match, so the first index is 0.
Solution
def solution(nums, target):
left, right = 0, len(nums) - 1
answer = -1
while left <= right:
mid = (left + right) // 2
if nums[mid] >= target:
if nums[mid] == target:
answer = mid
right = mid - 1
else:
left = mid + 1
return answerTime complexity: O(log n). Space complexity: O(1).
Hints
- When you find the target, do not stop immediately if duplicates may exist.
- Think about how to shrink the search space toward the leftmost valid index.
Part 5: Top-K Frequent Elements with a Doubly Linked List
Constraints
- 0 <= len(nums) <= 200000
- -10^9 <= nums[i] <= 10^9
- 0 <= k <= number of distinct values or larger
Examples
Input: ([5, 2, 5, 3, 2, 5, 2, 4], 2)
Expected Output: [2, 5]
Explanation: Values 2 and 5 both have frequency 3, so the smaller value comes first.
Input: ([], 3)
Expected Output: []
Explanation: Edge case: empty stream.
Input: ([1, 2, 3], 5)
Expected Output: [1, 2, 3]
Explanation: All values have the same frequency, so return them in ascending order.
Input: ([4, 4, -1, -1, -1, 2, 2, 2], 3)
Expected Output: [-1, 2, 4]
Explanation: The top frequencies are -1 and 2 with 3 each, then 4 with 2.
Solution
def solution(nums, k):
class Node:
__slots__ = ('freq', 'keys', 'prev', 'next')
def __init__(self, freq):
self.freq = freq
self.keys = set()
self.prev = None
self.next = None
def insert_after(node, new_node):
new_node.prev = node
new_node.next = node.next
node.next.prev = new_node
node.next = new_node
def remove(node):
node.prev.next = node.next
node.next.prev = node.prev
if k <= 0 or not nums:
return []
head = Node(0)
tail = Node(0)
head.next = tail
tail.prev = head
where = {}
for x in nums:
if x not in where:
if head.next is tail or head.next.freq != 1:
insert_after(head, Node(1))
node = head.next
node.keys.add(x)
where[x] = node
else:
cur = where[x]
next_freq = cur.freq + 1
if cur.next is tail or cur.next.freq != next_freq:
insert_after(cur, Node(next_freq))
nxt = cur.next
nxt.keys.add(x)
where[x] = nxt
cur.keys.remove(x)
if not cur.keys:
remove(cur)
result = []
node = tail.prev
while node is not head and len(result) < k:
for x in sorted(node.keys):
result.append(x)
if len(result) == k:
break
node = node.prev
return resultTime complexity: O(n + u log u) in the worst case, where n is the stream length and u is the number of distinct values. Space complexity: O(u).
Hints
- Move an element only from frequency f to frequency f + 1 as you process the stream.
- A map from value to its current bucket lets you update frequencies in O(1) amortized time.
Part 6: Common Free Slots Across Calendars
Constraints
- 1 <= number of people <= 200
- 0 <= total number of busy intervals <= 200000
- 0 <= start < end <= 1440
- Each bounds entry satisfies 0 <= work_start < work_end <= 1440
Examples
Input: ([[[540, 570], [630, 660]], [[585, 600]], [[600, 615]]], [[540, 720], [540, 720], [540, 720]], 15)
Expected Output: [[570, 585], [615, 630], [660, 720]]
Explanation: These are the gaps that remain after merging all busy intervals inside the shared workday.
Input: ([[[60, 120], [90, 150]], []], [[0, 200], [50, 180]], 20)
Expected Output: [[150, 180]]
Explanation: The overlapping intervals on the first calendar merge into one busy block [60, 150).
Input: ([[], []], [[0, 30], [40, 60]], 10)
Expected Output: []
Explanation: Edge case: the working bounds do not overlap.
Input: ([[], []], [[60, 180], [90, 150]], 20)
Expected Output: [[90, 150]]
Explanation: With no busy intervals, the full overlap of the work bounds is available.
Solution
def solution(calendars, bounds, duration):
def merge_intervals(intervals):
intervals = sorted(intervals)
merged = []
for s, e in intervals:
if s >= e:
continue
if not merged or s > merged[-1][1]:
merged.append([s, e])
else:
merged[-1][1] = max(merged[-1][1], e)
return merged
if not calendars or not bounds or len(calendars) != len(bounds):
return []
start = max(b[0] for b in bounds)
end = min(b[1] for b in bounds)
if start >= end:
return []
busy = []
for calendar in calendars:
for s, e in merge_intervals(calendar):
s = max(s, start)
e = min(e, end)
if s < e:
busy.append([s, e])
busy = merge_intervals(busy)
result = []
cursor = start
for s, e in busy:
if cursor < s and s - cursor >= duration:
result.append([cursor, s])
cursor = max(cursor, e)
if cursor < end and end - cursor >= duration:
result.append([cursor, end])
return resultTime complexity: O(B log B), where B is the total number of busy intervals. Space complexity: O(B).
Hints
- First reduce the search space to the intersection of all working bounds.
- Merge overlapping or touching busy intervals before looking for gaps.
Part 7: MapReduce-Style Meeting Scheduler
Constraints
- 0 <= number of shards <= 10000
- 0 <= total busy intervals across all shards <= 200000
- 0 <= day_start < day_end <= 10^9
- Intervals may be unsorted, overlapping, or partially outside the day bounds
Examples
Input: ([[[540, 600], [660, 690]], [[555, 570], [630, 660]], [[600, 630], [690, 720]]], 540, 780, 30)
Expected Output: [[720, 780]]
Explanation: All busy blocks connect into one large interval [540, 720).
Input: ([[[10, 20], [40, 50]], [[15, 25]], [[30, 35]]], 0, 60, 5)
Expected Output: [[0, 10], [25, 30], [35, 40], [50, 60]]
Explanation: These are the gaps where the reduced active meeting count is zero.
Input: ([], 0, 10, 3)
Expected Output: [[0, 10]]
Explanation: Edge case: no busy intervals at all.
Input: ([[[-5, 2], [8, 12]], [[2, 4], [6, 8]]], 0, 10, 2)
Expected Output: [[4, 6]]
Explanation: Intervals are clipped to the day bounds, then touching intervals merge through the event sweep.
Solution
def solution(shards, day_start, day_end, duration):
if day_start >= day_end:
return []
if duration <= 0:
return [[day_start, day_end]]
events = {}
for shard in shards:
for s, e in shard:
s = max(s, day_start)
e = min(e, day_end)
if s >= e:
continue
events[s] = events.get(s, 0) + 1
events[e] = events.get(e, 0) - 1
if not events:
return [[day_start, day_end]] if day_end - day_start >= duration else []
result = []
active = 0
cursor = day_start
for t in sorted(events):
if cursor < t and active == 0 and t - cursor >= duration:
result.append([cursor, t])
active += events[t]
cursor = t
if cursor < day_end and active == 0 and day_end - cursor >= duration:
result.append([cursor, day_end])
return resultTime complexity: O(M log M), where M is the number of emitted event timestamps. Space complexity: O(M).
Hints
- Convert each busy interval into two events: +1 at start and -1 at end.
- After combining events with the same timestamp, sweep from left to right and look for stretches where the active count is zero.
Part 8: Rank and Assign Meeting Rooms by Priority
Constraints
- 0 <= number of rooms <= 200
- 0 <= total initial meetings <= 20000
- 0 <= number of requests <= 5000
- Within a room, initial meetings do not overlap but may be unsorted
- All intervals satisfy start < end
Examples
Input: ({1: [[9, 10], [13, 14]], 2: [[9, 11]], 3: []}, [[10, 11], [11, 12], [13, 14]])
Expected Output: [3, 3, 2]
Explanation: Room 3 is least used for the first two requests. For the third request, room 1 conflicts, so room 2 wins.
Input: ({1: [], 2: []}, [[0, 10]])
Expected Output: [1]
Explanation: Edge case: same priority values, so the smaller room id is chosen.
Input: ({1: [[0, 5]], 2: [[1, 4]]}, [[2, 3], [5, 6]])
Expected Output: [-1, 2]
Explanation: The first request fits nowhere. For the second, both rooms are available, so room 2 wins because it has less total booked time.
Input: ({1: [[0, 30]], 2: [[0, 10], [20, 30]], 3: [[5, 15]]}, [[15, 20], [30, 40]])
Expected Output: [3, 1]
Explanation: Priority values change after each assignment.
Solution
def solution(rooms, requests):
room_ids = sorted(rooms.keys())
schedules = {}
usage = {}
booked = {}
for room_id in room_ids:
schedule = sorted([list(meeting) for meeting in rooms[room_id]])
schedules[room_id] = schedule
usage[room_id] = len(schedule)
booked[room_id] = sum(e - s for s, e in schedule)
def find_position(schedule, start):
lo, hi = 0, len(schedule)
while lo < hi:
mid = (lo + hi) // 2
if schedule[mid][0] < start:
lo = mid + 1
else:
hi = mid
return lo
def can_place(schedule, start, end):
idx = find_position(schedule, start)
if idx > 0 and schedule[idx - 1][1] > start:
return False, idx
if idx < len(schedule) and schedule[idx][0] < end:
return False, idx
return True, idx
result = []
for start, end in requests:
best_room = None
best_key = None
best_idx = None
for room_id in room_ids:
ok, idx = can_place(schedules[room_id], start, end)
if not ok:
continue
key = (usage[room_id], booked[room_id], room_id)
if best_key is None or key < best_key:
best_key = key
best_room = room_id
best_idx = idx
if best_room is None:
result.append(-1)
continue
schedules[best_room].insert(best_idx, [start, end])
usage[best_room] += 1
booked[best_room] += end - start
result.append(best_room)
return resultTime complexity: O(Q * R * log S), where Q is the number of requests, R is the number of rooms, and S is the maximum meetings in any one room. Space complexity: O(total initial meetings + assigned meetings).
Hints
- For each room, keep its meetings sorted by start time so availability can be checked quickly.
- Turn the priority rule into a tuple like `(usage_count, total_booked_duration, room_id)`.