Difficulty: Medium
数组中的数两两出现,只有一个出现了一次,把它找出来
异或即可;也可使用Hash Table,但效率不如异或;或者对每一位进行计数,需要大小为 32 的 int 型数组
// Runtime: 20 ms
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ret = 0;
for (auto num: nums) {
ret ^= num;
}
return ret;
}
};
Difficulty: Medium
数组中的元素出现三次,只有一个出现了一次,把它找出来
考虑对每一位的出现次数进行计数,0–>1–>2–>3(0),由于出现三次则清零,因此,用三个变量来表示三种状态。ones表示每一位出现一次的情况,twos表示每一位出现两次的情况,reset表示某一位尚未出现或出现三次的情况。参考文章
// Runtime: 12 ms
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ones = 0, twos = 0, reset = 0;
for (auto num : nums) {
twos = twos | (ones & num);
ones = ones ^ num;
reset = ones & twos;
ones = ones ^ reset;
twos = twos ^ reset;
}
return ones;
}
};
另一篇参考文章
考虑对每一位的出现次数进行计数,00–>01–>10–>11(00),因此,次数可以用两位二进制数来表示,twos ones
对于 ones,ones = (ones ^ num),if (twos == 1) then ones = 0,因此,ones* = (ones ^ num) & ~twos
同样的,对于 twos,twos = (twos ^ num),if (ones* == 1) then twos = 0,因此,twos* = (twos ^ num) & ~ones*。注意,ones* 是 ones 的新值,因此,twos 需要后计算
// Runtime: 12 ms
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ones = 0, twos = 0;
for (auto num : nums) {
ones = (ones ^ num) & ~twos;
twos = (twos ^ num) & ~ones;
}
return ones;
}
};
Difficulty: Medium
数组中的数两两出现,只有两个出现了一次,把它们找出来
如果可以把数组中的数分成两类,则转化为已经解决过的问题。考虑到数组中所有元素相异或,得到 a ^ b,必有一位数字为 1,该位表明,a 和 b 在这一位是不同的,因此,可以按照这一位来分类。遍历两遍数组即可实现。
// Runtime: 20 ms
class Solution {
public:
vector<int> singleNumber(vector<int>& nums) {
int all = 0;
for (auto n : nums) {
all ^= n;
}
int bit1 = 1;
while ((all & 1) == 0) { // Operator precedence, & is less than ==
all >>= 1;
bit1 <<= 1;
}
vector<int> ret;
int a = 0, b = 0;
for (auto n : nums) {
if (n & bit1) {
a ^= n;
} else {
b ^= n;
}
}
ret.push_back(a);
ret.push_back(b);
return ret;
}
};
https://leetcode.com/discuss/6632/challenge-me-thx
000–>001–>010–>011–>100–>101(000)
threes(nc) twos(nb) ones(na)
对于 nb,在 A[i] 和 na 的相应位均为 1 的情况下,nb 的相应位取反,若 A[i] 和 na 的相应位至少有一个 0,则 nb 保持。因此,nb = nb ^ (A[i] & na)
对于 na,na = na ^ A[i],但是,如果 nc 为 1,则 na 还需要继续保持为 0。因此,na = (na ^ A[i]) & ~nc
对于 nc,当新的 nb,nc 均为 0,并且 A[i] 为 1 时,nc 取反。因此,nc = nc ^ (A[i] & ~na & ~nb)
略显复杂,一定要结合每一位的变化来考虑。另外,LeetCode 的讨论中还给出一种算法,暂时还没看懂,也一并放到这里。
// 貌似这个更好理解一些
int singleNumber(int A[], int n) {
int na = 0, nb = 0, nc = 0;
for(int i = 0; i < n; i++){
nb = nb ^ (A[i] & na);
na = (na ^ A[i]) & ~nc;
nc = nc ^ (A[i] & ~na & ~nb);
}
return na & ~nb & ~nc;
}
int singleNumber(int A[], int n) {
int twos = 0xffffffff, threes = 0xffffffff, ones = 0;
for(int i = 0; i < n; i++){
threes = threes ^ (A[i] & twos);
twos = (twos ^ A[i]) & ~ones;
ones = ones ^ (A[i] & ~twos & ~threes);
}
return ones;
}
写完后发现也有人写了 general 的方法
再观察一下 5n + 1 的算法,可以看出,nb 和 na 都是由老的 nb 和 na 算出来的,而 nc 则是由新的 nb 和 na 算出来的,自然地,它也可以由老的 nb 和 na 算出来。如果我们保留原值,思考新值的变化将会容易很多。
即 nc = (nc ^ (na & nb & x)) & ~(nc & x),异或也可以改为或。na,nb,x 全为 1 时,nc 为 1,nc 和 x 为 1 时,nc 为 0。
写到这里,自然会想到,是否有通解?
我们再来回顾一下 3n + 1, 5n + 1, 7n + 1
c | b | a | count |
---|---|---|---|
0 | 0 | 0 | 0 |
0 | 0 | 1 | 1 |
0 | 1 | 0 | 2 |
0 | 1 | 1 | 3 |
1 | 0 | 0 | 4 |
1 | 0 | 1 | 5 |
1 | 1 | 0 | 6 |
3n + 1,取 count = 0, 1, 2
a = (old_a ^ x) & ~(old_b & x);
b = (old_b ^ (old_a & x)) & ~(old_b & x);
5n + 1,取 count = 0, 1, 2, 3, 4
a = (old_a ^ x) & ~(old_c & x);
b = (old_b ^ (old_a & x)) & ~(old_c & x);
c = (old_c ^ (old_b & old_a & x)) & ~(old_c & x);
7n + 1,取 count = 0, 1, 2, 3, 4, 5, 6
a = (old_a ^ x) & ~(old_c & old_b & x);
b = (old_b ^ (old_a & x)) & ~(old_c & old_b & x);
c = (old_c ^ (old_b & old_a & x)) & ~(old_c & old_b & x);
基本上一目了然了,根据 count 的最大取值可能,模拟 count 的每一位。某一位之后的所有位以及输入全为 1 时,则进位(注意,此处为异或);当计数器为 max count 并且输入为 1 时,则清零(reset)。因此,每一位的计算均由两部分构成。进位项 & 清零项。
需要注意的是,最高位用 异或 或者 或 均可,不会出现同时为 1 的情况。
实现上的考虑:根据 max count 确定的位数构造数组,进位项很容易实现,清零项可通过判断 max count 的相应为为 0 为 1 来决定是否使用相应的 old 项,然后再与 x 想与即可。
是否可以不依赖于 old 项有待进一步思考,暂时作为一个 open problem。
看到了解决方案……其实就是从高位到低位进行运算 (c–>b–>a),等所有的新值都计算出来之后,再计算 reset 项,这时的 reset 项不需要考虑 x 了。最后,每个新值都需要与 reset 项相与。其实就是 Single Number II 中的第一个算法。
由于所有元素出现的次数均为奇数,因此无法通过按位相与对元素分类。可以通过 Hash Table 记录元素出现的次数。