448. 找到所有数组中消失的数字(Find All Numbers Disappeared in an Array)
频次 ★★★ · 难度 🟢 · 高频:百度
题目
给定一个整数数组,其中 1 <= nums[i] <= n(n 为数组长度),有些元素出现两次,有些出现一次。找出所有在 [1, n] 范围内没有出现在数组中的数字。
示例:
输入: nums = [4, 3, 2, 7, 8, 2, 3, 1]
输出: [5, 6]
思路
原地标记法:
- 对于每个数
nums[i],将其对应索引位置nums[i]-1处的值标记为负数 - 第二次遍历时,正值所在的位置就是缺失的数字
因为所有值都在 [1, n] 范围内,所以可以安全地用负号来标记”已出现”。
代码
public List<Integer> findDisappearedNumbers(int[] nums) {
List<Integer> result = new ArrayList<>();
for (int i = 0; i < nums.length; i++) {
int index = Math.abs(nums[i]) - 1;
if (nums[index] > 0) {
nums[index] = -nums[index];
}
}
for (int i = 0; i < nums.length; i++) {
if (nums[i] > 0) {
result.add(i + 1);
}
}
return result;
}复杂度
- 时间:O(n) — 两趟遍历
- 空间:O(1) — 不计输出数组
边界条件
- 数组恰好是
1..n的排列(无缺失):所有位置最终都是正数,第二次遍历结果为空列表。 - 所有数字都相同(如
[1,1,1],n=3):只有下标 0 会被标记(多次标记也没关系,if (nums[index] > 0)保证同一位置不会被重复取负两次),下标 1、2 保持正数,正确返回[2,3]。 - 数组长度为 1(
[1]):唯一位置被标记为负,结果为空列表,正确。
变式
- 找出重复的数字而不是缺失的数字(LeetCode 442 题):同样用负号标记,当发现某个
nums[index]已经是负数时,说明这个值之前出现过,即为重复值。 - 数字范围不是
[1,n]而是任意范围:负号标记法失效(因为下标不能直接对应任意值),需要退回到 HashSet 或先做值域映射。
易错点
- 标记前要用
Math.abs(nums[i]),因为前面的标记操作可能已经把某个位置变成了负数,直接用nums[i] - 1做下标会得到错误的负索引。 - 标记时要判断
nums[index] > 0才取负,否则同一个值出现 3 次以上时会被反复取负导致重新变回正数,破坏标记状态。
面试追问
- 这种”用数组自身存储的值当下标”的技巧还能用在哪些题目上? 这是一类经典技巧,只要满足”数值范围恰好是
[1,n]或[0,n-1]”就可以把值和下标关联起来原地做标记,比如 41. 缺失的第一个正数(用交换代替取负)、寻找重复数字等。 - 为什么用取负号而不是用一个额外的布尔数组? 额外布尔数组需要 O(n) 空间,而原数组本身有 n 个位置可以复用;取负号不改变数字的绝对值信息(后续还能用
Math.abs还原),是一种”原地复用存储空间”的常见手法。
关联题
- 同套路:41. 缺失的第一个正数 —— 同款”下标即哈希”,它是本题的困难版
- 进阶:442. 数组中重复的数据 —— 同套路改问”谁出现了两次”
- 知识点:值域限定在 [1, n] 是原地哈希成立的前提;看到”O(1) 空间找缺失/重复”先确认值域