379. Design Phone Directory 🔒
Problem Description
You need to design a phone directory system that manages a pool of phone numbers. The directory starts with a fixed number of available slots (from 0
to maxNumbers - 1
), and you need to implement operations to allocate, check availability, and release phone numbers.
The PhoneDirectory
class should support the following operations:
-
Constructor
PhoneDirectory(int maxNumbers)
: Initialize the directory withmaxNumbers
available slots. Initially, all numbers from0
tomaxNumbers - 1
are available for allocation. -
Get a number
get()
: Allocate and return any available phone number. Once a number is allocated, it becomes unavailable. If no numbers are available, return-1
. -
Check availability
check(int number)
: Check if a specific phone number is currently available (not allocated). Returntrue
if the number is available,false
if it's already allocated. -
Release a number
release(int number)
: Return a previously allocated phone number back to the pool of available numbers, making it available for future allocation again.
The solution uses a hash set to track all available phone numbers. Initially, the set contains all numbers from 0
to maxNumbers - 1
. When get()
is called, it removes and returns any number from the set (or -1
if empty). The check()
method simply verifies if a number exists in the set. The release()
method adds a number back to the available set. All operations run in O(1)
time complexity.
Intuition
The key insight is recognizing that we need to efficiently track which phone numbers are available versus allocated. This is fundamentally a problem of managing two distinct sets - available numbers and allocated numbers.
Since we start with all numbers available and only need to know which ones are free at any point, we can maintain just one set containing the available numbers. Why a set? Because we need fast operations for:
- Removing an arbitrary element when allocating (
get
) - Checking membership when verifying availability (
check
) - Adding elements back when releasing (
release
)
A hash set gives us O(1)
time complexity for all these operations.
The beauty of using set(range(maxNumbers))
for initialization is that it immediately gives us all valid phone numbers. When we call get()
, we can use pop()
to both retrieve and remove any available number in one operation - we don't care which specific number we give out, just that it's available.
For check()
, it's a simple membership test - if the number is in our available set, it hasn't been allocated yet. And release()
just adds the number back to our available pool.
This approach avoids the complexity of tracking allocated numbers separately or maintaining any ordering. We let the set data structure handle the heavy lifting of efficient storage and lookup, while our logic remains simple: available numbers live in the set, allocated numbers don't.
Solution Approach
We implement the phone directory using a hash set called available
to store all unallocated phone numbers.
Initialization (__init__
):
self.available = set(range(maxNumbers))
We create a set containing all numbers from 0
to maxNumbers - 1
. This represents our initial pool of available phone numbers. Using set(range(maxNumbers))
efficiently generates and stores all valid phone numbers.
Getting a number (get
):
def get(self) -> int:
if not self.available:
return -1
return self.available.pop()
First, we check if the set is empty. If it is, no numbers are available, so we return -1
. Otherwise, we use pop()
to remove and return an arbitrary element from the set. The pop()
operation on a set removes and returns any element in O(1)
time - we don't need to care which specific number is returned, just that it's available.
Checking availability (check
):
def check(self, number: int) -> bool:
return number in self.available
This is a straightforward membership test. We simply check if the given number
exists in our available
set. If it's present, the number hasn't been allocated yet and is available. The in
operator on a hash set runs in O(1)
time.
Releasing a number (release
):
def release(self, number: int) -> None:
self.available.add(number)
When a phone number is released, we add it back to the available
set using the add()
method. This makes the number available for future allocation. The add()
operation on a hash set also runs in O(1)
time.
All operations achieve O(1)
time complexity, making this solution highly efficient. The space complexity is O(n)
where n
is maxNumbers
, as we need to store up to maxNumbers
elements in our set.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with maxNumbers = 3
, which gives us phone numbers 0, 1, and 2.
Step 1: Initialize the directory
directory = PhoneDirectory(3) # available = {0, 1, 2}
All three numbers are initially available in our set.
Step 2: Get a phone number
number1 = directory.get() # Returns 2 (could be any of 0, 1, or 2) # available = {0, 1}
The pop()
method removes and returns an arbitrary element. Let's say it returns 2. Now only 0 and 1 remain available.
Step 3: Check if number 2 is available
directory.check(2) # Returns False # available = {0, 1} (unchanged)
Since we just allocated number 2, it's not in the available set anymore, so check(2)
returns False
.
Step 4: Get another number
number2 = directory.get() # Returns 1 # available = {0}
We allocate another number. Let's say it returns 1. Only number 0 remains.
Step 5: Release number 2 back to the pool
directory.release(2) # available = {0, 2}
Number 2 is added back to the available set and can be allocated again.
Step 6: Check availability
directory.check(2) # Returns True directory.check(1) # Returns False directory.check(0) # Returns True
Number 2 is available (we just released it), number 1 is still allocated, and number 0 was never allocated.
Step 7: Get remaining numbers
directory.get() # Returns 2 directory.get() # Returns 0 directory.get() # Returns -1 (no numbers left) # available = {} (empty set)
After allocating the last two available numbers, the set becomes empty. Any subsequent get()
calls return -1 to indicate no numbers are available.
This example demonstrates how the hash set efficiently tracks available numbers, removing them when allocated and adding them back when released.
Solution Implementation
1class PhoneDirectory:
2 """
3 A phone directory system that manages available phone numbers.
4 Numbers are allocated from a pool of available numbers ranging from 0 to maxNumbers-1.
5 """
6
7 def __init__(self, max_numbers: int) -> None:
8 """
9 Initialize the phone directory with a pool of available numbers.
10
11 Args:
12 max_numbers: The maximum count of phone numbers (0 to max_numbers-1)
13 """
14 # Create a set containing all available phone numbers from 0 to max_numbers-1
15 self.available = set(range(max_numbers))
16
17 def get(self) -> int:
18 """
19 Allocate and return an available phone number.
20
21 Returns:
22 An available phone number if one exists, otherwise -1
23 """
24 # Check if there are any available numbers
25 if not self.available:
26 return -1
27
28 # Remove and return an arbitrary number from the available set
29 return self.available.pop()
30
31 def check(self, number: int) -> bool:
32 """
33 Check if a specific phone number is available.
34
35 Args:
36 number: The phone number to check
37
38 Returns:
39 True if the number is available, False otherwise
40 """
41 # Check if the number exists in the available set
42 return number in self.available
43
44 def release(self, number: int) -> None:
45 """
46 Release a phone number back to the available pool.
47
48 Args:
49 number: The phone number to release
50 """
51 # Add the number back to the available set
52 # Note: If number is already available, set.add() will have no effect
53 self.available.add(number)
54
55
56# Your PhoneDirectory object will be instantiated and called as such:
57# obj = PhoneDirectory(maxNumbers)
58# param_1 = obj.get()
59# param_2 = obj.check(number)
60# obj.release(number)
61
1/**
2 * Phone Directory management system that handles allocation and release of phone numbers.
3 * Maintains a pool of available phone numbers from 0 to maxNumbers-1.
4 */
5class PhoneDirectory {
6 // Set to store all currently available phone numbers
7 private Set<Integer> availableNumbers;
8
9 /**
10 * Initializes the phone directory with a range of available numbers.
11 * @param maxNumbers The total number of phone numbers (0 to maxNumbers-1)
12 */
13 public PhoneDirectory(int maxNumbers) {
14 availableNumbers = new HashSet<>();
15
16 // Initialize all numbers from 0 to maxNumbers-1 as available
17 for (int i = 0; i < maxNumbers; i++) {
18 availableNumbers.add(i);
19 }
20 }
21
22 /**
23 * Provides an available phone number and marks it as unavailable.
24 * @return An available phone number, or -1 if no numbers are available
25 */
26 public int get() {
27 // Check if there are any available numbers
28 if (availableNumbers.isEmpty()) {
29 return -1;
30 }
31
32 // Get any available number using iterator
33 int phoneNumber = availableNumbers.iterator().next();
34
35 // Remove the number from available pool
36 availableNumbers.remove(phoneNumber);
37
38 return phoneNumber;
39 }
40
41 /**
42 * Checks whether a specific phone number is available.
43 * @param number The phone number to check
44 * @return true if the number is available, false otherwise
45 */
46 public boolean check(int number) {
47 return availableNumbers.contains(number);
48 }
49
50 /**
51 * Releases a phone number back to the available pool.
52 * @param number The phone number to release
53 */
54 public void release(int number) {
55 availableNumbers.add(number);
56 }
57}
58
59/**
60 * Your PhoneDirectory object will be instantiated and called as such:
61 * PhoneDirectory obj = new PhoneDirectory(maxNumbers);
62 * int param_1 = obj.get();
63 * boolean param_2 = obj.check(number);
64 * obj.release(number);
65 */
66
1class PhoneDirectory {
2public:
3 /**
4 * Initialize phone directory with maximum available phone numbers [0, maxNumbers)
5 * @param maxNumbers: The maximum count of phone numbers in the directory
6 */
7 PhoneDirectory(int maxNumbers) {
8 // Add all phone numbers from 0 to maxNumbers-1 to the available set
9 for (int i = 0; i < maxNumbers; ++i) {
10 available_numbers_.insert(i);
11 }
12 }
13
14 /**
15 * Provide an available phone number and mark it as used
16 * @return: An available phone number, or -1 if no number is available
17 */
18 int get() {
19 // Check if there are any available numbers
20 if (available_numbers_.empty()) {
21 return -1;
22 }
23
24 // Get the first available number from the set
25 int phone_number = *available_numbers_.begin();
26
27 // Remove the number from available set (mark as used)
28 available_numbers_.erase(phone_number);
29
30 return phone_number;
31 }
32
33 /**
34 * Check if a specific phone number is available
35 * @param number: The phone number to check
36 * @return: true if the number is available, false otherwise
37 */
38 bool check(int number) {
39 // Check if the number exists in the available set
40 return available_numbers_.count(number) > 0;
41 }
42
43 /**
44 * Release a phone number back to the pool of available numbers
45 * @param number: The phone number to release
46 */
47 void release(int number) {
48 // Add the number back to the available set
49 available_numbers_.insert(number);
50 }
51
52private:
53 // Set to store all currently available phone numbers
54 std::unordered_set<int> available_numbers_;
55};
56
57/**
58 * Your PhoneDirectory object will be instantiated and called as such:
59 * PhoneDirectory* obj = new PhoneDirectory(maxNumbers);
60 * int param_1 = obj->get();
61 * bool param_2 = obj->check(number);
62 * obj->release(number);
63 */
64
1// Set to store all available phone numbers
2let available: Set<number> = new Set();
3
4/**
5 * Initializes the phone directory with a range of available numbers
6 * @param maxNumbers - The maximum count of phone numbers (0 to maxNumbers-1)
7 */
8function initPhoneDirectory(maxNumbers: number): void {
9 available = new Set();
10 // Add all numbers from 0 to maxNumbers-1 to the available set
11 for (let i = 0; i < maxNumbers; i++) {
12 available.add(i);
13 }
14}
15
16/**
17 * Provides an available phone number and marks it as taken
18 * @returns An available phone number, or -1 if none are available
19 */
20function get(): number {
21 // Get the first element from the Set using destructuring
22 const [firstAvailableNumber] = available;
23
24 // Check if there are no available numbers
25 if (firstAvailableNumber === undefined) {
26 return -1;
27 }
28
29 // Remove the number from available pool and return it
30 available.delete(firstAvailableNumber);
31 return firstAvailableNumber;
32}
33
34/**
35 * Checks if a specific phone number is available
36 * @param number - The phone number to check
37 * @returns true if the number is available, false otherwise
38 */
39function check(number: number): boolean {
40 return available.has(number);
41}
42
43/**
44 * Releases a phone number back to the available pool
45 * @param number - The phone number to release
46 */
47function release(number: number): void {
48 available.add(number);
49}
50
Time and Space Complexity
Time Complexity:
__init__(maxNumbers)
:O(n)
wheren
is the value ofmaxNumbers
, as we need to create a set containing all numbers from0
tomaxNumbers - 1
.get()
:O(1)
on average. Thepop()
operation on a set has average caseO(1)
time complexity.check(number)
:O(1)
on average. Checking membership in a set is anO(1)
average case operation.release(number)
:O(1)
on average. Adding an element to a set is anO(1)
average case operation.
Space Complexity:
O(n)
where n
is the value of maxNumbers
. The primary space usage comes from the self.available
set, which in the worst case (when all numbers are available) stores n
integers from 0
to maxNumbers - 1
. This dominates the space complexity of the entire data structure.
Common Pitfalls
1. Not Handling Invalid Release Operations
The current implementation doesn't validate whether a number being released was actually allocated before. This can lead to incorrect behavior where:
- Releasing a number that was never allocated (e.g., releasing number 100 when maxNumbers is 50)
- Releasing the same number multiple times
- Having duplicate numbers in the available pool
Example of the problem:
directory = PhoneDirectory(3) # Numbers 0, 1, 2 available num = directory.get() # Get number 2 (for example) directory.release(2) # Release it back directory.release(2) # Release again - no error but creates duplicate directory.release(5) # Release invalid number - adds number outside range
Solution: Add validation to ensure only valid, previously allocated numbers can be released:
def __init__(self, max_numbers: int) -> None:
self.available = set(range(max_numbers))
self.max_numbers = max_numbers # Store the maximum limit
def release(self, number: int) -> None:
# Validate the number is within valid range
if 0 <= number < self.max_numbers:
# Only add if not already available (prevents duplicates)
self.available.add(number)
2. Thread Safety Issues in Concurrent Environments
The current implementation is not thread-safe. If multiple threads access the PhoneDirectory simultaneously, race conditions can occur:
- Two threads calling
get()
might receive the same number - A thread checking availability while another is allocating might get inconsistent results
Solution: Use threading locks to ensure atomic operations:
import threading
def __init__(self, max_numbers: int) -> None:
self.available = set(range(max_numbers))
self.lock = threading.Lock()
def get(self) -> int:
with self.lock:
if not self.available:
return -1
return self.available.pop()
def check(self, number: int) -> bool:
with self.lock:
return number in self.available
def release(self, number: int) -> None:
with self.lock:
self.available.add(number)
3. Memory Inefficiency for Large Number Ranges
When maxNumbers
is very large (e.g., 10 million), storing all available numbers in a set consumes significant memory even when most numbers are allocated.
Alternative Solution for Large Ranges: Use a set to track only allocated numbers instead of available ones:
def __init__(self, max_numbers: int) -> None:
self.allocated = set()
self.max_numbers = max_numbers
self.next_available = 0 # Track next number to try
def get(self) -> int:
if len(self.allocated) >= self.max_numbers:
return -1
# Find next available number
while self.next_available in self.allocated:
self.next_available += 1
if self.next_available >= self.max_numbers:
# Wrap around and search from beginning
for i in range(self.max_numbers):
if i not in self.allocated:
self.allocated.add(i)
return i
return -1
num = self.next_available
self.allocated.add(num)
return num
def check(self, number: int) -> bool:
return 0 <= number < self.max_numbers and number not in self.allocated
def release(self, number: int) -> None:
if number in self.allocated:
self.allocated.remove(number)
self.next_available = min(self.next_available, number)
This approach uses less memory when most numbers are allocated, as it only tracks the allocated set rather than the available set.
Which of the following uses divide and conquer strategy?
Recommended Readings
Queue Intro Think of the last time you stood in line to buy movie tickets The first person in line gets their ticket first and any newcomers join the end of the line This system where the first person to arrive is the first to be served is a queue in real
Linked List Cycle Given a linked list with potentially a loop determine whether the linked list from the first node contains a cycle in it For bonus points do this with constant space Parameters nodes The first node of a linked list with potentially a loop Result Whether there is a loop contained
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!