题目来源
题目描述
题目解析
哈希表
我们可以使用哈希映射统计数组中每个元素的出现次数。对于哈希映射中的每个键值对,键表示一个元素,值表示其出现的次数。
在统计完成后,我们遍历哈希映射即可找出只出现一次的元素。
class Solution {
public:
int singleNumber(vector<int>& nums) {
unordered_map<int, int> freq;
for (int num: nums) {
++freq[num];
}
int ans = 0;
for (auto [num, occ]: freq) {
if (occ == 1) {
ans = num;
break;
}
}
return ans;
}
};
求和
注意,防止溢出
class Solution {
public:
int singleNumber(vector<int>& nums) {
long sum_total = 0, sum_single = 0;
std::unordered_set<int> set;
for(auto num : nums){
sum_total += num;
if(set.count(num)){
continue;
}
set.insert(num);
sum_single += num;
}
return (sum_single * 3 - sum_total) / 2;
}
};
思路
如下图,考虑数字的二进制形式,对于出现三次的数字,各个二进制位出现的次数都是3的倍数。因此,统计所有数字的各二进制位中1的出现次数,并对3求余,结果则为只出现一次的数字
在具体的操作实现上,问题中给出数组中的数据在int范围之内,那么我们就可以在实现上可以对int的32个位每个位进行依次判断该位1的个数求余3后是否为1,如果为1说明结果该位二进制为1可以将结果加上去。最终得到的值即为答案。
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ans = 0;
for (int i = 0; i < 32; ++i) {
int sum = 0;
for(auto num : nums){
if((num >> i & 1) == 1){
++sum;
}
}
if(sum % 3 != 0){
ans |= (1 << i);
}
}
return ans;
}
};
统计
有限状态自动机
分析:DFA
如果我们考虑「除了某个元素只出现一次以外,其余每个元素均出现两次」的情况,那么可以使用「异或」运算。
利用相同数异或为 0 的性质,可以帮助我们很好实现状态切换:
本题是考虑「除了某个元素只出现一次以外,其余每个元素均出现三次」的情况,那么对应了「出现 0 次」、「出现 1 次」和「出现 2 次」三种状态,意味着至少需要两位进行记录,且状态转换关系为:
这是一道经典的 DFA 入门题
DFA,全称 Deterministic Finite Automaton 即确定有穷自动机:从一个状态通过一系列的事件转换到另一个状态,即 state -> event -> state。
- 确定:状态以及引起状态转换的事件都是可确定的,不存在“意外”。
- 有穷:状态以及事件的数量都是可穷举的。
计算机操作系统中的进程状态与切换可以作为 DFA 算法的一种近似理解。如下图所示,其中椭圆表示状态,状态之间的连线表示事件,进程的状态以及事件都是可确定的,且都可以穷举。
class Solution {
public int singleNumber(int[] nums) {
int one = 0, two = 0;
for(int x : nums){
one = one ^ x & ~two;
two = two ^ x & ~one;
}
return one;
}
}
具体推导过程如下
各二进制位的位运算规则相同,因此只需要考虑一位即可。如下图所示,对于所有数字中的某二进制位1
的个数,存在三种状态,即对 3 余数为 0, 1, 2 。
- 若输入二进制位 1 ,则状态按照以下顺序转换;
- 若输入二进制位 0 ,则状态不变。
如下图所示,由于二进制只能表示 0, 1 ,因此需要使用两个二进制位来表示 3个状态。设此两位分别为 two , one,则状态转换变为:
接下来,需要通过 状态转换表 导出 状态转换的计算公式 。首先回忆一下位运算特点,对于任意二进制位 xx ,有:
- 异或运算:x ^ 0 = x , x ^ 1 = ~x
- 与运算:x & 0 = 0 , x & 1 = x
计算 oneone 方法:
if two == 0:
if n == 0:
one = one
if n == 1:
one = ~one
if two == 1:
one = 0
引入 异或运算 ,可将以上拆分简化为:
if two == 0:
one = one ^ n
if two == 1:
one = 0
引入 与运算 ,可继续简化为:
one = one ^ n & ~two
计算 two方法:
- 由于是先计算 one,因此应在新 one 的基础上计算 two。
- 如下图所示,修改为新 one后,得到了新的状态图。观察发现,可以使用同样的方法计算 two,即:
返回值:
以上是对数字的二进制中 “一位” 的分析,而 int 类型的其他 31 位具有相同的运算规则,因此可将以上公式直接套用在 32 位数上。
遍历完所有数字后,各二进制位都处于状态 00 和状态 01(取决于 “只出现一次的数字” 的各二进制位是 1 还是 0 ),而此两状态是由 one来记录的(此两状态下 two 恒为 00 ),因此返回 one 即可。