1. 描述
只出现一次的数字
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
说明:
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
2. 示例
输入: [2,2,1]
输出: 1
输入: [4,1,2,1,2]
输出: 4
3. 分析
方法一:利用额外空间来实现。
统计数组中出现次数最多/最少的元素这种问题,很自然地想到字典。
将元素的值作为字典的键,该元素出现的次数作为值,形成(键,值)对。
该方法首先需要遍历一次数组,时间复杂度为 O ( n ) O(n) O(n);接着需要遍历一次字典,字典的大小为 ( ⌊ n / 2 ⌋ + 1 ) (\lfloor n / 2\rfloor + 1) (⌊n/2⌋+1),因此时间复杂度为 O ( n ) O(n) O(n)。总的时间复杂度为 O ( n ) O(n) O(n),满足题目“线性时间复杂度”的要求。
方法二:不使用额外空间来实现。
题目特意指明除某个元素只出现一次之外,其余每个元素均出现两次。这句话有什么特殊含义吗?
假设一个数字是5,它出现了两次,也就是2个5,让它们之间做与(&)运算。
运算 | 二进制表示 |
---|---|
. | 0101(5) |
&(与运算) | 0101(5) |
结果 | 0101(5) |
好像没什么变化!和查找只出现一次的元素扯不上关系。
再试试或(|)运算。
运算 | 二进制表示 |
---|---|
. | 0101(5) |
| (或运算) | 0101(5) |
结果 | 0101(5) |
好像还是什么变化!和查找只出现一次的元素扯不上关系。
再试试异或(^)运算。
运算 | 二进制表示 |
---|---|
. | 0101(5) |
^ (异或运算) | 0101(5) |
结果 | 0000(0) |
有点启示了。
如果某个数字出现两次,那么异或运算的结果为0。
如果某个数字只出现一次,那么这个数字与0(其余数字都出现两次,异或运算结果为0)异或运算的结果如下
运算 | 二进制表示 |
---|---|
. | 0100(4) |
^ (异或运算) | 0000(0) |
结果 | 0100(4) |
结果就是只出现一次的数字本身。
由此,得出结论:
数组中只出现一次的数字就等于数组中所有数字进行异或运算的结果。
遍历一次数组,时间复杂度为 O ( n ) O(n) O(n);异或运算是位运算,可以认为时间复杂度为 O ( 1 ) O(1) O(1)。
所以总的时间复杂度为 O ( n ) O(n) O(n),满足题目“线性时间复杂度”的要求,且比方法一更简洁、高效!
4. 代码
方法一:
class Solution
{
public int singleNumber(int[] nums)
{
// HashMap 作为字典使用
Map<Integer, Integer> pair = new HashMap<>();
// 统计数字(键)出现的次数(值)
for(int element: nums)
{
// 如果数字在字典中出现过,那么其出现次数 + 1
if(pair.containsKey(element))
{
pair.put(element, pair.get(element) + 1);
}
// 否则, 其出现次数置为1
else
{
pair.put(element, 1);
}
}
int only = 0;
// 遍历得到字典中值为1的键,也即是只出现一次的数字
for(int key: pair.keySet())
{
if(pair.get(key) == 1)
{
only = key;
break;
}
}
return only;
}
}
方法二:
class Solution
{
public int singleNumber(int[] nums)
{
// bitExclusiveOr 保存每一步异或运算的结果
int bitExclusiveOr = 0;
// 将数组中所有数字进行异或运算
for(int element : nums)
{
bitExclusiveOr ^= element;
}
// 数组中只出现一次的数字就等于异或运算的结果
return bitExclusiveOr;
}
}
5. 验证
方法一:
方法二: