Facebook Pixel

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:

  1. Constructor PhoneDirectory(int maxNumbers): Initialize the directory with maxNumbers available slots. Initially, all numbers from 0 to maxNumbers - 1 are available for allocation.

  2. 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.

  3. Check availability check(int number): Check if a specific phone number is currently available (not allocated). Return true if the number is available, false if it's already allocated.

  4. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 Evaluator

Example 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) where n is the value of maxNumbers, as we need to create a set containing all numbers from 0 to maxNumbers - 1.
  • get(): O(1) on average. The pop() operation on a set has average case O(1) time complexity.
  • check(number): O(1) on average. Checking membership in a set is an O(1) average case operation.
  • release(number): O(1) on average. Adding an element to a set is an O(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.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of the following uses divide and conquer strategy?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More