442. 数组中重复的数据
给你一个长度为 n
的整数数组 nums
,其中 nums
的所有整数都在范围 [1, n]
内,且每个整数出现 一次 或 两次 。请你找出所有出现 两次 的整数,并以数组形式返回。
你必须设计并实现一个时间复杂度为 O(n)
且仅使用常量额外空间的算法解决此问题。
示例 1:
输入:nums = [4,3,2,7,8,2,3,1]
输出:[2,3]
示例 2:
输入:nums = [1,1,2]
输出:[1]
示例 3:
输入:nums = [1]
输出:[]
提示:
n == nums.length
1 <= n <= 10^5
1 <= nums[i] <= n
nums
中的每个元素出现 一次 或 两次
哈希集
遇到一个元素,如果它不在哈希集中,则加入哈希集,否则加入重复集。
class Solution {
public List<Integer> findDuplicates(int[] nums) {
int n = nums.length;
// 用数组模拟哈希集合。题目说每个元素都属于[1,n]。seen[i]=1表示值为i的元素出现过,seen[i]=0表示值为i的元素没有出现过。
int[] seen = new int[n + 1];
List<Integer> res = new LinkedList<>();
for (int num : nums) {
if (seen[num] == 0) {
// 添加到哈希集合
seen[num] = 1;
} else {
// 找到重复元素
res.add(num);
}
}
return res;
}
}
- 时间复杂度O(n)
- 空间复杂度O(n)
原地修改+利用元素的取值范围构造映射
- 遇到一个新元素
x
,我们根据映射关系x -> num[|x| - 1]
把索引num[|x|-1]
处的元素变为负数。注意x
是元素,num[|x|-1]
是索引。 - 因为
num
中可能有重复的x
,所以会访问两次索引num[|x| - 1]
,第一次访问的时候会把该位置的元素变为负数,第二次一看是负数,就知道重复了。 - 之所以取绝对值是因为当前值可能被其它值修改了,需要取绝对值获取原值。
以[2,3,1,1]为例,2对应的索引是1,因此我们把索引1 处的元素3变成-3,此时数组为[2,-3,1,1]。
然后前进到索引1处,-3的绝对值是3,3对应的索引是2,因此我们把索引2处的1变为-1,此时数组为[2,-3,-1,1]。
然后我们前进到索引2处,-1的绝对值是1,1对应的索引是0,因此我们把索引0处的2变成-2,此时数组为[-2,-3,-1,1]。
然后我们前进到索引3处,该位置的元素是1,1对应的索引是0,索引0处的元素是-2,小于0,说明这是第二次通过映射访问索引0,即索引3处的元素是重复元素。
class Solution {
public List<Integer> findDuplicates(int[] nums) {
List<Integer> res = new LinkedList<>();
for (int num : nums) {
// 注意索引,nums 中元素大小从 1 开始,而索引是从 0 开始的,所以有一位索引偏移
if (nums[Math.abs(num) - 1] < 0) {
// 之前已经把对应索引的元素变成负数了,这说明 num 重复出现了两次。
// 为什么遇到重复元素的条件是nums[Math.abs(num) - 1] < 0而不是num<0?因为num是通过for循环遍历到的,而nums[Math.abs(num) - 1]是通过映射访问到的,nums[Math.abs(num) - 1] < 0说明之前已经通过映射操作过该位置的元素,这是第二次通过映射访问,这就说明自变量num是重复元素。
res.add(Math.abs(num));
} else {
// 把索引 num - 1 置为负数
nums[Math.abs(num) - 1] *= -1;
}
}
return res;
}
}
- 时间复杂度:O(n)。
- 空间复杂度:O(1)。返回值不计入空间复杂度。
原地交换+利用元素的取值范围构造映射
- 将元素交换到对应的位置,即元素
x
应放在x-1
位置处。 - 交换后再遍历一次,位置与值对不上的元素就是重复元素。
例如[3,1,2,1],3应该放在索引2处,交换3和索引2处的元素2,得到[2,1,3,1]。
交换完继续检查当前位置,2应该放在索引2-1=1处,索引1处的元素是1,不等于2,因此交换2和1,让2在索引1处,得到[1,2,3,1]。
交换完继续检查当前位置,1应该放在索引0处,索引0处的元素是1,没问题。
继续检查索引1,没问题。
继续检查索引2,没问题。
继续检查索引3,1应该放在索引0处,索引0处的元素是1,没问题。
最后遍历一遍数组,发现索引3处的元素是1而不是4,所以1是重复元素。
class Solution {
/**
* 找到数组中所有重复出现的数字
*
* @param nums 输入的整数数组,其中1 ≤ nums[i] ≤ n (n为数组的长度)
* @return 包含所有重复数字的列表
*/
public List<Integer> findDuplicates(int[] nums) {
List<Integer> res = new ArrayList<>(); // 用于存储结果的列表
int len = nums.length; // 数组的长度
// 如果数组为空,直接返回空列表
if (len == 0) {
return res;
}
// 将每个数字放到其正确的位置(索引)上
for (int i = 0; i < len; i++) {
// 使用 while 而不是 if 是为了处理可能多次交换的情况。当一个数字放到其正确的位置后,新的数字可能也不在正确位置上,因此需要继续交换,直到该位置上是正确的数字。例如,如果 nums=[2,3,1],则第一个位置需要两次交换才能变成正确的1。
// while循环只保证当前位置的元素nums[i]在正确的位置上,不保证当前位置有正确的元素。假设数组中有两个a,则第一次遇到a时while循环会保证a在a-1处,第二次遇到a时发现a在a-1处就直接跳过了。for循环结束后必有几个位置的元素不正确,这些元素就是重复元素。
while (nums[nums[i] - 1] != nums[i]) {
swap(nums, i, nums[i] - 1); // 交换当前位置的数字到其应该在的位置
}
}
// 检查每个位置上的数字,如果不在其应该在的位置上,说明是重复的
for (int i = 0; i < len; i++) {
if (nums[i] - 1 != i) {
res.add(nums[i]); // 记录重复的数字
}
}
return res; // 返回包含所有重复数字的列表
}
/**
* 交换数组中的两个元素
*
* @param nums 需要交换的数组
* @param index1 第一个元素的索引
* @param index2 第二个元素的索引
*/
private void swap(int[] nums, int index1, int index2) {
if (index1 == index2) {
return; // 如果索引相同,不需要交换
}
// 使用异或运算交换两个元素
nums[index1] = nums[index1] ^ nums[index2];
nums[index2] = nums[index1] ^ nums[index2];
nums[index1] = nums[index1] ^ nums[index2];
}
}
- 时间复杂度:O(n)。每一次交换操作会使得至少一个元素被交换到对应的正确位置,因此交换的次数为 O(n),总时间复杂度为 O(n)。
- 空间复杂度:O(1)。返回值不计入空间复杂度。
为什么可以使用异或运算交换两个元素?
异或操作有以下几个重要性质:
- 自反性:
x ^ x = 0
- 零与任何数的异或等于这个数本身:
x ^ 0 = x
- 交换性:
x ^ y = y ^ x
- 结合性:
x ^ (y ^ z) = (x ^ y) ^ z
基于这些性质,可以通过三个异或操作实现两个变量的交换。下面是具体例子:
假设 a = A
,b = B
。
第一步:a = a ^ b
。现在 a
的值变成了 A ^ B
:
a = A ^ B
b = B
第二步:b = a ^ b
。现在 b
的值变成了 (A ^ B) ^ B
,根据异或操作的自反性 x ^ x = 0
和 x ^ 0 = x
,可以得到:
b = (A ^ B) ^ B = A ^ (B ^ B) = A ^ 0 = A
此时 b
的值变成了 A
:
a = A ^ B
b = A
第三步:a = a ^ b
。现在 a
的值变成了 (A ^ B) ^ A
,同样根据异或操作的性质,可以得到:
a = (A ^ B) ^ A = (A ^ A) ^ B = 0 ^ B = B
此时 a
的值变成了 B
,而 b
的值已经变成了 A
:
a = B
b = A