Single Number是一个位操作(Bit Operation)系列的题目,对于像我这种之前对bit Operation接触不是太多的同学,看到很多博客一笔带过的解析可能很懵。下面一起先易后难,从Single Number到Single Number II,逐步拨云见日。
1.Single Number
题目的意思是:在一个非空的整型数数组中,除了某一个元素只出现了1次之外,其余元素都出现了2次,寻找出那个只出现了1次的元素。下面先直接给出bit Operation的解法,再给出解释。
class Solution {
public:
int singleNumber(vector<int>& nums) {
if(nums.size()==1)
return nums[0];
else
{
int res = 0;
for(auto num:nums) //C++11中的auto此处的功能是依次取出nums数组中的元素放在num中
// for(int i=0; i<nums.size();i++) num = nums[i] 即相当于这样的功能
{
res ^=num; // ^即为按位异或(xor)操作
}
return res;
}
}
};
首先复习一下异或操作,即相同出0,不同出1。按位异或就是将此处的整型数(十进制)转换为01二进制之后,逐位进行异或操作。看到答案的时候可能会疑惑,为什么一个简单的连续按位异或操作就可以找出只出现了一次的元素。下面举一个例子。
nums[0]-->数字4:0-1-0-0(二进制) //nums[0]与0按位异或结果是其本身
nums[1]-->数字1:0-0-0-1(二进制) //上一轮结果(nums[0])与nums[1]按位异或等于0-1-0-1
nums[2]-->数字2:0-0-1-0(二进制) //上一轮结果(0-1-0-1)与nums[2]按位异或等于0-1-1-1
nums[3]-->数字1:0-0-0-1(二进制) //上一轮结果(0-1-1-1)与nums[3]按位异或等于0-1-1-0
nums[4]-->数字2:0-0-1-0(二进制) //上一轮结果(0-1-1-0)与nums[4]按位异或等于0-1-0-0
//最后输出结果为0-1-0-0,即为十进制数4.
从上面的例子中可以看出,在按位异或的操作中,一个元素会使得特定bit位上的数字出现变号(0->1或者1->0
),相同元素出现2次会让特定bit位上数字出现2次变号,变化两次等于没有变,所以对按位异或的结果没有造成影响。而只出现1次的元素,在使得特定bit位的发生变号之后,没有相同元素再次出现抵消其特定位置上的变号,所以连续按位异或的结果就使得只出现了1次的元素钉在了柱子上。
从例子中理解一下此处简单的总结:针对某一个bit位而言,连续的异或操作实现的功能是第一次遇到编码1的时候变成1并保持,一直保持直到下一次(第二次)遇到编码1。偶数次遇到编码1最后连续异或的结果是0,奇数次的遇到编码1最后连续异或的结果是1。
2.single Number II
简单解释一下题目,上一题是从2次的当中找1次的,升级之后是从3次的当中找1次的。出现偶数次刚好在按位异或操作中可以相互抵消在特定bit位上操作,出现3次的其中2次相互抵消,还剩余1次的影响仍存在,混淆只出现了1次的元素在特定bit位上的影响。
升级版Single NumberII最暴力的解法是把每一个十进制数对应的二进制的bit位都统计一遍,把出现1的次数对3取余,那么出现3次取余变成0,只剩下出现1次的。这种暴力解法是GrandYang文章中的第一种方法,思路简单,此处不过多解析。
2.1 思路分析
以下考虑仍然沿着按位异或的思路往前走,直接看例子会更加直观。
nums[0]-->数字8:1-0-0-0(二进制) //nums[0]与0按位异或结果是其本身
nums[1]-->数字5:0-1-0-1(二进制) //上一轮结果(nums[0])与nums[1]按位异或等于1-1-0-1
nums[2]-->数字4:0-1-0-0(二进制) //上一轮结果(1-1-0-1)与nums[2]按位异或等于1-0-0-1
nums[3]-->数字5:0-1-0-1(二进制) //上一轮结果(1-0-0-1)与nums[3]按位异或等于1-1-0-0
nums[4]-->数字4:0-1-0-0(二进制) //上一轮结果(1-1-0-0)与nums[4]按位异或等于1-0-0-0
nums[5]-->数字5:0-1-0-1(二进制) //上一轮结果(1-0-0-0)与nums[5]按位异或等于1-1-0-1
nums[6]-->数字4:0-1-0-0(二进制) //上一轮结果(0-1-1-0)与nums[6]按位异或等于1-0-0-1
首先,直观可以看出,连续按位异或的结果已经不是想要的数字8了,在连续异或从nums[0]到nums[4]的结果是符合前一题的规律的,即数字4和数字5各出现了2次,然后连续异或使得对特定bit位的影响相互抵消,只剩下数字8。而之后nums[5]和nums[6]继续出现第三个数字4和第三个数字5的时候,其在特定的bit位上造成了影响,数字8无法被从连续异或的结果中找出来。
所以影响来自于第三次出现的数字,它的出现造成了无法被抵消的影响,使得最后连续异或的结果中既有第一次出现的待寻元素又有第三次出现的元素的影响。那么问题转化为“如何消除元素第三次出现的影响”,要想消除其影响前提是“如何识别元素是第三次出现”。
沿着连续按位异或操作的思路继续往前走。因为是bit Operation,在这一题中每一个bit位都是执行连续按位异或操作,所以每一个bit位都是一样的,所以以下的分析是针对其中某一个bit位进行。
对某一个bit位而言,连续的按位异或操作行使的功能是在遇到一次1之后,连续异或结果变成1并保持;并且该结果保持直到遇到下一个1,结果变成0。通过下面的例子也可简单再次印证加黑部分的结论。
//数组为[4, 1, 2, 1, 2, 1, 2 ]
nums: 4 000100 Result of Continual Xor: 4 000100
nums: 1 000001 Result of Continual Xor: 5 000101
nums: 2 000010 Result of Continual Xor: 7 000111
nums: 1 000001 Result of Continual Xor: 6 000110
nums: 2 000010 Result of Continual Xor: 5 000101
nums: 1 000001 Result of Continual Xor: 6 000110
nums: 2 000010 Result of Continual Xor: 5 000101
那么,要记录某一个bit位上1出现多次就可以利用此处的情况,用"已经连续异或的结果"与"待异或的数"首先进行“与操作”(与操作是全1出1,否则出0),那么当"已经连续异或的结果"与"待异或的数"同时都为1的时候即该bit位再次遇见1。其中加黑强调的部分是需要注意的点,连续异或的结果等于1只能已经异或了奇数个1,再次在等待异或的数当中遇到了1,只能说明再次(也是第偶数次)遇到了1,无法具体说明是第几次。具体可以看下面的例子,无法从与操作之后的结果中判断到底出现了几次,甚至有点什么东西都看不出来的感觉。
int twos =0; //"已经连续异或的结果"与"待异或的数"进行"与操作"的结果
int ones =0; //数组中数值连续异或的结果 即 Result of Continual Xor
//其中twos |= nums[i] & ones; //实际上记录了该bit位是否有重复出现1
nums: 4 000100 twos: 0 000000 Result of Continual Xor: 4 000100
nums: 1 000001 twos: 0 000000 Result of Continual Xor: 5 000101
nums: 2 000010 twos: 0 000000 Result of Continual Xor: 7 000111
nums: 1 000001 twos: 1 000001 Result of Continual Xor: 6 000110
nums: 2 000010 twos: 3 000011 Result of Continual Xor: 5 000101
nums: 1 000001 twos: 3 000011 Result of Continual Xor: 6 000110
nums: 2 000010 twos: 3 000011 Result of Continual Xor: 5 000101
为了方便后的叙述,我们定义连续异或的结果为result,记录某个bit位出现两次1的变量为two,记录某个bit位出现三次1的变量为three。
2.2 解法一
首先注意本题中的数字只可能出现3次或者1次,如果能找出哪些是出现2次的,也就等于找出了哪些是出现3次的,也就是找出哪些是出现超过一次的即可。只要在twos上有体现超过一次的就可以认为是出现3次的,既可以记录这些bit位进行额外的处理。额外的处理就是让这些bit位再次与1进行异或,进而形成偶数次的异或,抵消对连续异或的影响。操作起来就是在连续异或完成之后,将连续异或的结果与记录是否重复出现的twos进行按位异或操作。
注:上面给出了如何记录某个bit位出现了两次1,很多博文提到要注意a操作与b操作之间的顺序问题。a."已经连续异或的结果"与"待异或的数"之间的“与操作”;b.对待处理数组不断进行的连续按位异或操作。也就是说,在从数组中取出来一个数进行处理的时候,是应该:1)先进行连续异或,然后将异或的结果与该数进行与操作,还是应该:2)将取出来的数与上一轮的异或结果首先进行与操作。很显然,首先应该进行与操作,比较上一轮已有的连续异或结果与新取出来的数之间的关系,如果上一轮连续异或结果为1(即记录已经出现了一次1),遇到新的1进行与操作结果就是1(即第2次遇到了1,表示为tows=1)。反之,如果让取出来的数先进行连续按位异或,如果两次出现1,连续异或的结果为0,该结果再进行与操作无法得出two=1;
看似问题似乎解决了,然而还是有致命漏洞的,预知后事如何,请看下面分解。
//错误代码
class Solution {
public:
int singleNumber(vector<int>& nums) {
int one = 0, two = 0, three = 0;
for (int i = 0; i < nums.size(); ++i)
{
two |= one & nums[i];
one ^= nums[i];
}
ones = ones ^ twos;
return one;
}
};
//错误代码无法处理的情况
nums: 1 00000000001 ones: 00000000001 twos: 00000000000
nums: 4 00000000100 ones: 00000000101 twos: 00000000000
nums: 6 00000000110 ones: 00000000011 twos: 00000000100
nums: 1 00000000001 ones: 00000000010 twos: 00000000101
nums: 4 00000000100 ones: 00000000110 twos: 00000000101
nums: 4 00000000100 ones: 00000000010 twos: 00000000101
nums: 1 00000000001 ones: 00000000011 twos: 00000000101
nums: 5 00000000101 ones: 00000000110 twos: 00000000101
nums: 6 00000000110 ones: 00000000000 twos: 00000000111
nums: 6 00000000110 ones: 00000000110 twos: 00000000111
Result: 1 00000000001 //最后ones^twos的结果
不难看出,导致上述问题的原因在于最后一个bit位上出现了4次1,倒数第二个bit位上出现了3次1,倒数第三个bit位上出现了7次1,最终异或的结果肯定会体现为110(因为偶数次的1已经被抵消了),而每一个bit位均出现了多次1所以对应twos的每个bit位都为1,这样两者之间异或已经无法达到目的。
所以不断连续异或,在最后的时候将连续异或结果与(记录是否多次出现1的变量)twos异或,企图将原本出现3次的1再经过最后一次异或(形成偶数次)来抵消其影响。正确的应该是对出现奇数次1的bit位再与1进行一次异或,而twos无法记录哪些bit位出现了奇数次的1,只能表征哪些bit位多次出现了1。多次出现了1的bit位并不代表就一定是多次出现元素生成的影响,仅出现一次的元素也可能在已经多次出现1的bit位上叠加影响,进而无法通过哪些bit位是否多次出现1来分辨是单次元素造成的影响还是3次元素造成的影响。
所以这道题的解决方法:1.某个bit位上1出现3次就将ones对应bit位置0;2.某个bit位上1出现2次就将ones对应bit位置1;必须清楚知道某个bit位上1出现的次数,某bit位出现1大于1次不一定仅仅是出现3次元素生成的影响。
上述的第二种方法企图通过在相应bit位置1,使得再次遇到1的时候异或结果变成0。这种思路将bit位置1会影响下一次twos的取值,下一次nums中出现1,仍会使得twos中取1,进而认为之后的每一次都是第二次,都会将相应的bit位置1。那么就是要在ones置1,之后第一次再遇到1的时候twos不计数,使得ones该bit位被抵消置为0。那么就是twos某bit位为1的时候,使得ones中某bit位为1,并且抵消twos该bit位下一次为1的行为。那就是要对twos进行异或操作。
解法一:两次重复即叠加一次异或,抵消第三次异或的影响
class Solution {
public:
int singleNumber(vector<int>& nums) {
int one = 0, two = 0, three = 0;
for (int i = 0; i < nums.size(); ++i)
{
two ^= one & nums[i]; //表征重复出现1的变量,并且连续按位异或,实现周期性
one ^= nums[i]; //将nums不断进行按位异或操作
one = one | two; //重复出现两次1即手动增加一次异或抵消(已发生2次+后面1次)的影响。
}
return one;
}
};
Hello, World!
//其中two = nums[i]&ones(针对每一轮) twos ^=two(连续异或的结果)
nums: 1 00000000001 two: 00000000000 ones: 00000000001 twos: 00000000000 ONES: 00000000001
nums: 4 00000000100 two: 00000000000 ones: 00000000101 twos: 00000000000 ONES: 00000000101
nums: 6 00000000110 two: 00000000100 ones: 00000000011 twos: 00000000100 ONES: 00000000111
//two中体现了倒数第三bit位重复出现1,twos体现是奇数次重复。所以将ones相应位置置为1
nums: 1 00000000001 two: 00000000001 ones: 00000000110 twos: 00000000101 ONES: 00000000111
nums: 4 00000000100 two: 00000000100 ones: 00000000011 twos: 00000000001 ONES: 00000000011
nums: 4 00000000100 two: 00000000000 ones: 00000000111 twos: 00000000001 ONES: 00000000111
nums: 1 00000000001 two: 00000000001 ones: 00000000110 twos: 00000000000 ONES: 00000000110
nums: 5 00000000101 two: 00000000100 ones: 00000000011 twos: 00000000100 ONES: 00000000111
nums: 6 00000000110 two: 00000000110 ones: 00000000001 twos: 00000000010 ONES: 00000000011
nums: 6 00000000110 two: 00000000010 ones: 00000000101 twos: 00000000000 ONES: 00000000101
ONES: 5 00000000101
如果上面连载一起看比较复杂,我们可以针对某一个bit位置来进行分析:
nums该bit位第一次出现1 -->连续异或结果ones = 1
nums该bit位第二次出现1 -->连续异或结果ones = 0 ;上一轮的ones和该轮nums使得twos=1; 进而触发修正动作:ones=1
nums该bit位第三次出现1 -->连续异或结果ones = 0(因为上轮twos=1导致动作ones=1发生,继续异或结果为0);
上轮的ones=1与该轮的nums使得本轮的twos=1,由于twos也在不断的异或,最终twos=0,将不会触发修正动作
nums该bit位第四次出现1 -->连续异或结果ones = 1
nums该bit位第五次出现1 -->连续异或结果ones = 0 ;上一轮的ones和该轮nums使得twos=1; 进而触发修正动作:ones=1
nums该bit位第六次出现1 -->连续异或结果ones = 0(因为上轮twos=1导致动作ones=1发生,继续异或结果为0);
上轮的ones=1与该轮的nums使得本轮的twos=1,由于twos也在不断的异或,最终twos=0,将不会触发修正动作
……以3为周期循环往复,实现了对三取余的操作。
然而该解答方法的Runtime还是不尽如人意,需要改进。
2.3 解法二
更改使用更加简单的思路,直接统计哪些bit位上是出现了3次1,直接将哪些bit位先置为0。该思路简单清晰,不妨试试。问题在于如何基于连续异或的结果记录某一个bit位出现了三次1。ones中出现1,说明原nums中出现了奇数个1;twos中出现1说明在nums中再次遇到了1;如果在twos为1的基础上再次在nums中遇到1,那就是第三次遇到1。然后ones和twos都清零从头来过,每三个一轮会被置零。
梳理一下各个变量的功能:ones在于表征在nums中第一次遇到了1,不断的按位异或;直到在第二次遇到1的时候,由nums的1与前一轮的ones的1通过与运算得到twos=1,而此时ones由于两个1异或运算变成0,twos表征在nums中第二次遇到1。为了保证所得到的1能够延续下去直到完成一次循环,各轮次之间的twos应该是或运算;在第三次遇到1的时候,twos的1与nums的1进行与运算得到threes的1,threes表征在nums中第三次遇到1。为了进行下一轮的循环,此时twos和ones相应的bit位都要置0,即与上three取反之后的结果。以下依旧采用针对某一个bit位进行分析。
nums该bit位第一次出现1 -->连续异或结果ones = 1
nums该bit位第二次出现1 -->上一轮的ones和该轮nums使得twos=1; 连续异或结果ones = 0 ;
nums该bit位第三次出现1 -->上一轮的twos和该轮的nums使得threes=1;twos采用与运算仍延续值为1;连续异或结果ones=1;
//因为threes=1,触发清零操作,对ones和twos对应bit位清零便于后一轮的统计;清零操作是one ^=~three; two^=~three;
nums该bit位第一次出现1 -->连续异或结果ones = 1
nums该bit位第二次出现1 -->上一轮的ones和该轮nums使得twos=1; 连续异或结果ones = 0 ;
nums该bit位第三次出现1 -->上一轮的twos和该轮的nums使得threes=1;twos采用与运算仍延续值为1;连续异或结果ones=1;
//因为threes=1,触发清零操作,对ones和twos对应bit位清零便于后一轮的统计;清零操作是one ^=~three; two^=~three;
……以3为周期循环往复,实现了对三取余的操作。
对应的代码如下:
解法二:某bit位出现三次1,即进行清零操作。
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ones = 0, twos = 0, threes = 0;
for (int i = 0; i < nums.size(); ++i)
{
threes = twos&nums[i]; //three应该位于第一个,否则第二次遇到1会触发threes=1
twos |= ones & nums[i]; //表征重复出现1的变量,并且连续按位异或,实现周期性
ones ^= nums[i]; //将nums不断进行按位异或操作
ones &=~threes;
twos &=~threes;
}
return ones;
}
};
以下是解法二的运行结果,看起来Runtime是提升了
2.4 解法三
最后直接给出网上的另外一种解法,与刚才的解法二是一个思路,只是在确定第三次遇见1的方式上有点差异,我们直接看代码。
解法三:某bit位出现三次1,即进行清零操作。采取另一种确定第三次遇见1的方式
class Solution {
public:
int singleNumber(vector<int>& nums) {
int one = 0, two = 0, three = 0;
for (int i = 0; i < nums.size(); ++i) {
two |= one & nums[i];
one ^= nums[i];
three = one & two; //此处认为one和two同时取到1就是第三次遇见1
one &= ~three;
two &= ~three;
}
return one;
}
};
此处认为one和two同时取到1就是第三次遇见1,返回去查看解法二当中针对某一个bit位6次遇见1的过程分析,即可知道,该方法与解法二是一样的。
3.写在最后
因为负数的特殊的异或操作方式,本来以为负数的异或操作会影响本题的结果。然而发现本题的根本在于统计每一个bit位上的1的数目,然后对3取余。无论是补码还是原码,都是通过这个方式剥离多次出现元素对单次出现元素的影响。所以负数的特殊异或方式并没有影响本题,不需要单独考虑。但是还是把负数异或贴出来为自己扫盲哈哈哈。
补充一下负数的异或操作:负数的异或是将两数的补码按位进行异或,然后作为补码求其源码即为异或结果。关于负数的二进制表示和异或操作,可以参考C++中负数的位操作和负数的异或操作。
以 3 ^ (-2)为例
3 的补码:0000 0011 //正数的补码就是其原码本身
-2的补码:1111 1110
异或: 1111 1101 此为结果的补码,然后要反过来计算原码
由 原码 -> 取反 -> +1 补码 得 补码 -1 -> 取反 -> 原码
补码: 1111 1101
补码减1 : 1111 1100
原码: 1000 0011
即结果为 -3