LeetCode 379 电话目录管理系统


文章目录

    • 摘要
    • 描述
    • 题解答案
    • 题解代码分析
      • 1. 数据结构的选择
      • 2. 初始化方法
      • 3. get() 方法:分配号码
      • 4. check() 方法:检查号码是否可用
      • 5. release() 方法:释放号码
      • 6. 为什么使用 Set + Array 的组合?
      • 7. 边界情况处理
    • 示例测试及结果
      • 示例 1:基本操作
      • 示例 2:边界情况测试
      • 示例 3:重复释放测试
      • 示例 4:大量操作测试
    • 时间复杂度
    • 空间复杂度
    • 实际应用场景
      • 场景一:资源池管理
      • 场景二:ID 分配器
      • 场景三:端口号管理
      • 场景四:会议室预订系统
    • 总结

摘要

这道题其实挺有意思的,它让我们设计一个电话目录管理系统。听起来像是实际业务场景中的问题,但实际上是一道经典的数据结构设计题。我们需要实现三个操作:分配号码、检查号码是否可用、释放号码。这三个操作都需要高效地执行,所以选择合适的数据结构很重要。

这道题的核心在于如何高效地管理可用号码的集合,既要能快速分配,又要能快速检查状态,还要能快速释放。今天我们就用 Swift 来搞定这道题,顺便聊聊这种设计模式在实际开发中的应用场景。

描述

题目要求是这样的:设计一个电话目录管理系统,这个系统初始化时有maxNumbers个电话号码槽位,从0maxNumbers - 1。我们需要实现以下三个操作:

  1. get():分配一个还没被使用的号码,返回那个号码。如果没有可用号码了,返回-1
  2. check(number):检查某个号码是否可用(即没被分配出去)。如果可用返回true,否则返回false
  3. release(number):将一个已经分配出去的号码释放,使其可以被后续get()再次分配。

示例:

PhoneDirectory directory = PhoneDirectory(3); // 初始化后,可用号码:0, 1, 2 directory.get(); // 返回 0(或其他可用号码) directory.get(); // 返回 1(或其他可用号码) directory.check(2); // 返回 true,因为 2 还没被分配 directory.get(); // 返回 2 directory.check(2); // 返回 false,因为 2 已经被分配了 directory.release(2); // 释放号码 2 directory.check(2); // 返回 true,因为 2 已经被释放,现在可用了

约束:

  • 1 <= maxNumbers <= 10^4
  • 0 <= number < maxNumbers
  • 总调用次数最多接近2 * 10^4

这道题的核心思路是什么呢?我们需要维护一个可用号码的集合,能够快速地进行分配、检查和释放操作。可以用 Set 来存储可用号码,用队列或数组来快速分配号码。这样get()check()都能达到 O(1) 的时间复杂度。

题解答案

下面是完整的 Swift 解决方案:

classPhoneDirectory{privateletmaxNumbers:Intprivatevaravailable:Set<Int>privatevarqueue:[Int]init(_maxNumbers:Int){self.maxNumbers=maxNumbersself.available=Set(0..<maxNumbers)self.queue=Array(0..<maxNumbers)}/// 分配一个可用号码,如果没有就返回 -1funcget()->Int{guard!queue.isEmptyelse{return-1}letnum=queue.removeLast()available.remove(num)returnnum}/// 检查某号码是否可用funccheck(_number:Int)->Bool{guardnumber>=0&&number<maxNumberselse{returnfalse}returnavailable.contains(number)}/// 释放一个号码,让它可再次被分配funcrelease(_number:Int){guardnumber>=0&&number<maxNumberselse{return}// 只有之前被分配(即当前不可用)的才需要释放if!available.contains(number){available.insert(number)queue.append(number)}}}

题解代码分析

让我们一步步分析这个解决方案:

1. 数据结构的选择

这道题的关键在于选择合适的数据结构来高效地支持三个操作:

privateletmaxNumbers:Intprivatevaravailable:Set<Int>privatevarqueue:[Int]

我们使用了三个数据结构:

  • maxNumbers:记录最大号码数,用于边界检查
  • available:一个Set<Int>,存储所有当前可用的号码。Set 的查找和插入操作平均时间复杂度都是 O(1),非常适合快速检查号码是否可用
  • queue:一个Array<Int>,维护未分配的号码序列,用于快速分配号码。我们使用数组而不是真正的队列,因为我们可以从尾部移除元素,这样操作是 O(1) 的

2. 初始化方法

init(_maxNumbers:Int){self.maxNumbers=maxNumbersself.available=Set(0..<maxNumbers)self.queue=Array(0..<maxNumbers)}

初始化时,我们需要:

  1. 保存maxNumbers用于后续的边界检查
  2. 将所有号码0maxNumbers - 1都加入到available集合中
  3. 将所有号码也加入到queue数组中,这样get()操作可以快速分配

时间复杂度是 O(maxNumbers),因为需要初始化集合和数组。

3. get() 方法:分配号码

funcget()->Int{guard!queue.isEmptyelse{return-1}letnum=queue.removeLast()available.remove(num)returnnum}

get()方法的逻辑是:

  1. 检查是否有可用号码:如果queue为空,说明没有可用号码了,返回-1
  2. 从队列中取出一个号码:使用removeLast()从数组尾部移除一个元素,这是 O(1) 操作
  3. 从可用集合中移除:将这个号码从available集合中移除,标记为已分配
  4. 返回号码:返回分配到的号码

时间复杂度是 O(1)(平均情况),因为removeLast()Set.remove()都是 O(1) 操作。

4. check() 方法:检查号码是否可用

funccheck(_number:Int)->Bool{guardnumber>=0&&number<maxNumberselse{returnfalse}returnavailable.contains(number)}

check()方法的逻辑是:

  1. 边界检查:首先检查number是否在合法范围内(0 <= number < maxNumbers)。如果不在范围内,直接返回false
  2. 检查是否在可用集合中:使用available.contains(number)检查号码是否在可用集合中。如果在,说明可用,返回true;否则返回false

时间复杂度是 O(1)(平均情况),因为Set.contains()是 O(1) 操作。

5. release() 方法:释放号码

funcrelease(_number:Int){guardnumber>=0&&number<maxNumberselse{return}// 只有之前被分配(即当前不可用)的才需要释放if!available.contains(number){available.insert(number)queue.append(number)}}

release()方法的逻辑是:

  1. 边界检查:首先检查number是否在合法范围内。如果不在范围内,直接返回,不做任何操作
  2. 检查是否需要释放:检查号码是否当前不在available集合中(即已被分配)。如果已经在集合中(即已经可用),就不需要重复释放,直接返回
  3. 释放号码:如果号码已被分配,需要:
    • 将号码加入到available集合中,标记为可用
    • 将号码加入到queue数组的尾部,这样后续get()操作可以再次分配这个号码

时间复杂度是 O(1)(平均情况),因为Set.contains()Set.insert()Array.append()都是 O(1) 操作。

6. 为什么使用 Set + Array 的组合?

我们同时使用SetArray的原因:

  • Set 的优势check()操作需要快速检查号码是否可用,Set 的contains()操作是 O(1),非常适合这个需求
  • Array 的优势get()操作需要快速分配一个号码,Array 的removeLast()操作是 O(1),非常适合这个需求
  • 两者配合release()操作需要同时更新两个数据结构,虽然看起来有冗余,但能保证两个操作都是 O(1)

如果只用 Set,get()操作需要遍历 Set 来找到一个可用号码,时间复杂度会变成 O(n)。如果只用 Array,check()操作需要遍历数组来检查号码是否存在,时间复杂度也会变成 O(n)。所以两者结合使用是最优解。

7. 边界情况处理

代码中处理了几个重要的边界情况:

  1. get()时没有可用号码:返回-1
  2. check()时号码超出范围:返回false
  3. release()时号码超出范围:直接返回,不做任何操作
  4. release()时重复释放:检查号码是否已经在可用集合中,如果是就不重复释放

这些边界情况的处理确保了代码的健壮性。

示例测试及结果

让我们用几个例子来测试一下这个解决方案:

示例 1:基本操作

letdirectory=PhoneDirectory(3)print("初始化后,可用号码:0, 1, 2")print("---")letnum1=directory.get()print("get() 返回:\(num1)")print("check(\(num1)):\(directory.check(num1))")// falseletnum2=directory.get()print("get() 返回:\(num2)")print("check(\(num2)):\(directory.check(num2))")// falseprint("check(2):\(directory.check(2))")// true(如果 2 还没被分配)letnum3=directory.get()print("get() 返回:\(num3)")print("check(2):\(directory.check(2))")// false(现在 2 已经被分配了)directory.release(2)print("release(2) 后")print("check(2):\(directory.check(2))")// true(2 现在可用了)letnum4=directory.get()print("get() 返回:\(num4)")// 可能是 2

可能的输出:

初始化后,可用号码:0, 1, 2 --- get() 返回: 2 check(2): false get() 返回: 1 check(1): false check(2): false get() 返回: 0 check(2): false release(2) 后 check(2): true get() 返回: 2

注意:由于我们使用removeLast(),号码的分配顺序是从大到小的(2, 1, 0),但这是正常的,因为题目没有要求特定的分配顺序。

示例 2:边界情况测试

letdirectory=PhoneDirectory(3)// 测试边界检查print("check(-1):\(directory.check(-1))")// falseprint("check(3):\(directory.check(3))")// falseprint("check(0):\(directory.check(0))")// true// 分配所有号码letnum1=directory.get()letnum2=directory.get()letnum3=directory.get()print("分配了三个号码:\(num1),\(num2),\(num3)")// 尝试再分配一个letnum4=directory.get()print("get() 返回:\(num4)")// -1// 释放一个号码directory.release(num1)print("release(\(num1))后")print("check(\(num1)):\(directory.check(num1))")// true// 再次分配letnum5=directory.get()print("get() 返回:\(num5)")// 应该是 num1

输出:

check(-1): false check(3): false check(0): true 分配了三个号码: 2, 1, 0 get() 返回: -1 release(2) 后 check(2): true get() 返回: 2

示例 3:重复释放测试

letdirectory=PhoneDirectory(3)letnum=directory.get()print("分配号码:\(num)")directory.release(num)print("第一次 release(\(num))")print("check(\(num)):\(directory.check(num))")// truedirectory.release(num)print("第二次 release(\(num))(重复释放)")print("check(\(num)):\(directory.check(num))")// 仍然是 true,不会出错

输出:

分配号码: 2 第一次 release(2) check(2): true 第二次 release(2)(重复释放) check(2): true

重复释放不会导致错误,因为我们的代码会检查号码是否已经在可用集合中。

示例 4:大量操作测试

letdirectory=PhoneDirectory(1000)// 分配 500 个号码varallocated:[Int]=[]for_in0..<500{letnum=directory.get()allocated.append(num)}print("分配了 500 个号码")print("check(0):\(directory.check(0))")// false(如果 0 被分配了)// 释放前 100 个foriin0..<100{directory.release(allocated[i])}print("释放了 100 个号码")print("check(\(allocated[0])):\(directory.check(allocated[0]))")// true// 再分配 100 个for_in0..<100{letnum=directory.get()print("重新分配号码:\(num)")}

这个测试展示了系统在处理大量操作时的正确性。

时间复杂度

让我们分析一下每个操作的时间复杂度:

操作时间复杂度说明
init(_ maxNumbers: Int)O(maxNumbers)需要初始化 Set 和 Array,每个号码都要插入一次
get()O(1) 平均removeLast()是 O(1),Set.remove()平均 O(1)
check(_ number: Int)O(1) 平均Set.contains()平均 O(1)
release(_ number: Int)O(1) 平均Set.contains()Set.insert()Array.append()都是 O(1)

总体时间复杂度:

  • 初始化:O(maxNumbers)
  • 每次操作:O(1) 平均时间复杂度

对于题目约束(maxNumbers <= 10^4,总调用次数接近2 * 10^4),这个时间复杂度是完全可接受的。

空间复杂度

空间复杂度分析:

  • maxNumbers:一个整数,O(1)
  • available:一个 Set,最多存储maxNumbers个整数,O(maxNumbers)
  • queue:一个 Array,最多存储maxNumbers个整数,O(maxNumbers)

总空间复杂度:O(maxNumbers)

虽然我们使用了两个数据结构(Set 和 Array),但它们存储的是相同的数据(可用号码),所以空间复杂度仍然是 O(maxNumbers),这是必要的,因为我们需要同时支持 O(1) 的查找和分配操作。

实际应用场景

这种电话目录管理系统的设计模式在实际开发中应用非常广泛:

场景一:资源池管理

在很多系统中,我们需要管理资源池,比如:

  • 数据库连接池:分配和释放数据库连接
  • 线程池:分配和释放线程
  • 对象池:分配和释放对象

这些场景都需要类似的操作:分配资源、检查资源是否可用、释放资源。

// 伪代码示例:数据库连接池classConnectionPool{privatevaravailable:Set<Connection>privatevarqueue:[Connection]funcgetConnection()->Connection?{// 类似 get() 操作}funcisAvailable(_connection:Connection)->Bool{// 类似 check() 操作}funcrelease(_connection:Connection){// 类似 release() 操作}}

场景二:ID 分配器

在很多系统中,我们需要分配唯一的 ID,比如:

  • 用户 ID 分配:为新用户分配唯一 ID
  • 订单号分配:为新订单分配唯一订单号
  • 会话 ID 分配:为新会话分配唯一会话 ID
// 伪代码示例:用户 ID 分配器classUserIDAllocator{privatevaravailable:Set<Int>privatevarqueue:[Int]funcallocateUserID()->Int{// 类似 get() 操作}funcisUserIDAvailable(_id:Int)->Bool{// 类似 check() 操作}funcreleaseUserID(_id:Int){// 类似 release() 操作(比如用户注销时)}}

场景三:端口号管理

在网络编程中,我们需要管理端口号:

// 伪代码示例:端口号管理器classPortManager{privatevaravailable:Set<Int>privatevarqueue:[Int]funcallocatePort()->Int?{// 分配一个可用端口}funcisPortAvailable(_port:Int)->Bool{// 检查端口是否可用}funcreleasePort(_port:Int){// 释放端口}}

场景四:会议室预订系统

在会议室预订系统中,我们需要管理会议室的可用性:

// 伪代码示例:会议室管理器classMeetingRoomManager{privatevaravailable:Set<Int>// 可用会议室编号privatevarqueue:[Int]funcbookRoom()->Int?{// 预订一个会议室}funcisRoomAvailable(_roomId:Int)->Bool{// 检查会议室是否可用}funcreleaseRoom(_roomId:Int){// 释放会议室}}

总结

这道题虽然看起来简单,但实际上涉及了很多重要的设计思想:

  1. 数据结构的选择:选择合适的数据结构来支持不同的操作。Set 适合快速查找,Array 适合快速分配,两者结合使用能达到最优性能。

  2. 时间复杂度优化:通过合理的数据结构选择,让所有操作都达到 O(1) 的平均时间复杂度。

  3. 边界情况处理:正确处理边界情况,如号码超出范围、重复释放等,确保代码的健壮性。

  4. 实际应用:这种设计模式在实际开发中应用广泛,如资源池管理、ID 分配、端口管理等。

关键点总结:

  • 使用Set来快速检查号码是否可用(O(1) 查找)
  • 使用Array来快速分配号码(O(1) 移除尾部元素)
  • 两者配合使用,保证所有操作都是 O(1) 时间复杂度
  • 正确处理边界情况,确保代码健壮性

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1190655.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

量子计算模拟器性能基准测试方法论

随着量子算法在金融建模、药物研发等领域的应用突破&#xff0c;量子计算模拟器已成为经典计算机环境验证量子程序的核心工具。软件测试从业者亟需建立一套针对量子特性的标准化基准测试体系。本文旨在系统阐述测试框架的设计原则、关键性能指标及工具链实践方案&#xff0c;为…

基于微信小程序的电子元器件商城系统源码文档部署文档代码讲解等

课题介绍本课题旨在开发一款基于微信小程序的电子元器件商城系统&#xff0c;适配电子元器件品类多、规格杂、采购场景多元的特性&#xff0c;解决传统采购渠道分散、比价繁琐、库存查询不便等痛点。系统以微信小程序为前端载体&#xff0c;依托Node.js搭建后端服务&#xff0c…

【Linux 网络】拒绝传输卡顿!滑动窗口如何让数据 “跑赢” 等待?

一、滑动窗口滑动窗口大小&#xff1a;指的是无需等待确认应答而可以继续发送数据的最大值&#xff1b;注意&#xff1a;这里的无需等待确认应答&#xff0c;不是不要确认应答&#xff0c;而是暂时不要&#xff1b;站在发送方&#xff08;主机A 视角&#xff09;&#xff1a;图…

硬核干货:Checkpoint对齐诅咒与Timer风暴——Flink周期性反压的终极排查

第一章&#xff1a;那只准时敲门的“幽灵”——Checkpoint与其背后的IO风暴我们拿到的是一个极其诡异的现场&#xff1a;每30分钟一次&#xff0c;持续5分钟的反压。这不像是因为数据倾斜导致的“长尾”&#xff0c;也不像代码逻辑死循环导致的“猝死”。它太规律了&#xff0c…

基于微信小程序的付费自习室系统源码文档部署文档代码讲解等

课题介绍本课题聚焦付费自习室行业数字化需求&#xff0c;设计并实现一款基于微信小程序的付费自习室系统&#xff0c;解决传统自习室预约繁琐、计费不透明、座位管理低效等痛点。系统以微信小程序为前端交互入口&#xff0c;采用Node.js搭建后端服务&#xff0c;搭配MySQL数据…

基于微信小程序的高校毕业生公考助手系统源码文档部署文档代码讲解等

课题介绍本课题针对高校毕业生公考备考信息零散、规划混乱、刷题低效等痛点&#xff0c;设计并实现一款基于微信小程序的高校毕业生公考助手系统&#xff0c;为毕业生提供一站式公考备考服务。系统以微信小程序为前端载体&#xff0c;采用Node.js搭建后端服务&#xff0c;结合M…

边缘计算节点延迟专项测试实践指南

1. 测试概述与重要性 边缘计算节点的延迟直接影响实时应用性能&#xff08;如工业自动化、车联网&#xff09;&#xff0c;延迟过高可能导致业务中断或数据不一致。专项测试需评估端到端响应时间、抖动及丢包率等指标&#xff0c;确保节点在5G等低延迟场景下满足SLA要求&#…

大数据领域Kafka的性能调优实战

大数据领域Kafka的性能调优实战&#xff1a;从青铜到王者的进阶指南 关键词&#xff1a;Kafka性能调优、生产者优化、Broker配置、消费者调优、吞吐量与延迟 摘要&#xff1a;在大数据时代&#xff0c;Kafka作为分布式消息队列和流处理平台的"扛把子"&#xff0c;其性…

Flutter 2025 测试策略全景:从单元测试到混沌工程,构建坚不可摧的高质量应用 - 指南

Flutter 2025 测试策略全景:从单元测试到混沌工程,构建坚不可摧的高质量应用 - 指南pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; …

LLM Weekly(2026.1.5-2026.1.11)

网络资讯 ChatGPT 健康功能全新上线 OpenAI 推出了 ChatGPT Health,这是一个专属加密空间,可将用户的医疗记录和健康类应用程序与 ChatGPT 相连接,从而提供个性化的非诊断性健康指导。该服务采用数据隔离机制,健康相关对话内容不会用于模型训练,同时支持多重身份验证(MF…

计算机网络经典问题透视:漏桶管制器的工作原理是怎么样的?

在当今这个流量爆炸的时代&#xff0c;无论是云计算、大数据还是边缘计算&#xff0c;都离不开一个核心议题——流量控制。网络拥塞、服务质量&#xff08;QoS&#xff09;下降、系统雪崩&#xff0c;这些问题的根源往往都与失控的流量有关。今天&#xff0c;我们将一起回到计算…

‘huggingface-cli‘ 不是内部或外部命令,也不是可运行的程序 或批处理文件

出现“huggingface-cli不是内部命令”的问题&#xff0c;通常由权限、环境变量、命令弃用或虚拟环境未激活导致&#xff0c;可按以下步骤排查解决&#xff1a; 1. 检查管理员权限安装问题若Python环境位于C盘&#xff0c;普通权限运行安装命令可能导致huggingface_hub安装不完整…

小红删数字【牛客tracker 每日一题】

小红删数字 时间限制&#xff1a;1秒 空间限制&#xff1a;256M 网页链接 牛客tracker 牛客tracker & 每日一题&#xff0c;完成每日打卡&#xff0c;即可获得牛币。获得相应数量的牛币&#xff0c;能在【牛币兑换中心】&#xff0c;换取相应奖品&#xff01;助力每日有…

为什么我辞去高薪开发工作?2026年反思

职业转折点&#xff1a;代码之外的觉醒 当我在2026年初递交辞呈时&#xff0c;部门主管看着远超行业均值的薪资单反复确认&#xff1a;"你确定要放弃年薪85万的开发岗&#xff0c;去做测试&#xff1f;" 这个看似悖论的选择背后&#xff0c;藏着对软件行业生态的深度…

情感分享:当代码成为我的第二语言——一位测试工程师的心路历程

在软件测试的世界里&#xff0c;我们常常被定义为“质量守门人”、“Bug猎人”&#xff0c;但鲜少有人关注我们与代码之间建立的那份深刻而复杂的情感连接。本文从一个资深软件测试工程师的视角出发&#xff0c;探讨代码如何超越工具属性&#xff0c;逐渐成为我们思维、表达甚至…

Node.js WebAssembly零拷贝图像处理

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 Node.js WebAssembly零拷贝图像处理&#xff1a;性能优化的革命性实践目录Node.js WebAssembly零拷贝图像处理&#xff1a;性能优…

别再裸连 OpenAI 了!我用这一招,帮公司节省百万成本,还搞定了 Gemini 3.0 和 Sora 2

摘要&#xff1a; 2026 年&#xff0c;AI 模型大爆发。 Gemini 3.0 Pro 突破上下文极限。 Sora 2 让视频生成进入电影级时代。 但对于开发者来说&#xff0c; 这简直是“API 地狱”。 本文将揭秘如何用 向量引擎&#xff08;Vector Engine&#xff09;。 这一行代码&#xff0c…

当AI刺破泡沫:算力瓶颈、能源战争与资本主义的“物理转向”

如果说过去二十年的科技主旋律是“软件吞噬世界”,那么在 Jordi Visser 看来,这一章节正在剧烈翻篇。我们正处于一个甚至连“资本主义”本身都在面临终结与重构的奇点时刻。 当大众还在惊叹于 ChatGPT 的生成能力时,华尔街的敏锐资金已经嗅到了风向的改变:AI 的竞争不再仅…

4.自注意机制__self-attention

自注意机制也是一个常见的network架构目前来说&#xff0c;输入都是一个向量&#xff0c;但是如果我们的输入变成了一排向量并且数目可以改变呢&#xff1f;这就是自注意机制解决的问题&#xff0c;现在model每次输入的sequence长度都不一样&#xff0c;如下图假设network现在要…