https://oj.leetcode.com/problems/single-number-ii/
Given an array of integers, every element appears three times except for one. Find that single one.
Note:
Your algorithm should have a linear runtime complexity. Could you implement it without using extra memory?
这一题把重复两次,变成重复三次,难度瞬间就特别不一样了。HashMap依旧是一个可选项,但必然就不是without using extra memory了。而且,单纯的异或也不管用了。也没有什么简单的bit operation能够化解这个东西了。那么,下面,我要首先感谢code ganker。反正我是肯定想不出这种复杂的bit operation解决方案的。所以直接先给代码,然后我再努力解释一下:
public int singleNumber(int[] A) {
int ones = 0,twos = 0, xthrees = 0;
for(int i : A){
twos |= (i & ones);
ones ^= i;
xthrees = ~(ones & twos);
ones &= xthrees;
twos &= xthrees;
}
return ones;
}
好,我们看到了三个变量,ones, twos, xthrees。里面保存着什么呢?正如其名。
ones保存的是只出现过一次的bit,twos保存的是只出现过两次的bit,xthrees实际上是一个filter,出现过三次的bit是0,反之则是1.
twos |= (i & ones); 这一句话的含义在于将当前数字的二进制bit和ones进行与操作,这样留下的,就是出现过两次的bit。
ones ^= i: ones里面保存的是只出现过一次的bit,所以如果在这里i也出现了,自然是要剔除的。
xthrees = ~(ones & twos); 事实上上面两步并不能保证twos和ones里不存在出现过三次的bit。举个例子,如果某一位的bit,在twos里是1,在i里也是1,但是ones里是0。那么在经历过第一次和第二次的操作的时候,ones和twos里那一位都会是1。会造成这种情况实际是因为这个bit已经出现过两次,所以在第一次出现的时候,ones里是1,twos里是0,在第二次出现的时候twos里是1,ones里是0。在第三次出现的时候就会是这样。于是乎xthrees就会承担filter的作用清除twos和ones里的这一bit。下面两句就是进行filter的。
所以最后还留在ones里的便是出现过一次的bit。根据题意,数字有出现过三次的和唯一一个出现过一次的。所以某一位bit出现的次数k可能只有两种:k % 3 == 0, 或者 k % 3 == 1。事实上ones里保留的并不是真的只出现过一次的bit,而是k % 3 == 1的bit。但就这样,就足够还原出原来的数字了。
2018-01-20
我觉得真的非常有必要重新回看一下这一题,因为刚看的时候只记得大概是 与或异或的操作却完全不记得具体如何。所以我们再一步步的寻找这一题的答案吧。接下来的所有解答都参考了leetcode的discuss板块,里面的人真的很牛逼。
我们先给出一种非常容易理解的解答。首先要明白因为不是偶数,所以异或的做法不能用了。这里需要统计的是一个整型数32位,每一位出现的次数模除3可以等于1。所以一个非常直观的算法出现了,就是循环数组所有数字32次,记录第一位到第三十二位出现的次数。这样做依旧是符合o(n)的要求的,因为o(32n)依旧是o(n)
给出代码如下
public int singleNumber(int[] nums) {
int result = 0;
int count;
for (int i = 0; i < 32; i++) {
count = 0;
for (int num : nums) {
if ((num & (1 << i)) != 0) {
count++;
}
}
if (count % 3 == 1) {
result |= 1 << i;
}
}
return result;
}这种做法的好处在于非常直观,容易理解,而且很好延伸出譬如数组中其他数字出现了任意奇数次的情况(因为偶数次都可以用异或法解决)。但是毕竟常数量是32,performance是没有那么好的。但也能够通过leetcode的检查。
当然,为了达到同样的目的,我们完全不需要32位一个个bit去进行统计。我们可以通过异或运算和与运算一次过达到目的。先给出一段代码如下:
public int singleNumber(int[] nums) {
int ones = 0, twos = 0;
for (int i : nums) {
ones ^= i & ~twos;
twos ^= i & ~ones;
}
return ones;
}是不是很神奇,是不是很懵逼?这段代码比我四年前写的代码简单多了,却是同样的效果。
先让我们复习一下异或的效果:1^0 = 1, 0 ^ 1 = 1, 0 ^ 0 = 0, 1 ^ 1 = 0。也就是如果当前位两者一样为0,不一样则为1。所以当我们做ones ^= i & ~twos的时候,表示的是,如果i这个数字用二进制表示时,出现的1而ones没有的(同时twos,也就是出现过两次的也不能有。所以我们做了~twos),就赋予ones,但是如果ones在那些位置上出现了,则表示这些bits出现了不止一次了,需要清零。twos的原理也是一样的,它的二进制表达是出现过两次(模除3)的bits。i ^ ~ones的作用是取得出现在i但没有出现在ones的数字,然后twos和其进行异或,也就是在i为1且没有出现在ones的bit中,出现过在twos的bit清零,没有的话就变成1。这里隐含了一件事情。在ones中,bit为0的情况有两种:一是从来没有出现在之前的数字里,二是出现过又被清理掉。然而如果当我们做twos ^= i & ~ones时,如果ones没有但是i有,其实就是这些在ones中为0的数字就是被当前这个i被清理掉了。所以我们可以确信这个数字出现了两次(模除3)。
简单点说:
一个特定位置上的bit,如果在遍历中出现过1次,那么它会出现在ones里但不会出现在twos里,因为在ones ^= i & ~twos中,ones那个bit是0,i中那个bit是1,~twos中那个bit是1。所以就是 0 ^ (1 & 1) = 1。而在接下来的twos ^= i & ~ones中,twos那个bit是0,i那个bit是1,~ones中那个bit是0(因为one中那个bit刚变成1)。所以就是0 ^ (1 & 0) = 0。
如果出现过两次,那么它会出现在twos里但不会出现在ones里,因为此时的ones ^= i & ~twos是 1 ^ (1 & 1) = 0,而twos ^= i & ~ones就变成了 0 ^ (1 & 1) = 1(推理过程就不重复了)。
当第三次出现的时候,那么ones和twos里面那个bit都会是0。 因为此时的ones ^= i & ~twos是 0 ^ (1 & 0) = 0,而twos ^= i & ~ones就是 1 ^ (1 & 1) = 0。
此时,一个循环就完成了,那么出现第四次,第五次,第六次的时候,就会重复上述第一次,第二次,第三次的流程,长此往复。因为抽掉那个特殊数字,所有bit出现的次数必然为3的倍数,再加入那个特殊的数字,那个特殊数字的二进制的bits就会出现3n + 1次。也就是还剩下在ones里的bits就是那个特殊数字了。其实这里还隐含着一件事情,一个bit,不会同时在ones和twos里存在,因为不会有一个数字模除3同时为1和2,也符合算法特征
我一直在思考怎么简单的去描述这个算法
ones ^= i & ~twos (ones对i说,你有,我没有而且twos没有的给我,但如果我有了,你就吃掉)
twos ^= i & ~ones (twos对i说,你有,我没有而且ones刚才有的被你吃掉的就给我,但如果我有了,你就吃掉)
这个做法的优点在于真的简洁,思维很聪慧很屌炸天。缺点在于难以理解和扩展(主要是难以理解)。但是也不是不能扩展的,https://discuss.leetcode.com/topic/2031/challenge-me-thx/17 给出了一个可以通过类似思路扩展到如果所有别的数字都出现了5次的情况。然而你们玩得开心,我下次回来再好好斟酌。。。囧rz...
本文详细解析了LeetCode中的单数III问题,提供了一种直观的统计每位出现次数的方法,并介绍了使用位操作实现线性时间和常数空间复杂度的高效算法。
412

被折叠的 条评论
为什么被折叠?



