先说个结论:这道题我刷了三遍才彻底想明白。不是因为解法多难,而是我第一次拿到题时陷入了思维定式——上来就想着开个哈希表记录出现次数,然后遍历找缺失值。等看到“要求O(1)空间”才意识到,力扣hot100这个级别的题目,每一道都在教你一件事:怎样在资源受限的情况下,把手头已有的东西用到极致。
448. 找到所有数组中消失的数字,题面简单到让人放松警惕:给你一个长度为 n 的整数数组 nums,其中 nums[i] 在区间 [1, n] 内,请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字,以数组形式返回结果。
举个例子:输入 nums = [4,3,2,7,8,2,3,1],输出 [5,6]。就这么一道题,牵扯出数组哈希、索引映射、原地修改、值域与下标的关系,几乎把数组类题目的底层思维全串起来了。我自己在刷这题时踩过不少坑,也总结了一套能直接用的模板,下面从头到脚拆一遍。
1. 题目本质上在考什么:从暴力解到哈希表的思维跳变
1.1 题意还原与最直观的暴力方案
先把题目的约束条件摆清楚。数组长度是 n,元素值的范围被限定在 [1, n] 之间,注意这个“值域等于索引域”的设定——它是整道题的灵魂。你需要找出 [1, n] 中哪些数字没有在数组中出现过。
最没技术含量的做法是双层循环:外层枚举 1 到 n 的每个数字,内层扫描数组看它是否出现过。时间复杂度 O(n²),在 n 达到 10⁵ 甚至 10⁶ 的规模下直接超时。这种方案唯一的优点是脑死亡式好写,适合用来确认自己对题意的理解没有偏差。
第二种是排序后比较:先对数组排序,然后从 1 到 n 依次核对。排序本身 O(n log n),空间 O(1)(原地排序的话)。这个方案能过,但面试官问一句“能不能 O(n) 时间 O(1) 空间”就会卡住。
第三种是哈希集合:遍历数组把所有出现过的值塞进 Set,再遍历 1 到 n,不在 Set 里的就是缺失值。时间 O(n),空间 O(n),非常直观。但题目通常标注了进阶要求:能否在 O(n) 时间且仅使用额外 O(1) 空间的条件下解决?
1.2 哈希集合方案:牺牲空间换时间
用哈希集合是最容易想到的解法,尤其适合刚接触这类题目的人。具体步骤很简单:
- 初始化一个空 Set。
- 遍历数组,把所有值 add 进 Set。
- 从 1 循环到 n,逐个检查 Set.has(i),没有就 push 到结果数组。
这样写没有任何难度,而且边界情况也好处理。但它违背了题目的进阶意图。LeetCode 上这道题明确写了“你能在不使用额外空间且时间复杂度为 O(n) 的情况下解决这个问题吗?”,这句话不是摆设,它希望你把数组本身当成哈希表。
我在实际刷题时会先想清楚一个问题:哈希表在这里到底做了什么?答案是提供了“某个值是否存在”的 O(1) 查询。如果我们能把“查询存在性”这件事转移到输入数组本身上,那就不需要额外空间了。这个转移的核心依据是什么?值域 [1, n] 与索引域 [0, n-1] 恰好是一一对应的偏移关系。
1.3 关键约束:为什么 O(1) 空间成了必选项
很多人不理解为什么要纠结那 O(n) 的空间。如果是在力扣上刷题,哈希集合解法已经能通过,平台不会因为空间多用了 O(n) 就不给你过。但真实场景下,海量数据场景内存是稀缺资源;面试考察的是你能否在极端约束下设计算法。更重要的是,O(1) 空间解法所要求的思维模式——利用数据本身的特征进行原地标记——是一种可迁移的高频技巧,后续你会反复遇到。
我自己的经验是:遇到“值域与数组长度一致”这个特征,就要敏锐起来。这不是巧合,而是出题人故意设置的条件。如果你只是无脑用哈希表,等于放弃了理解出题意图的机会。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 原地标记法的核心原理:把数组本身变成一张哈希表
2.1 值域与索引的映射关系拆解
原地标记法的核心是看穿一层映射:数字 v 应当对应索引 v-1。
为什么是 v-1 而不是 v?因为数组索引从 0 开始。如果数组里有数字 1,它对应的索引就是 0;有数字 5,对应索引就是 4。换句话说,题目给的数值范围 [1, n] 刚好偏移一位就能映射到 [0, n-1] 的索引范围。这个偏移看似简单,但实际编码时非常容易出错——我见过不少人取模、减一、加一之间把自己绕晕。
标记法的具体思路
