448. 找到所有数组中消失的数字

难度:简单 | 主题:数组、哈希表

题目

给你一个含 n 个整数的数组 nums,其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字,并以数组的形式返回结果。要求不使用额外空间且时间复杂度为 O(n)。

示例

 
nums = [4,3,2,7,8,2,3,1]  → 输出 [5,6]
 
解释:长度为 8,缺失 5 和 6
 

思路

先讲个故事:教室点名

你是老师,班上 n 个学生学号是 1 到 n。你点名时发现有些人没来,但你不准拿额外的纸笔记——只能用手里的花名册本身做标记。

你想了个办法:每个被点到的学生,就在自己的座位上放本书占位。点完一圈,空座位就是缺席的人。

对应到数组:值就是学号,下标减 1 就是座位号。每个值出现时,把对应座位号位置的值取负作为标记。最后遍历一遍,值还为正的位置就是没人坐过的座位——下标 +1 就是消失的数字


引导式推导:从暴力到原地

暴力法:用哈希集存所有出现的数,再遍历 1 到 n 检查缺失。O(n) 时间 O(n) 空间——题目要求不用额外空间。

优化方向:没有额外空间 → 拿数组本身当哈希表。

关键洞察:数组的值域 [1, n] 和下标域 [0, n-1] 天然一一对应。

123n
下标012n-1

所以值 x 出现,就是在告诉下标 x-1 的位置:「我被访问过了」。

怎么标记?取负。遍历每个数 num,把 nums[abs(num) - 1] 取负。最后遍历,值为正的位置就是没有被访问过的——即缺失的数字。


graph LR

    A["遍历 nums"] --> B["取 idx = abs(num)-1"]

    B --> C["nums[idx] 取负"]

    C --> D["遍历完成"]

    D --> E["值为正的位置 → 缺失"]


代码

 
def findDisappearedNumbers(self, nums):
 
    for num in nums:
 
        idx = abs(num) - 1          # 值映射到下标(减1是因为值从1开始)
 
        nums[idx] = -abs(nums[idx]) # 标记为负数,表示该下标+1出现了
 
    return [i + 1 for i, num in enumerate(nums) if num > 0]  # 正数位置就是缺失的
 

复杂度

指标解释
时间O(n)两遍线性遍历
空间O(1)不计输出,原地修改

实战考量

频率分析

出现在:字节/腾讯/阿里约 20% 的同类题会考这种「原地哈希」技巧。核心是看你能不能想到把数组本身当哈希表用

延伸思考

Q:为什么不用集合?

A:集合需要 O(n) 额外空间,题目明确要求 O(1)。但可以先用集合讲思路,再优化到原地。

Q:用取负标记,如果数组中有 0 怎么办?

A:题目保证值域 [1, n],没有 0。如果有 0,可以用 +n 标记(值加 n,最后检查 ≤ n 的),取模还原索引。

Q:如果既要找消失的数字,又要找重复的数字呢?

A:数学法——求和与平方和,解方程组。或者原地标记时,被标记两次的位置可以推断重复。

Q:标记法会不会把后续要读的值搞乱?

A:不会。用 abs(num) 取原始值,负号只用来标记,不影响索引计算。

易错点

  • 必须用 abs(num) 读值,因为前面的标记可能已经把它变负数了

  • 最后收集的是 i + 1 不是 i

  • 循环内不要调 count()——会变成 O(n²)


生活类比

花名册点名 → 原地哈希

想象你手里只有一张名单表,不能拿额外的纸。

每点到一个人,就在他名字后面画个✓。

点名结束,没打✓的就是缺席的人。

画✓是取负,没打✓的是正数——下标就是座位号,数组就是花名册。


相关题目

题目关系
268丢失的数字同族,找缺失的一个数,更简单
41缺失的第一个正数原地哈希进阶版,条件更复杂
287寻找重复数原地标记思想,但用快慢指针

→ 返回题单:LeetCode学习路线图 > 一、数组与哈希(含双指针、矩阵)

速记卡(面试闪卡)

Q1:一句话讲清「448. 找到所有数组中消失的数字」到底是什么?

A:在长度 n、值域 1~n 的数组里找出所有消失的数字,原地标记、不占额外空间。

Q2:题目理解 —— 要你交出哪些缺席者? —— 怎么理解?

A:像老师点名,学号 1 到 n,不准拿额外纸笔,只能在本上画钩。你要把没来的人挑出来。英文:Find Disappeared Numbers。

Q3:核心思路 —— 怎么把数组当哈希表? —— 怎么理解?

A:像在花名册上给到场者打钩:值 x 出现就把座位 x-1 取负占位,最后正数座位就是缺席者。英文:In-place Hashing。

Q4:代码实现 —— 两遍扫描怎么写? —— 怎么理解?

A:像巡检员绕场两圈:第一圈每人数 idx=abs(num)-1,把该位取负做标记;第二圈收集仍为正的下标+1。英文:Two-pass Scan。

Q5:复杂度与实战 —— 易错点在哪? —— 怎么理解?

A:像对账:时间 O(n) 两遍线性,空间 O(1) 原地改。易错在必须用 abs 读值、收集 i+1 而非 i。英文:In-place Marking。

Q6:核心速记主线有哪些?

  • 题意:值域 [1,n] 对应下标 [0,n-1],找缺失的数字

  • 原地哈希:值 x 出现就把 nums[x-1] 取负做标记

  • 收集时只取仍为正的下标,结果要 i+1

  • 必须用 abs(num) 读值,负号只是标记不影响索引

  • 时间 O(n)、空间 O(1),不用额外集合

口诀

A:消失数字藏阵中,

取负占位标记工;

两遍扫描常数费,

原位对账现影踪。

相关链接