题解-找单独的数 | 豆包MarsCode AI刷题

67 阅读5分钟

题目:找单独的数

问题描述: 在一个班级中,每位同学都拿到了一张卡片,上面有一个整数。有趣的是,除了一个数字之外,所有的数字都恰好出现了两次。现在需要你帮助班长小C快速找到那个拿了独特数字卡片的同学手上的数字是什么。 要求:

  1. 设计一个算法,使其时间复杂度为 O(n),其中 n 是班级的人数。
  2. 尽量减少额外空间的使用,以体现你的算法优化能力。

解析

如果不考虑时间复杂度和空间复杂度的限制,这道题有很多种解法,可能的解法有如下几种。
(省流:方法四最优)

方法一

使用集合存储数字。遍历数组中的每个数字,如果集合中没有该数字,则将该数字加入集合,如果集合中已经有该数字,则将该数字从集合中删除,最后剩下的数字就是只出现一次的数字。
最慢的做法是依次拿到数组每个元素,然后再遍历数组中的其他元素看有没有重复的元素,这样的话时间复杂度是O(n2)O(n^2),不符合题目要求。
可以使用Hashset集合进行优化,定义一个HashSet,遍历数组中每个元素,并存入HashSet中,由于Set中不允许出现重复的数字,因此当添加发挥false时,证明这个元素是重复的,不是我们要找的元素,将其在HashSet中删除,遍历结束后,由于除了一个我们要找的元素外,其余的都有2个,所以重复的元素都会被移出HashSet,最终HashSet还剩下的元素就是我们要找的,solution函数代码如下:

public class Main {
    public static int solution(int[] inp) {
        Set<Integer> set = new HashSet<>();
        for (int num : inp) {
            if (!set.add(num)) {
                set.remove(num);
            }
        }
        return set.iterator().next();
    }
}

复杂度分析:

  1. 时间复杂度:
  • 遍历数组 inp 并尝试将每个元素添加到集合 set 中。这个操作的时间复杂度是 O(n),其中 n 是数组 inp 的长度。对于每个元素,我们执行常数时间的操作(尝试添加和可能的移除)。
  • 如果集合中已经存在该元素,则将其从集合中移除,这也是一个常数时间操作。
  • 最后,我们使用 set.iterator().next() 来获取集合中的唯一元素。由于集合中只有一个元素,这个操作的时间复杂度是 O(1)。

因此,总的时间复杂度是 O(n)O(n)

  1. 空间复杂度:
  • 集合 set 用来存储数组中的所有不同元素。在最坏的情况下,如果数组中的每个元素都是唯一的,那么集合的大小将是 n。但是,由于题目条件是除了一个元素外,其余每个元素都出现两次,因此集合的大小最多是 n/2。
  • 在平均情况下,集合的大小会小于 n/2,但为了简化分析,我们考虑最坏情况。

因此,空间复杂度是O(n)O(n)

方法二

使用哈希表存储每个数字和该数字出现的次数。遍历数组即可得到每个数字出现的次数,并更新哈希表,最后遍历哈希表,得到只出现一次的数字。solution函数代码如下:

public class Main {
    public static int solution(int[] inp) {
        Map<Integer, Integer> countMap = new HashMap<>();
        for (int num : inp) {
            countMap.put(num, countMap.getOrDefault(num, 0) + 1);
        }
        for (Map.Entry<Integer, Integer> entry : countMap.entrySet()) {
            if (entry.getValue() == 1) {
                return entry.getKey();
            }
        }
    }
}

复杂度分析:

  • 时间复杂度:O(n)O(n) ,其中 n 是数组长度。只需要对数组遍历一次
  • 空间复杂度:O(n)O(n)

方法三

使用集合存储数组中出现的所有数字,并计算数组中的元素之和。由于集合保证元素无重复,因此计算集合中的所有元素之和的两倍,即为每个元素出现两次的情况下的元素之和。由于数组中只有一个元素出现一次,其余元素都出现两次,因此用集合中的元素之和的两倍减去数组中的元素之和,剩下的数就是数组中只出现一次的数字。solution函数代码如下:

public class Main {
    public static int solution(int[] inp) {
        Set<Integer> set = new HashSet<>();
        int sumSet = 0;
        int sumArray = 0;
        for (int num : inp) {
            set.add(num);
            sumArray += num;
        }
        for (int num : set) {
            sumSet += num;
        }
        return 2 * sumSet - sumArray;
    }
}

复杂度分析:

  • 时间复杂度:O(n)O(n) ,其中 n 是数组长度。只需要对数组遍历一次
  • 空间复杂度:O(n)O(n)

方法四:位运算

前三种方法都需要用到O(n)O(n)的空间,如何在保证O(n)O(n)的时间复杂度情况下,只用到常数空间呢。
这就需要用到位运算,由于题目中明确说了除了一个只出现一次的数字,其余数字都出现2次,所以可以用到异或运算⊕,异或运算有以下三个性质:

  1. 交换律:a ⊕ b = b ⊕ a
  2. 结合律:(a ⊕ b) ⊕ c = a ⊕ (b ⊕ c)
  3. 自反律:a ⊕ a = 0
  4. 对于任何数 a,a ⊕ 0 = a
  5. 对于任何数 a,a ⊕ 1 的结果是 a 的按位取反(每一位取反,0变1,1变0)

我们可以由前四个性格得到结论,数组中的全部元素的异或运算结果就是我们想找的数字,证明如下:

假设数组中有2m+12m+1个数,其中mm个数各出现两次,一个数出现一次。令a1a2ama_{1}、a_{2}、…、a_{m}为出现两次的mm个数,am+1a_{m+1}为出现一次的数。根据性质1和性质2,数组中的全部元素的异或运算结果总可以写成如下形式:
(a1a1)(a2a2)(amam)am+1(a_{1}\oplus a_{1})\oplus(a_{2}\oplus a_{2})\oplus···\oplus(a_{m}\oplus a_{m})\oplus a_{m+1}
根据性质3和性质4,上式可化简和计算得到如下结果:
000am+1=am+10\oplus0\oplus···\oplus0\oplus a_{m+1}=a_{m+1}
因此,数组中的全部元素的异或运算结果即为数组中只出现一次的数字。

最终solution函数代码如下:

public class Main {
    public static int solution(int[] inp) {
        int ans=0;
        for(int num:inp){
            ans^=num;
        }
        return ans;
    }
}

复杂度分析:

  • 时间复杂度:O(n)O(n) ,其中 n 是数组长度。只需要对数组遍历一次
  • 空间复杂度:O(1)O(1)