2026/3/17 0:14:42
网站建设
项目流程
上往建站,昆明做网站优化价格,做网站.服务器怎么买,网站英语培训文章目录摘要描述题解答案题解代码分析1. 数据结构的选择2. 初始化方法3. get() 方法#xff1a;分配号码4. check() 方法#xff1a;检查号码是否可用5. release() 方法#xff1a;释放号码6. 为什么使用 Set Array 的组合#xff1f;7. 边界情况处理示例测试及结果示例 …文章目录摘要描述题解答案题解代码分析1. 数据结构的选择2. 初始化方法3. get() 方法分配号码4. check() 方法检查号码是否可用5. release() 方法释放号码6. 为什么使用 Set Array 的组合7. 边界情况处理示例测试及结果示例 1基本操作示例 2边界情况测试示例 3重复释放测试示例 4大量操作测试时间复杂度空间复杂度实际应用场景场景一资源池管理场景二ID 分配器场景三端口号管理场景四会议室预订系统总结摘要这道题其实挺有意思的它让我们设计一个电话目录管理系统。听起来像是实际业务场景中的问题但实际上是一道经典的数据结构设计题。我们需要实现三个操作分配号码、检查号码是否可用、释放号码。这三个操作都需要高效地执行所以选择合适的数据结构很重要。这道题的核心在于如何高效地管理可用号码的集合既要能快速分配又要能快速检查状态还要能快速释放。今天我们就用 Swift 来搞定这道题顺便聊聊这种设计模式在实际开发中的应用场景。描述题目要求是这样的设计一个电话目录管理系统这个系统初始化时有maxNumbers个电话号码槽位从0到maxNumbers - 1。我们需要实现以下三个操作get()分配一个还没被使用的号码返回那个号码。如果没有可用号码了返回-1。check(number)检查某个号码是否可用即没被分配出去。如果可用返回true否则返回false。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^40 number maxNumbers总调用次数最多接近2 * 10^4这道题的核心思路是什么呢我们需要维护一个可用号码的集合能够快速地进行分配、检查和释放操作。可以用 Set 来存储可用号码用队列或数组来快速分配号码。这样get()和check()都能达到 O(1) 的时间复杂度。题解答案下面是完整的 Swift 解决方案classPhoneDirectory{privateletmaxNumbers:Intprivatevaravailable:SetIntprivatevarqueue:[Int]init(_maxNumbers:Int){self.maxNumbersmaxNumbersself.availableSet(0..maxNumbers)self.queueArray(0..maxNumbers)}/// 分配一个可用号码如果没有就返回 -1funcget()-Int{guard!queue.isEmptyelse{return-1}letnumqueue.removeLast()available.remove(num)returnnum}/// 检查某号码是否可用funccheck(_number:Int)-Bool{guardnumber0numbermaxNumberselse{returnfalse}returnavailable.contains(number)}/// 释放一个号码让它可再次被分配funcrelease(_number:Int){guardnumber0numbermaxNumberselse{return}// 只有之前被分配即当前不可用的才需要释放if!available.contains(number){available.insert(number)queue.append(number)}}}题解代码分析让我们一步步分析这个解决方案1. 数据结构的选择这道题的关键在于选择合适的数据结构来高效地支持三个操作privateletmaxNumbers:Intprivatevaravailable:SetIntprivatevarqueue:[Int]我们使用了三个数据结构maxNumbers记录最大号码数用于边界检查available一个SetInt存储所有当前可用的号码。Set 的查找和插入操作平均时间复杂度都是 O(1)非常适合快速检查号码是否可用queue一个ArrayInt维护未分配的号码序列用于快速分配号码。我们使用数组而不是真正的队列因为我们可以从尾部移除元素这样操作是 O(1) 的2. 初始化方法init(_maxNumbers:Int){self.maxNumbersmaxNumbersself.availableSet(0..maxNumbers)self.queueArray(0..maxNumbers)}初始化时我们需要保存maxNumbers用于后续的边界检查将所有号码0到maxNumbers - 1都加入到available集合中将所有号码也加入到queue数组中这样get()操作可以快速分配时间复杂度是 O(maxNumbers)因为需要初始化集合和数组。3. get() 方法分配号码funcget()-Int{guard!queue.isEmptyelse{return-1}letnumqueue.removeLast()available.remove(num)returnnum}get()方法的逻辑是检查是否有可用号码如果queue为空说明没有可用号码了返回-1从队列中取出一个号码使用removeLast()从数组尾部移除一个元素这是 O(1) 操作从可用集合中移除将这个号码从available集合中移除标记为已分配返回号码返回分配到的号码时间复杂度是 O(1)平均情况因为removeLast()和Set.remove()都是 O(1) 操作。4. check() 方法检查号码是否可用funccheck(_number:Int)-Bool{guardnumber0numbermaxNumberselse{returnfalse}returnavailable.contains(number)}check()方法的逻辑是边界检查首先检查number是否在合法范围内0 number maxNumbers。如果不在范围内直接返回false检查是否在可用集合中使用available.contains(number)检查号码是否在可用集合中。如果在说明可用返回true否则返回false时间复杂度是 O(1)平均情况因为Set.contains()是 O(1) 操作。5. release() 方法释放号码funcrelease(_number:Int){guardnumber0numbermaxNumberselse{return}// 只有之前被分配即当前不可用的才需要释放if!available.contains(number){available.insert(number)queue.append(number)}}release()方法的逻辑是边界检查首先检查number是否在合法范围内。如果不在范围内直接返回不做任何操作检查是否需要释放检查号码是否当前不在available集合中即已被分配。如果已经在集合中即已经可用就不需要重复释放直接返回释放号码如果号码已被分配需要将号码加入到available集合中标记为可用将号码加入到queue数组的尾部这样后续get()操作可以再次分配这个号码时间复杂度是 O(1)平均情况因为Set.contains()、Set.insert()和Array.append()都是 O(1) 操作。6. 为什么使用 Set Array 的组合我们同时使用Set和Array的原因Set 的优势check()操作需要快速检查号码是否可用Set 的contains()操作是 O(1)非常适合这个需求Array 的优势get()操作需要快速分配一个号码Array 的removeLast()操作是 O(1)非常适合这个需求两者配合release()操作需要同时更新两个数据结构虽然看起来有冗余但能保证两个操作都是 O(1)如果只用 Setget()操作需要遍历 Set 来找到一个可用号码时间复杂度会变成 O(n)。如果只用 Arraycheck()操作需要遍历数组来检查号码是否存在时间复杂度也会变成 O(n)。所以两者结合使用是最优解。7. 边界情况处理代码中处理了几个重要的边界情况get()时没有可用号码返回-1check()时号码超出范围返回falserelease()时号码超出范围直接返回不做任何操作release()时重复释放检查号码是否已经在可用集合中如果是就不重复释放这些边界情况的处理确保了代码的健壮性。示例测试及结果让我们用几个例子来测试一下这个解决方案示例 1基本操作letdirectoryPhoneDirectory(3)print(初始化后可用号码0, 1, 2)print(---)letnum1directory.get()print(get() 返回:\(num1))print(check(\(num1)):\(directory.check(num1)))// falseletnum2directory.get()print(get() 返回:\(num2))print(check(\(num2)):\(directory.check(num2)))// falseprint(check(2):\(directory.check(2)))// true如果 2 还没被分配letnum3directory.get()print(get() 返回:\(num3))print(check(2):\(directory.check(2)))// false现在 2 已经被分配了directory.release(2)print(release(2) 后)print(check(2):\(directory.check(2)))// true2 现在可用了letnum4directory.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边界情况测试letdirectoryPhoneDirectory(3)// 测试边界检查print(check(-1):\(directory.check(-1)))// falseprint(check(3):\(directory.check(3)))// falseprint(check(0):\(directory.check(0)))// true// 分配所有号码letnum1directory.get()letnum2directory.get()letnum3directory.get()print(分配了三个号码:\(num1),\(num2),\(num3))// 尝试再分配一个letnum4directory.get()print(get() 返回:\(num4))// -1// 释放一个号码directory.release(num1)print(release(\(num1))后)print(check(\(num1)):\(directory.check(num1)))// true// 再次分配letnum5directory.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重复释放测试letdirectoryPhoneDirectory(3)letnumdirectory.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大量操作测试letdirectoryPhoneDirectory(1000)// 分配 500 个号码varallocated:[Int][]for_in0..500{letnumdirectory.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{letnumdirectory.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:SetConnectionprivatevarqueue:[Connection]funcgetConnection()-Connection?{// 类似 get() 操作}funcisAvailable(_connection:Connection)-Bool{// 类似 check() 操作}funcrelease(_connection:Connection){// 类似 release() 操作}}场景二ID 分配器在很多系统中我们需要分配唯一的 ID比如用户 ID 分配为新用户分配唯一 ID订单号分配为新订单分配唯一订单号会话 ID 分配为新会话分配唯一会话 ID// 伪代码示例用户 ID 分配器classUserIDAllocator{privatevaravailable:SetIntprivatevarqueue:[Int]funcallocateUserID()-Int{// 类似 get() 操作}funcisUserIDAvailable(_id:Int)-Bool{// 类似 check() 操作}funcreleaseUserID(_id:Int){// 类似 release() 操作比如用户注销时}}场景三端口号管理在网络编程中我们需要管理端口号// 伪代码示例端口号管理器classPortManager{privatevaravailable:SetIntprivatevarqueue:[Int]funcallocatePort()-Int?{// 分配一个可用端口}funcisPortAvailable(_port:Int)-Bool{// 检查端口是否可用}funcreleasePort(_port:Int){// 释放端口}}场景四会议室预订系统在会议室预订系统中我们需要管理会议室的可用性// 伪代码示例会议室管理器classMeetingRoomManager{privatevaravailable:SetInt// 可用会议室编号privatevarqueue:[Int]funcbookRoom()-Int?{// 预订一个会议室}funcisRoomAvailable(_roomId:Int)-Bool{// 检查会议室是否可用}funcreleaseRoom(_roomId:Int){// 释放会议室}}总结这道题虽然看起来简单但实际上涉及了很多重要的设计思想数据结构的选择选择合适的数据结构来支持不同的操作。Set 适合快速查找Array 适合快速分配两者结合使用能达到最优性能。时间复杂度优化通过合理的数据结构选择让所有操作都达到 O(1) 的平均时间复杂度。边界情况处理正确处理边界情况如号码超出范围、重复释放等确保代码的健壮性。实际应用这种设计模式在实际开发中应用广泛如资源池管理、ID 分配、端口管理等。关键点总结使用Set来快速检查号码是否可用O(1) 查找使用Array来快速分配号码O(1) 移除尾部元素两者配合使用保证所有操作都是 O(1) 时间复杂度正确处理边界情况确保代码健壮性