1. 问题背景与需求分析
电话目录管理系统是LeetCode第379题,属于设计类题目中的经典案例。这类题目在实际面试中经常出现,特别是对于初级到中级开发岗位的考察。题目要求我们设计一个系统,能够高效地分配和回收电话号码资源。
这类系统在现实世界中有广泛的应用场景:
- 电信运营商的号码池管理
- 企业内部分机号分配
- 酒店房间号管理系统
- 云计算中的资源ID分配
核心需求可以归纳为三个基本操作:
- 获取一个可用的号码(get)
- 检查某个号码是否可用(check)
- 释放/回收一个正在使用的号码(release)
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据结构选型与复杂度分析
2.1 直观解法与性能瓶颈
最直观的解法是使用一个布尔数组来标记号码的使用状态。初始化时所有号码都可用(true),当号码被分配时标记为false。这种实现方式:
- get操作:O(n)时间遍历数组查找第一个true
- check操作:O(1)时间直接访问数组元素
- release操作:O(1)时间修改数组元素
这种实现虽然简单,但get操作在最坏情况下需要遍历整个数组,当号码池很大时(比如百万级),性能会成为瓶颈。
2.2 优化思路与数据结构选择
为了提高get操作的效率,我们需要一种能快速找到可用号码的数据结构。常见的选择有:
-
队列(Queue):
- 将可用号码预先存入队列
- get操作变为O(1)的出队操作
- release操作需要O(1)的入队操作
-
堆(Heap):
- 使用最小堆存储可用号码
- get操作是O(log n)的堆顶取出
- release操作是O(log n)的堆插入
-
位图(Bitmap)+ 队列:
- 用位图记录号码使用状态(节省空间)
- 用队列存储可用号码(快速获取)
经过比较,队列方案在平均情况下性能最优,实现也最简单。下面是具体实现时的考虑:
java复制class PhoneDirectory {
private Queue<Integer> available;
private Set<Integer> used;
private int maxNumbers;
public PhoneDirectory(int maxNumbers) {
this.maxNumbers = maxNumbers;
available = new LinkedList<>();
for (int i = 0; i < maxNumbers; i++) {
available.offer(i);
}
used = new HashSet<>();
}
public int get() {
if (available.isEmpty()) {
return -1;
}
int num = available.poll();
used.add(num);
return num;
}
public boolean check(int number) {
if (number < 0 || number >= maxNumbers) {
return false;
}
return !used.contains(number);
}
public void release(int number) {
if (used.remove(number)) {
available.offer(number);
}
}
}
3. 边界条件与异常处理
3.1 输入验证
在实现这类系统时,必须考虑各种边界情况:
- 初始化时maxNumbers为0或负数
- 请求的号码超出范围(负数或≥maxNumbers)
- 重复释放同一个号码
- 号码池耗尽时的get请求
3.2 线程安全考虑
虽然LeetCode题目通常不要求线程安全,但在实际工程中这是必须考虑的。我们可以通过以下方式保证线程安全:
java复制public class ConcurrentPhoneDirectory {
private final Queue<Integer> available = new ConcurrentLinkedQueue<>();
private final Set<Integer> used = ConcurrentHashMap.newKeySet();
private final int maxNumbers;
public ConcurrentPhoneDirectory(int maxNumbers) {
this.maxNumbers = maxNumbers;
for (int i = 0; i < maxNumbers; i++) {
available.offer(i);
}
}
public int get() {
Integer num = available.poll();
if (num == null) {
return -1;
}
used.add(num);
return num;
}
public boolean check(int number) {
if (number < 0 || number >= maxNumbers) {
return false;
}
return !used.contains(number);
}
public void release(int number) {
if (number >= 0 && number < maxNumbers && used.remove(number)) {
available.offer(number);
}
}
}
4. 测试用例设计与验证
4.1 基础功能测试
完整的测试应该包含以下场景:
- 正常获取和释放号码
- 号码池耗尽测试
- 边界号码测试(0和maxNumbers-1)
- 非法输入测试
- 并发环境下的正确性测试
示例测试代码:
java复制@Test
public void testPhoneDirectory() {
PhoneDirectory pd = new PhoneDirectory(3);
// 初始状态检查
assertTrue(pd.check(0));
assertTrue(pd.check(1));
assertTrue(pd.check(2));
assertFalse(pd.check(3)); // 超出范围
// 正常获取
assertEquals(0, pd.get());
assertEquals(1, pd.get());
assertTrue(pd.check(0)); // 0已被获取
assertTrue(pd.check(1)); // 1已被获取
// 释放并重新获取
pd.release(0);
assertTrue(pd.check(0)); // 0已释放
assertEquals(0, pd.get()); // 应重新获取0
// 号码池耗尽
assertEquals(2, pd.get()); // 获取最后一个可用号码
assertEquals(-1, pd.get()); // 已无可用号码
}
4.2 性能测试
对于大规模号码池(如1,000,000个号码),我们需要验证:
- 初始化时间
- 连续get操作的吞吐量
- 混合get/release操作的性能
- 内存占用情况
5. 实际工程中的扩展考虑
5.1 持久化存储
在实际系统中,我们可能需要将号码状态持久化到数据库。这时可以考虑:
- 使用数据库表存储号码状态
- 结合缓存提高性能
- 批量操作减少数据库访问
5.2 号码回收策略
有些场景下需要自动回收长期未使用的号码:
- 添加最后使用时间戳
- 后台线程定期扫描并回收过期号码
- 使用弱引用或软引用管理号码
5.3 分布式环境
在分布式系统中,我们需要考虑:
- 使用分布式锁协调号码分配
- 采用分片策略(不同节点管理不同号码段)
- 使用Redis等分布式数据结构
6. 类似题目与变种
掌握这个模式后,可以解决许多类似的资源管理问题:
- LeetCode 622:设计循环队列
- LeetCode 146:LRU缓存机制
- LeetCode 460:LFU缓存
- 酒店房间管理系统
- IP地址池管理
这类问题的共同特点是需要高效管理有限资源的分配与回收,核心在于选择合适的数据结构来优化最频繁的操作。
