目录

题目描述

442. 数组中重复的数据

题意分析

给定一个长度为 $n$ 的整数数组 nums,题目保证每个元素都落在 $[1, n]$ 这个区间里,其中一部分数字出现了两次,其余数字只出现一次。要求返回所有出现两次的数字,顺序不限。

题面里两句约束不是背景描述,而是解法的全部依据。第一句是「值域恰好是 $[1, n]$」:元素能取的值和数组的下标一一对应,数组自己就是一张长度刚好够用的桶。第二句是「每个元素至多出现两次」:这意味着每个数字最多只需要记录一次「我来过」的痕迹,不需要计数器,一个布尔量级的信息就够。

另外要注意进阶要求:时间 $O(n)$、除返回结果外只用常数额外空间。这一条直接封死了排序($O(n \log n)$)和哈希表($O(n)$ 额外空间)两条路,等于明示答案必须在原数组内部做文章。

边界方面:数组长度可能为 $1$,此时不可能有重复,返回空列表;也可能完全没有重复元素,同样返回空列表;结果个数最多为 $n / 2$。返回结果所占的空间按惯例不计入空间复杂度。

解法:原地符号标记出现状态

核心思路

问题关键:数组长度为 n,每个数都在 [1,n] 中,数值 x 可以天然映射到下标 x-1。题目又要求 $O(n)$ 时间和 $O(1)$ 额外空间,因此应复用原数组记录“是否见过”。

为什么选符号标记:输入初始全为正数,可以把 nums[x-1] 的正负当作数字 x 的访问标记,同时绝对值仍保留原数据。扫描到数字 x 时:

  • nums[x-1] > 0:第一次出现,将其变为负数;
  • nums[x-1] < 0:此前已经出现过,x 就是重复数字。

不变量与正确性:处理完任意前缀后,nums[x-1] < 0 当且仅当 x 已在该前缀出现过。第一次遇到 x 时建立标记,第二次遇到时必然命中标记并加入答案。题目保证每个数字最多出现两次,因此不会重复加入。

当前元素所在位置可能已被前面的数字改成负数,所以读取时必须先取绝对值。本解法会修改输入;若调用方要求保留原数组,结束后再统一恢复正数即可。

解题步骤

  • 创建结果列表,依次扫描数组。
  • 对当前值取绝对值得到 x,计算映射下标 idx = x - 1
  • nums[idx] 已为负,将 x 加入答案;否则把 nums[idx] 变为负数。
  • 扫描结束后返回答案。

[4,3,2,7,8,2,3,1] 为例,第一次遇到 2 和 3 时已把对应位置变负;再次遇到它们时命中负号,得到 [2,3]

代码实现

class Solution {
    public List<Integer> findDuplicates(int[] nums) {
        List<Integer> ans = new ArrayList<>();

        for (int value : nums) {
            int x = Math.abs(value);
            int idx = x - 1;
            if (nums[idx] < 0) {
                ans.add(x);
            } else {
                nums[idx] = -nums[idx];
            }
        }
        return ans;
    }
}
func findDuplicates(nums []int) []int {
    ans := make([]int, 0)

    for _, value := range nums {
        x := value
        if x < 0 {
            x = -x
        }
        idx := x - 1
        if nums[idx] < 0 {
            ans = append(ans, x)
        } else {
            nums[idx] = -nums[idx]
        }
    }
    return ans
}

复杂度分析

  • 时间复杂度:$O(n)$,每个元素只处理一次。
  • 空间复杂度:$O(1)$,返回结果不计入额外空间;标记直接写入输入数组。

关键点总结

  • 值域 [1,n] 让数值可以映射到数组下标,是原地标记成立的前提。
  • 负号只记录“出现过”,绝对值仍保存原始数字。
  • 读取当前元素必须先取绝对值,因为该位置可能已经被标记。
  • 本解法会修改输入;需要保留输入时可在结束后用一趟扫描恢复。

易错点总结

  • 直接使用 value 计算下标:它可能已变成负数,必须先取绝对值。
  • 忘记减一:数值 n 会映射到 nums[n],直接越界。
  • 用 0 作为标记:会破坏原值,之后无法知道该位置原来表示哪个数字。
  • 第二次命中后再次翻转符号:会把标记恢复为正数,破坏不变量。
  • 忽略副作用:算法结束后 nums 已被修改,若后续还要使用原数组应先恢复。

相似题目

题目 难度 考察点
448. 找到所有数组中消失的数字 简单 同样的符号标记,收集的是仍为正的下标
645. 错误的集合 简单 一次遍历同时定位重复数与缺失数
287. 寻找重复数 中等 禁止修改数组,改用快慢指针判圈
41. 缺失的第一个正数 困难 值域映射从标记升级为原地置换