【C/C++】442. 数组中重复的数据

101 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第8天,点击查看活动详情


题目链接:442. 数组中重复的数据

题目描述

给你一个长度为 n 的整数数组 nums ,其中 nums 的所有整数都在范围 [1, n] 内,且每个整数出现 一次两次 。请你找出所有出现 两次 的整数,并以数组形式返回。

你必须设计并实现一个时间复杂度为 O(n)O(n) 且仅使用常量额外空间的算法解决此问题。

提示:

  • n==nums.lengthn == nums.length
  • 1n1051 \leqslant n \leqslant 10^5
  • 1nums[i]n1 \leqslant nums[i] \leqslant n
  • nums 中的每个元素出现 一次 或 两次

示例 1:

输入:nums = [4,3,2,7,8,2,3,1]
输出:[2,3]

示例 2:

输入:nums = [1,1,2]
输出:[1]

示例 3:

输入:nums = [1]
输出:[]

整理题意

给定一个长度为 n 的数组 nums ,数组中的整数范围在 [1, n] 内,且每个整数只出现 一次两次,让我们找到出现两次的整数。

特别要求了时间复杂度为 O(n)O(n) ,空间复杂度为 O(1)O(1)

解题思路分析

习惯性动作,首先观察题目数据范围:

  • 数组长度最大为 10510^5
  • 数组中的整数范围在 [1, n] 内;(解题核心

方法一:将元素交换到对应的位置(抽屉原理)

由于数组中整数范围在 [1, n] 内,且每个整数只可能出现 一次两次,这意味着如果有数字出现了两次,那么就有数字没有出现:

  • 首先考虑将每个数值放入一个固定下标位置,由于数值范围在 [1, n] 闭区间内,而数组下标范围为 [0, n-1] 闭区间内,我们可以将数值为 nums[i] 的数字放入下标为 nums[i] - 1 的位置。
  • 对于出现过两次的数值来说,既然有数值出现过两次,那么由数据范围可得知 [1, n] 中就有数值没有出现过,那么我们可以将出现过两次的数值多出来的那一个放入那些没有出现过的数值的位置。
  • 最后遍历数组中那些没有在自己位置上的数值,说明自己位置上站着和自己相同的数字,也就是说当前数值出现了两次。

方法二:使用正负号作为标记(抽屉原理)

由于数值范围在 [1, n] 闭区间内,我们还可以使用负号来标记 nums[i] - 1 位置是否被标记过:

  • 如果 nums[abs(nums[i]) - 1] > 0:说明 nums[i] 还未出现过,将 nums[abs(nums[i]) - 1] *= -1 标记为负数。
  • 如果 nums[abs(nums[i]) - 1] > 0:说明 nums[i] 出现过,记录为答案。

具体操作

  • 方法一:将元素交换到对应的位置
  1. 遍历数组 nums 中每一个位置;
  2. 为当前位置上的数值 nums[i] 寻找它应该所在的位置下标 nums[i] - 1,并判断该位置上的值与当前位置上的值是否相等:nums[i] == nums[nums[i] - 1] ?
    • 不相等:交换位置,将当前位置上的数值归位,继续为当前位置上的数值继续寻找,直至相等。
    • 相等:相等的情况有两种,一种是下标不一样的相等,说明当前数值出现了两次;一种是下标一样的相等,说明当前位置上的数值已经在自己的位置上了。继续为下一个位置上的数值寻找。
  3. 最后遍历一遍数组,将不在自己位置上的数值记录为答案。因为说明该数值出现了两次,且多出来的一次占据了那些没有出现过的数值的位置。
  • 方法二:使用正负号作为标记
  1. 遍历数组 nums 中每一个数值。
  2. 查看下标为 abs(nums[i]) - 1 的数值:
    • 如果 nums[abs(nums[i]) - 1] 为正数,说明 nums[i] 还未出现过,将 nums[abs(nums[i]) - 1] *= -1 标记为负数。
    • 如果 nums[abs(nums[i]) - 1] 为负数,说明 nums[i] 出现过,记录为答案。
  3. 最后输出答案数组即可。

需要注意的是nums[i] 可能为负数,所以需要取绝对值,保证下标为正数。

复杂度分析

  • 时间复杂度:方法一中每一次交换操作会使得至少一个元素被交换到对应的正确位置,因此交换的次数为 O(n)O(n),总时间复杂度为 O(n)O(n);方法二中只需要对数组 nums 进行一次遍历,总时间复杂度为 O(n)O(n)
  • 空间复杂度:O(1)O(1)。返回值不计入空间复杂度。

基于“异或运算”交换两个变量的值

交换两个变量的值,例如 a 和 b,不使用第三个变量,有两种不同的方法:

基于异或运算基于加减法
a = a ^ ba = a + b
b = a ^ bb = a - b
a = a ^ ba = a - b

两数交换有较为节约空间的 异或交换 且避免了加法溢出问题,但是需要注意的是 异或交换 不能自己和自己进行交换,否则会导致交换后值为 0

需要注意的重点是:上面介绍的通过异或运算交换两个变量的做法,不要在平常工程当中用,节约不了多少空间;尽量抽取方法以体现主干逻辑,尽管不符合本题意思。写代码性能虽然很重要,但在很多时候,为了一点点性能,使得代码难以阅读和被他人理解,丢失可读性是没有必要的,没有必要为了节约空间去牺牲代码的可读性。

代码实现

  • 方法一:将元素交换到对应的位置
class Solution {
public:
    vector<int> findDuplicates(vector<int>& nums) {
        vector<int> res;
        res.clear();
        int n = nums.size();
        for(int i = 0; i < n; i++){
            while(nums[i] != nums[nums[i] - 1]){
                //普通交换使用到常量额外空间(栈空间)
                //swap(nums[i], nums[nums[i] - 1]);
                /*异或交换不使用额外空间,需要注意保证下标不随值的改变而改变*/
                int idx1 = i, idx2 = nums[i] - 1;
                nums[idx1] = nums[idx1] ^ nums[idx2];
                nums[idx2] = nums[idx1] ^ nums[idx2];
                nums[idx1] = nums[idx1] ^ nums[idx2];
            }
        }
        for(int i = 0; i < n; i++){
            //不在自己位置上的数字说明自己位置上的数字与自己相同,且占了数组中缺少数字的位置
            if(nums[i] - 1 != i) res.push_back(nums[i]);
        }
        return res;
    }
};

  • 方法二:使用正负号作为标记
class Solution {
public:
    vector<int> findDuplicates(vector<int>& nums) {
        vector<int> res;
        res.clear();
        int n = nums.size();
        for(int i = 0; i < n; i++){
            //注意下标不能为负
            int idx = abs(nums[i]);
            //如果当前位置为负数表示存在相同元素 idx
            if(nums[idx - 1] < 0) res.push_back(idx);
            //否则将当前位置元素标记为负数
            else nums[idx - 1] *= -1;
        }
        return res;
    }
};

总结

  • 该题的核心思想在于 抽屉原理 ,一个元素一个坑,多出来的元素无处安放,查找这些无处安放的元素即为答案。
  • 同时需要注意题目要求的时间复杂度和空间复杂度。另外在实现时需要注意下标为正的细节处理。
  • 以及基于“异或运算”交换两个变量的值的方法虽然可以提高性能,但在实际应用中由于缺乏代码的可读性,所以不常用。

结束语

幸福的感觉常常就藏在我们身边,若你能有一双欣赏万物的眼睛,从柴米油盐的琐碎日常中提炼美好,寻常光景里也能开出绚烂的花朵。