更多算法题的题解见:算法刷题题解汇总(持续更新中)
题目:力扣136:只出现一次的数&&掘金AI刷题:找单独的数
问题描述
掘金的描述:在一个班级中,每位同学都拿到了一张卡片,上面有一个整数。有趣的是,除了一个数字之外,所有的数字都恰好出现了两次。现在需要你帮助班长小C快速找到那个拿了独特数字卡片的同学手上的数字是什么。
要求:
- 设计一个算法,使其时间复杂度为 O(n),其中 n 是班级的人数。
- 尽量减少额外空间的使用,以体现你的算法优化能力。
力扣136的描述虽然有细微区别,但本质一样
解析
如果不考虑时间复杂度和空间复杂度的限制,这道题有很多种解法,可能的解法有如下几种。
(省流:方法四最优)
方法一
使用集合存储数字。遍历数组中的每个数字,如果集合中没有该数字,则将该数字加入集合,如果集合中已经有该数字,则将该数字从集合中删除,最后剩下的数字就是只出现一次的数字。
最慢的做法是依次拿到数组每个元素,然后再遍历数组中的其他元素看有没有重复的元素,这样的话时间复杂度是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();
}
}
复杂度分析:
- 时间复杂度:
- 遍历数组
inp
并尝试将每个元素添加到集合set
中。这个操作的时间复杂度是 O(n),其中 n 是数组inp
的长度。对于每个元素,我们执行常数时间的操作(尝试添加和可能的移除)。 - 如果集合中已经存在该元素,则将其从集合中移除,这也是一个常数时间操作。
- 最后,我们使用
set.iterator().next()
来获取集合中的唯一元素。由于集合中只有一个元素,这个操作的时间复杂度是 O(1)。
因此,总的时间复杂度是 O(n)
。
- 空间复杂度:
- 集合
set
用来存储数组中的所有不同元素。在最坏的情况下,如果数组中的每个元素都是唯一的,那么集合的大小将是 n。但是,由于题目条件是除了一个元素外,其余每个元素都出现两次,因此集合的大小最多是 n/2。 - 在平均情况下,集合的大小会小于 n/2,但为了简化分析,我们考虑最坏情况。
因此,空间复杂度是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)
,其中 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)
,其中 n 是数组长度。只需要对数组遍历一次 - 空间复杂度:
O(n)
方法四:位运算
前三种方法都需要用到O(n)
的空间,如何在保证O(n)
的时间复杂度情况下,只用到常数空间呢。
这就需要用到位运算,由于题目中明确说了除了一个只出现一次的数字,其余数字都出现2次,所以可以用到异或运算⊕,异或运算有以下三个性质:
- 交换律:a ⊕ b = b ⊕ a
- 结合律:(a ⊕ b) ⊕ c = a ⊕ (b ⊕ c)
- 自反律:a ⊕ a = 0
- 对于任何数 a,a ⊕ 0 = a
- 对于任何数 a,a ⊕ 1 的结果是 a 的按位取反(每一位取反,0变1,1变0)
我们可以由前四个性格得到结论,数组中的全部元素的异或运算结果就是我们想找的数字,证明如下:
假设数组中有 2 m + 1 2m+1 2m+1个数,其中 m m m个数各出现两次,一个数出现一次。令 a 1 、 a 2 、 … 、 a m a_{1}、a_{2}、…、a_{m} a1、a2、…、am为出现两次的 m m m个数, a m + 1 a_{m+1} am+1为出现一次的数。根据性质1和性质2,数组中的全部元素的异或运算结果总可以写成如下形式:
( a 1 ⊕ a 1 ) ⊕ ( a 2 ⊕ a 2 ) ⊕ ⋅ ⋅ ⋅ ⊕ ( a m ⊕ a m ) ⊕ a m + 1 (a_{1}\oplus a_{1})\oplus(a_{2}\oplus a_{2})\oplus···\oplus(a_{m}\oplus a_{m})\oplus a_{m+1} (a1⊕a1)⊕(a2⊕a2)⊕⋅⋅⋅⊕(am⊕am)⊕am+1
根据性质3和性质4,上式可化简和计算得到如下结果:
0 ⊕ 0 ⊕ ⋅ ⋅ ⋅ ⊕ 0 ⊕ a m + 1 = a m + 1 0\oplus0\oplus···\oplus0\oplus a_{m+1}=a_{m+1} 0⊕0⊕⋅⋅⋅⊕0⊕am+1=am+1
因此,数组中的全部元素的异或运算结果即为数组中只出现一次的数字。
最终solution函数代码如下:
public class Main {
public static int solution(int[] inp) {
int ans=0;
for(int num:inp){
ans^=num;
}
return ans;
}
}
复杂度分析:
- 时间复杂度:
O(n)
,其中 n 是数组长度。只需要对数组遍历一次 - 空间复杂度:
O(1)