Single number I
给定一个整数数组, 数组中的每个数都出现了 2 次,除了一个数, 它只出现了 1 次,请将它找出来.
思路
位操作 ^
有着非常漂亮的性质,首先,它支持交换律:
A ^ B = B ^ A
其次,它还支持结合率:
A ^ B ^ C = A ^ (B ^ C) = (A ^ B) ^ C
然后, ^
本身具有这样的语义:
A ^ A = 0, A ^ 0 = A
我们可以很清楚地知道:
A ^ B ^ C ^ ... ^ A ^ ... = A ^ A ^ B ^ C ^ ...
因此,这个问题就变得非常简单了:
int singleNumber(int A[], int n) {
int result = 0;
for (int i = 0; i < n; i++)
{
result ^= A[i];
}
return result;
}
Single number II
给定一个整数数组, 数组中的每个数都出现了 3 次,除了一个数, 它只出现了 1 次,请将它找出来.
思路
实现的思路是基于数组的元素是整数,我们通过统计整数的每一位来得到出现次数。我们知道如果每个元素重复出现三次,那么每一位出现 1 的次数也会是 3 的倍数,如果我们统计完对每一位进行取余 3 ,那么结果中就只剩下那个出现一次的元素。总体只需要对数组进行一次线性扫描,统计完之后每一位进行取余 3 并且将位数字赋给结果整数,这是一个常量操作(因为整数的位数是固定 32 位),所以时间复杂度是 O(n) 。而空间复杂度需要一个 32 个元素的数组,也是固定的,因而空间复杂度是 O(1) 。代码如下:
class Solution {
public:
int singleNumber(vector<int>& nums) {
int bits[32] = { 0 }; // 这里已经全部初始化过了
int result = 0;
for (int i = 0; i < nums.size(); i++) {
for (int j = 0; j < 32; j++) {
bits[j] = bits[j] + ((nums[i] >> j) & 0x1);
}
}
// bits[i] % 3只有0和1两种结果
for (int i = 0; i < 32; i++) {
result += (bits[i] % 3) << i;
}
return result;
}
};
更加通用的解法
下面的文章翻译自https://leetcode.com/problems/single-number-ii/discuss/
给一个整数数组,其中每个数都出现了 k 次( k > 1 ),除了一个数,它出现了 p 次,( p >= 1, p % k != 0 ),请找出这个数.
为了使用位操作解决这个问题,我们应当思考一下计算机是如何表示一个整数的.很明显,答案是二进制比特.首先,让我们考虑仅有一个bit的情况,假定我们有一个由仅有一个bit的数构成的数组(这些数只可能是 0 或者 1 ),我们要在这个数组中数 1 的个数,只要碰到的 1 的个数达到了特定的数目,比如说k ,计数值重新设为0,然后重新开始计数 .为了记录我们遇到过的1的数目,我们需要一个计数器counter.假定计数器有m个bit,它在电脑中可以这样来表示 xm, …, x1 我们可以总结出关于计数器counter的一些性质.
- counter有一个初始值,也就是 0 ;
- 对于数组中的每个数,如果我们碰到了一个 0 ,那么counter不会改变;
- 我们如果在数组中碰到了一个 1 ,那么计数器counter应该加 1 ;
- 为了使得计数器能够记到 k 次,那么 2m >= k ,这也就意味着 m >= logk .
接下来就是最核心的问题了,随着我们遍历数组, counter里面的每个bit( x1 到 xm )会发生什么样的改变? 我们先回答一个简单一点的问题,如果仅仅为了满足上面的第二条性质,什么位操作可以满足需求呢?这种位操作为 |
或者 ^
(异或), 因为 x = x | 0 以及 x = x ^ 0 .
我们已经有了些许头绪: x = x | i 或者 x = x ^ i, i 为数组中的元素. 那么究竟哪一个位操作更加合适呢?我们现在还不知道.但是我们可以用一些实际的例子来测试一下.
在最开始遍历之前, counter中的每一个bit都为 0 ,即 xm = 0, …, x1 = 0 . 由于我们要选择某种位操作,以保证遇到 0 的时候,counter中的每一个bit都不会改变, 直到我们遇到第一个 1 之前, counter一直为 0, 我们遍历到第一个 1之后,有 xm = 0, …,x2 = 0, x1 = 1 ,我们继续, 遇到第 2个 1 之后,有 xm = 0, …, x2 = 1, x1 = 0. 值得注意的是, x1 从 1 变为了 0 ,如果采用操作 x1 = x1 | i 的话,在遇到第 2 个 1 之后, x1 将仍为 1 ,很明显,操作 x1 = x1^ i 更为合适.那么 x2, …, xm 呢?
接下来的疑问是,在什么条件下,使得 x2, …, xm 将会改变他们的值,我们拿 x2 做一个例子.如果我们遍历到了一个 1 之后, x2 的值也发生了改变,在改变之前, x1 的值应当是什么呢?答案是 1 ,否则的话, x2 的值是不可能改变的,因为将 x1 从 0 变为 1 是个更为恰当的选择.
因此 x2 将会改变当且仅当 x1 和 i 都为 1 ,用更为抽象的形式可以表达为 x2 = x2 ^ (x1 & i). 推广这个结论的话,我们可以得到 xm 的值改变的条件为,仅当 xm-1, …, x1 以及 i 全部为 1 : xm = xm ^ (xm-1 & … & x1 & i). 到了这里,我们基本上就已经可以确定对应的位操作了.
也许,你已经注意到了,如果 k < 2m - 1 的话,上面提及的位操作会从 0 计数到 2m - 1, 而不是 k ,,我们需要某种切分(cutting)机制,使得当counter计数达到了 k 的时候,重新将counter变为 0, , 因为这个缘故,我们可以采用 &
操作,将 xm, …, x1 和掩码 mask 进行 & 操作. 即 xm = xm & mask, …, x1 = x1 & mask. 如果我们可以保证当counter达到 k 的时候, mask 为0,而其余时候,掩码都为 1 的话,那么一切都完成了.关键的问题在于,我们应当如何达成这个目标?
对于每一个计数值,counter都有一个独特的bit值的组合与之对应,我们可以将其视为状态.如果我们写出 k 的二进制形式 km, …, k1 ( k 为其余的数出现的次数).那么我们可以通过下面的方法来构建掩码 mask:
mask = ~(y1 & y2 & .. & ym), 其中如果 kj = 1 的话, yj = xj ,如果 kj = 0 的话, yj = ~xj (j从1 到 m)
接下来举一些例子:
k = 3: k1 = 1, k2 = 1, mask = ~(x1 & x2), 当计数值达到 3 时, x1 = 1, x2 = 1, mask = ~(1 & 1) = 0, x1 = x1 & mask = 0, x2 = x2 & mask = 0, 计数值为 1 时, x1 = 1, x2 = 0, mask = ~(1 & 0) = 1, x1 = 1, x2 = 0 .
k = 5: k1 = 1, k2 = 0, k3 = 1, mask = ~(x1 & ~x2 & x3) ,和上面类似,计数值达到 5 时, mask = 1 , 其余时候, mask = 0.
总之,我们的算法长这个样子:
for (int i : array) {
xm ^= (xm-1 & ... & x1 & i);
xm-1 ^= (xm-2 & ... & x1 & i);
.....
x1 ^= i;
mask = ~(y1 & y2 & ... & ym) where yj = xj if kj = 1 and yj = ~xj if kj = 0 (j = 1 to m).
xm &= mask;
......
x1 &= mask;
}
现在,是时候将我们的结果从1-bit的数的例子扩展到32-bit的整数了,一个直接了当的方式是构建32个counter,整数中的每个bit都对应一个counter.但是,如果你对位操作更为熟悉一点的话,我们其实可以同时操作这 32 个counter的,这意味着,我们可以仅用 m 个32-bit的整数而不是用32个m-bit的counter 来完成我们的操作, 其中 m 是满足 m >= logk 的最小整数.
可以同时操控这 32 个bit的原因在于,一个 32 位的整数可以被看做是32个独立的位,每一个位都可以单独考虑.又由于所有的位的操作都是相同的,所以我们可以将 32 个计数器组合在一起,
因为每个counter有 m 个比特,我们最终只需要 m 个32-bit的整数即可完成我们的计算.因此,在上面的算法中,我们仅需要将 x1 到 xm 视作一个32-bit的整数而不是一个1-bit的数,很容易,不是吗?
为了下文叙述的方便,我们将问题中要找的值记为 s .
最后一个疑问是 s 在哪里,或者说, x1 到 xm 中的哪一个等于 s .
在回答这个问题之前,我们首先要弄清楚, m 个32-bit的整数 x1 到 xm 代表着什么.我们用 x1 作为一个例子. x1 有 32 个比特,让我们将这些比特位标记为 r( r = 1 to 32 ).当我们遍历完整个输入数组之后, x1 的第 r 个bit将会由数组中所有数的第 r 个bit来决定,更确切地说,由数组中的所有数的第 r 个bit中 1 的数目所决定,假定在第 r 个bit位上 1 一共出现了 q 次, q’ = q % k ,它的二进制表示形式为 q’m, …, q’1 ,根据上面的图可以知道, x1 的第 r 个bit将会等于 q’1.
x1 的第 r 个bit为 1 ,这意味着什么? 究竟是什么原因使得该bit为 1 呢?一个元素在数组中出现了 k 次能够使得 x1 的第 r 个bit为 1 吗?答案是否定的,为什么呢?因为一个元素如果能够使得第 r 个bit为 1的话,它必须同时满足两个条件,第一是这个元素的第 r 个bit为 1 ,并且,该元素的出现次数不为 k 的倍数.
第一个条件显而易见,第二个条件来自这样一个事实.当计数器的值达到 k 之后,它将会重设为 0 ,这意味着,如果一个元素出现了 k 次, x1 中对应的bit位将会被设定为 0,那些在数组中出现了 k 次的元素是不可能同时满足这两个条件的.
因此到最后,只有出现了 p 次的 s 会在计数器中留下踪迹. 如果 p 为 k 的倍数,即 p % k == 0, 那么遍历完数组所有的计数器都会变为 0 ,因为计数器一旦到达 k,就会立马归零,这也是我们前面为什么要设定 p % k != 0这个条件的原因.我们将 p’ = p % k 记为该元素出现的有效次数.
将 p’ 写成二进制形式: p’m, …, p’1 , x1 == s 的条件为 p’1 = 1, 也就是说 p’1 == 1 时, x1 的值和 s 相等.下面是一个简单的证明.
如果 x1 的第 r 个bit为 1 , 那么 s 的第 r 个bit也必定为 1 , 这一点显而易见. 我们接下来要证明的是,如果 x1 的第 r 个bit为 0 ,那么 s 第 r 个bit也为 0 ,这一点不太好证明,但是我们可以从另外一个角度来证明.
假定 s 第 r 个bit为 1 ,并且遍历数组之后, x1 的第 r 个bit为 0, 实际上,遍历完数组, s 的第 r 个bit上的 1 将会被计数 p’ 次,我们将 p’ 写成二进制形式 p’m, …, p’1, 根据定义, x1 的第 r 个bit会等于 p’1, 根据前提, p’1 == 1, 我们得到了和之前的假定( x1 的第 r 个bit为 0 )矛盾的结果,因此, s 的第 r 个bit为 1 的话,在 p’1 等于1 的前提下, x1 的第 r 个bit也必定为 1 . 此外,如果 s 的 r 个bit为 0, 那么 x1 的第 r 个bit也绝对为 0 (显而易见).
总结起来的话,在 p’1 = 1 的前提下, s == x1 .
因为这个对 x1 的所有bit都成立,因此我们可以得到结论:如果 p’1 = 1的话, x1 == s .相似的,我们同样可以证明如果 p’j = 1的话, xj == s ( j = 1 to m ).
现在一切问题都解决了,首先计算 p’ = p % k, 并将 p’ 表示成它的二进制形式,如果 p’j = 1, 则 xj == s ( j = 1 到 m ).
最后总结一下,整个算法的时间复杂度为 O(n * logk) , 空间复杂度为 o(logk).
下面是一些例子:
- k = 2, p = 1
其余数字都出现 2 次,另外一个数字出现 1 次,这样的话,我们需要一个32-bit的整数作为计数器,
k为 2 , m 为 1 , 我们仅需要一个32-bit的整数( x1 ) 作为计数器.由 2m = k,因此我们甚至不需要掩码值了,下面是完整的java代码:
public int singleNumber(int[] A) {
int x1 = 0;
for (int i : A) {
x1 ^= i;
}
return x1;
}
- k = 3, p = 1
一个数字出现 1 次,其余数字出现 3 次,我们需要两个32-bit的整数( x2 , x1 )做计数器. 由于 2m > k , 因此我们需要一个掩码,将 k 的二进制形式表示出来: k = 11 , 有 k1 = 1, k2 = 1 ,因此,我们有掩码 mask = ~(x1 & x2).完整的java代码如下:
public int singleNumber(int[] A) {
int x1 = 0;
int x2 = 0;
int mask = 0;
for (int i : A) {
x2 ^= x1 & i; // x2 = x2 ^ (x1 & i)
x1 ^= i; // x1 = x1 ^ i
mask = ~(x1 & x2);
x2 &= mask;
x1 &= mask;
}
return x1;
// p = 1, in binary form p = '01', then p1 = 1, so we should return x1;
// if p = 2, in binary form p = '10', then p2 = 1, so we should return x2.
}
- k = 5, p = 3
k 为 5 ,那么 m = 3, 我们需要 3 个32-bit的整数( x3, x2, x1 )作为计数器. 由于 2m > k 因此,我们需要一个掩码.将 k 写成二进制的形式: k = 101, 因此 k1= 1, k2 = 0, k3 = 1 ,所以 mask = ~(x1 & ~x2 & x3) , java代码如下:
public int singleNumber(int[] A) {
int x1 = 0;
int x2 = 0;
int x3 = 0;
int mask = 0;
for (int i : A) {
x3 ^= x2 & x1 & i;
x2 ^= x1 & i;
x1 ^= i;
mask = ~(x1 & ~x2 & x3);
x3 &= mask;
x2 &= mask;
x1 &= mask;
}
return x1; // p = 3, in binary form p = '011', then p1 = p2 = 1,
// so we can return either x1 or x2;
// But if p = 4, in binary form p = '100', then only p3 = 1,
// which implies we can only return x3.
}
你可以很快想到其他的例子.
single number III
给定一个整数数组,其中有两个数出现了 1 次, 其余的数出现了 2 次, 请找出这两个数.
思路
我们依旧要利用 ^
这个位操作.和前面有所不同的是,这一次,我们要扫描两遍数组.
第一遍用于获取两个数异或的值.由于这两个值不相等,所以这个值不会为0,我们假定最终的结果为 s .
我们在 s 中选定一个不为 0 的bit, 将其作为一个切分点,
第二遍,我们需要将所有的数分为两组, 怎么分呢,这就要用到上面提到的切分点了,假设这个切分点的值为 k , 那么 k 的二进制形式中,仅有一个bit为 1 , 假设这个位置为 r ,其余bit都为 0, 让数组中所有的数和 k 进行 &
操作,结果只有 1 和 0 两种,因此,依靠这点,就可以将这些数分成两组,而且可以保证,我们要求的两个数必定在不同的组.因为这两个数在 r**bit上必定是一个为 **0 , 一个为 1 , 这样 k 的第 r 个bit才会为1 .而且更妙的是,相同的数会落在同一组中,这样, 对每一组里的数执行异或操作,就可以得到要求的值.
class Solution
{
public:
vector<int> singleNumber(vector<int>& nums)
{
// Pass 1 :
// Get the XOR of the two numbers we need to find
int diff = accumulate(nums.begin(), nums.end(), 0, bit_xor<int>());
// Get its last set bit
diff &= -diff;
// Pass 2 :
vector<int> rets = {0, 0}; // this vector stores the two numbers we will return
for (int num : nums)
{
if ((num & diff) == 0) // the bit is not set
{
rets[0] ^= num;
}
else // the bit is set
{
rets[1] ^= num;
}
}
return rets;
}
};