442. 数组中重复的数据
问题描述
给定一个整数数组 nums
,其中 1 ≤ nums[i] ≤ n
(n 为数组长度),数组中某些元素出现两次,其余元素出现一次。找出所有出现两次的元素。
示例:
示例 1:
输入:nums = [4,3,2,7,8,2,3,1]
输出:[2,3]
示例 2:
输入:nums = [1,1,2]
输出:[1]
示例 3:
输入:nums = [1]
输出:[]
算法思路
原地哈希标记法:
- 核心思想:利用数组值作为索引进行标记
- 标记策略:将对应位置的数值变为负数表示已访问
- 检测重复:当再次访问到负数时,说明该索引对应的值是重复的
关键:
- 数组元素范围
[1,n]
与索引范围[0,n-1]
存在映射关系:value → index = value-1
- 可以利用正负号作为"访问标记",不额外占用空间
- 遍历时,将
nums[Math.abs(nums[i])-1]
变为负数 - 若发现已是负数,说明
Math.abs(nums[i])
重复
代码实现
方法:原地哈希标记(最优解)
import java.util.ArrayList;
import java.util.List;
class Solution {
/**
* 找出数组中所有出现两次的元素
*
* @param nums 整数数组,1 ≤ nums[i] ≤ n
* @return 包含所有重复元素的列表
*/
public List<Integer> findDuplicates(int[] nums) {
List<Integer> result = new ArrayList<>();
// 第一次遍历:使用原数组进行标记
for (int i = 0; i < nums.length; i++) {
// 获取当前元素的绝对值(防止已被标记为负)
int value = Math.abs(nums[i]);
// 计算对应索引:value-1
int index = value - 1;
// 检查对应位置是否已被标记
if (nums[index] > 0) {
// 未标记:将其变为负数,表示value已出现过
nums[index] = -nums[index];
} else {
// 已标记:说明value是重复元素
result.add(value);
}
}
// 可选:恢复数组原始状态(如果要求不修改原数组)
// for (int i = 0; i < nums.length; i++) {
// nums[i] = Math.abs(nums[i]);
// }
return result;
}
}
方法二:交换法(另一种原地解法)
import java.util.ArrayList;
import java.util.List;
class Solution {
/**
* 使用交换法将元素放到正确位置
*
* @param nums 整数数组
* @return 重复元素列表
*/
public List<Integer> findDuplicates(int[] nums) {
List<Integer> result = new ArrayList<>();
// 将每个元素放到其应该在的位置
// 理想情况下:nums[i] = i+1
for (int i = 0; i < nums.length; i++) {
// 当前位置的元素不在正确位置,且目标位置的元素也不正确
while (nums[i] != i + 1 && nums[nums[i] - 1] != nums[i]) {
// 交换 nums[i] 和 nums[nums[i]-1]
int temp = nums[i];
nums[i] = nums[temp - 1];
nums[temp - 1] = temp;
}
}
// 再次遍历,找出不在正确位置的元素
for (int i = 0; i < nums.length; i++) {
if (nums[i] != i + 1) {
// 该位置的元素是重复的
// 但要避免重复添加
if (result.isEmpty() || result.get(result.size() - 1) != nums[i]) {
result.add(nums[i]);
}
}
}
return result;
}
}
算法分析
- 时间复杂度:O(n)
- 方法一:单次遍历 O(n)
- 方法二:虽然有while循环,但每个元素最多被交换两次,总体仍为 O(n)
- 空间复杂度:O(1)
- 不考虑返回结果占用的空间
- 原地操作,仅使用常数额外空间
算法过程
输入:nums = [4,3,2,7,8,2,3,1]
方法一执行过程:
初始: [4, 3, 2, 7, 8, 2, 3, 1]
i=0: value=4, index=3
nums[3]=7>0 → 标记为-7
数组: [4, 3, 2, -7, 8, 2, 3, 1]
i=1: value=3, index=2
nums[2]=2>0 → 标记为-2
数组: [4, 3, -2, -7, 8, 2, 3, 1]
i=2: value=2, index=1
nums[1]=3>0 → 标记为-3
数组: [4, -3, -2, -7, 8, 2, 3, 1]
i=3: value=7, index=6
nums[6]=3>0 → 标记为-3
数组: [4, -3, -2, -7, 8, 2, -3, 1]
i=4: value=8, index=7
nums[7]=1>0 → 标记为-1
数组: [4, -3, -2, -7, 8, 2, -3, -1]
i=5: value=2, index=1
nums[1]=-3<0 → 重复!添加2
result: [2]
i=6: value=3, index=2
nums[2]=-2<0 → 重复!添加3
result: [2,3]
i=7: value=1, index=0
nums[0]=4>0 → 标记为-4
数组: [-4, -3, -2, -7, 8, 2, -3, -1]
返回: [2,3]
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准示例
int[] nums1 = {4,3,2,7,8,2,3,1};
System.out.println("Test 1: " + solution.findDuplicates(nums1)); // [2,3]
// 测试用例2:无重复
int[] nums2 = {1,2,3,4,5};
System.out.println("Test 2: " + solution.findDuplicates(nums2)); // []
// 测试用例3:全部重复(偶数长度)
int[] nums3 = {1,1,2,2};
System.out.println("Test 3: " + solution.findDuplicates(nums3)); // [1,2]
// 测试用例4:单个重复
int[] nums4 = {1,2,2};
System.out.println("Test 4: " + solution.findDuplicates(nums4)); // [2]
// 测试用例5:连续重复
int[] nums5 = {1,1};
System.out.println("Test 5: " + solution.findDuplicates(nums5)); // [1]
// 测试用例6:空数组
int[] nums6 = {};
System.out.println("Test 6: " + solution.findDuplicates(nums6)); // []
// 测试用例7:长度为1
int[] nums7 = {1};
System.out.println("Test 7: " + solution.findDuplicates(nums7)); // []
}
关键点
-
索引映射关系:
- 数值
x
对应索引x-1
- 这是能使用原地标记的前提条件
- 数值
-
正负号标记法:
- 正数:未访问
- 负数:已访问过一次
- 再次遇到负数 → 重复
-
使用 Math.abs():
- 必须取绝对值获取原始数值
- 防止因标记操作影响数值判断
-
边界条件:
- 空数组返回空列表
- 单元素数组无重复
- 全部元素都可能重复
常见问题
-
为什么能保证正确性?
- 每个数值
x
都会尝试标记位置x-1
- 第一次标记成功(正→负)
- 第二次发现已标记(负),说明重复
- 不会漏判或误判
- 每个数值
-
是否修改了原数组?
- 是的,方法一会修改原数组
- 如果要求不修改,可在最后恢复数组状态
- 或使用额外空间的哈希表解法
-
能否扩展到找出现k次的元素?
- 正负号只能表示0次/1次/≥2次
- 找恰好k次需要其他方法(如计数排序)
-
与其他类似问题的关系?
- 与448. 找到所有数组中消失的数字思路相同
- 都是利用值域与索引的映射关系进行原地标记