442. 数组中重复的数据

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)

原地修改+利用元素的取值范围构造映射

  1. 遇到一个新元素x,我们根据映射关系x -> num[|x| - 1]把索引num[|x|-1]处的元素变为负数。注意x是元素,num[|x|-1]是索引。
  2. 因为num中可能有重复的x,所以会访问两次索引num[|x| - 1],第一次访问的时候会把该位置的元素变为负数,第二次一看是负数,就知道重复了。
  3. 之所以取绝对值是因为当前值可能被其它值修改了,需要取绝对值获取原值。

以[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)。返回值不计入空间复杂度。

原地交换+利用元素的取值范围构造映射

  1. 将元素交换到对应的位置,即元素x应放在x-1位置处。
  2. 交换后再遍历一次,位置与值对不上的元素就是重复元素。

例如[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)。返回值不计入空间复杂度。

为什么可以使用异或运算交换两个元素?

异或操作有以下几个重要性质:

  1. 自反性x ^ x = 0
  2. 零与任何数的异或等于这个数本身x ^ 0 = x
  3. 交换性x ^ y = y ^ x
  4. 结合性x ^ (y ^ z) = (x ^ y) ^ z

基于这些性质,可以通过三个异或操作实现两个变量的交换。下面是具体例子:

假设 a = Ab = B

第一步:a = a ^ b。现在 a 的值变成了 A ^ B

  • a = A ^ B
  • b = B

第二步:b = a ^ b。现在 b 的值变成了 (A ^ B) ^ B,根据异或操作的自反性 x ^ x = 0x ^ 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值