给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现了一次的元素。
示例 1:
输入: [2,2,3,2]
输出: 3
示例 2:
输入: [0,1,0,1,0,1,99]
输出: 99
答案:
1public int singleNumber(int[] nums) {
2 int a = 0, b = 0;
3 for (int i = 0; i < nums.length; ++i) {
4 b = (b ^ nums[i]) & ~a;
5 a = (a ^ nums[i]) & ~b;
6 }
7 return b;
8}
解析:
有没有能看得懂的,是不是一脸懵逼,如果一眼能看懂的我觉得也算是高手了。如果看不懂我就继续让你懵逼
1public int singleNumber(int[] nums) {
2 int x1 = 0, x2 = 0, mask = 0;
3 for (int i : nums) {
4 x2 ^= x1 & i;
5 x1 ^= i;
6 mask = ~(x1 & x2);
7 x2 &= mask;
8 x1 &= mask;
9 }
10 return x1;
11}
what,还看不懂,那我再给你写一个
1public int singleNumber(int[] nums) {
2 int a = 0;
3 int b = 0;
4 for (int c : nums) {
5 int ta = (~a & b & c) | (a & ~b & ~c);
6 b = (~a & ~b & c) | (~a & b & ~c);
7 a = ta;
8 }
9 return a | b;
10}
看不懂没关系,我们最后再讲,现在我先给大家介绍两个能看懂的,增强一下信心。一种是使用HashMap来存储,key存储数组的值,value存储的是key值出现的次数,最后找出value等于1的即可。原理很简单,代码就不再写了。还一种方式是使用位来存储,我们来先看下代码
1public int singleNumber(int[] nums) {
2 int ans = 0;
3 for (int i = 0; i < 32; i++) {
4 int sum = 0;
5 for (int j = 0; j < nums.length; j++) {
6 if (((nums[j] >> i) & 1) == 1) {
7 sum++;
8 sum %= 3;
9 }
10 }
11 if (sum != 0) {
12 ans |= sum << i;
13 }
14 }
15 return ans;
16}
或者
1public int singleNumber(int[] nums) {
2 int bitLength = 32;
3 int bits[] = new int[bitLength];
4 for (int i = 0; i < bitLength; i++) {
5 for (int j = 0; j < nums.length; j++) {
6 bits[i] += ((nums[j] >> i) & 1);
7 }
8 }
9 int result = 0;
10 for (int j = 0; j < bitLength; j++)
11 if (bits[j] % 3 != 0)
12 result += (1 << j);
13 return result;
14}
这种代码很容易理解,因为int是32位,所以只需要累加每一位上的和,看他是否能被3整除,如果不能整除,说明那个只出现一次的数字在这个位置上是有值的。举个例子,假如数组[5,5,5,6]那么他们的二进制分别是
101
101
101
110
——
所以相加的结果第一位是(从左往右)1+1+1+1=4,不能被3整除,所以结果为1,第二位0+0+0+1也不能被3整除,所以结果也为1,第三位1+1+1+0=3能被3整除,所以结果为0,所以我们要查找的那个数的二进制是110,换成十进制也就是6。代码很容易理解,
下面再看最重要的部分,也就是上面遗留的问题,最上面3种解法的原理是什么。其实他的原理非常简单,就是一个数如果出现3次,通过一些运算我们让他变为0,也就是说他的周期是3,前面我们讲过140,只出现一次的数字,通过异或运算即可。这种想法很容易想到,但怎么让他出现3次的时候让他变为0,就不那么简单了。
比如一个数x第一次出现的时候让他保存在b中,第二次出现的时候让他保存在a中,第3次出现的时候让他等于0,所以对于第一种解法我们很容易写出b=(b^x)&~a; a=(a^x)&~b;
对于第3种解法。我们看到3个相同的数每次执行完之后都会归0,也就是说他的周期是3,只要一个数出现3次就让他归0。那么他是怎么实现的呢,其实也很好理解,因为他的周期是3,那么他就具有3种状态。我们就拿0和1来说,如果用0和1来表示3种状态我们至少需要2位数,我们假如这3种状态是00,01,10。这里的3个数我们把它理解为三种状态,不要把它当成一个数,每次都会在这3种状态中循环,当出现3次的时候就让他回到初始状态。我们看一下
/**
*
* 3种状态
* 存储 输入 结果
* ab c ab
* 00 1 01
* 01 1 10
* 10 1 00
*
* 00 0 00
* 01 0 01
* 10 0 10
*
* 所以可以列出下面公式
* a=a&~b&~c | ~a&b&c
* b= ~a&b&~c|~a&~b&c
*/
我们上面刚才说了,ab只是表示一种状态,那么假如我不选择ab为00,01,10这3种状态,我们选择ab为00,01,11三种状态可不可以,其实完全可以,不管怎么选只要我们保证他的周期是3就行,我们来分析一下如果选00,01,11这3种状态下是什么样的一种情况。
/**
*
* 3种状态
* 存储 输入 结果
* ab c ab
* 00 1 01
* 01 1 11
* 11 1 00
*
* 00 0 00
* 01 0 01
* 11 0 11
*
* 所以可以列出下面公式
* a=a&~b&~c | ~a&b&c|a&b&~c
* b= ~a&b&~c|~a&~b&c|~a&b&c|a&b&~c
*/
1public int singleNumber(int[] nums) {
2 int a = 0, b = 0, i = 0, length = nums.length;
3 while (i < length) {
4 int c = nums[i];
5 int ta = a & ~b & ~c | ~a & b & c | a & b & ~c;
6 b = ~a & b & ~c | ~a & ~b & c | ~a & b & c | a & b & ~c;
7 a = ta;
8 i++;
9 }
10 return b;
11}
写法上确实要复杂一点。确实有一定的难度,希望大家能收藏下来,或者发送给喜欢算法的同学一起来研究下。这里面有很多精髓的地方,并且写法也非常多,我没有一一列出,大家有兴趣的可以研究下。
这里顺便再出几道题,
1,从数组中找出只出现一次的两个数,数组中其他数都出现两次。(就是有2个数只出现一次,其他的数都出现了两次)
1private static int[] findTwo(int[] array) {
2 if (array.length <= 2)
3 return array;
4 int res = 0;
5 for (int i = 0; i < array.length; i++) {
6 //计算只出现一次的两个数^的结果
7 res ^= array[i];
8 }
9 int index = 1;//从右往左找出第一次出现1的位置。
10 while (res != 0) {
11 if ((res & 1) == 0) {
12 res = res >> 1;
13 index = index << 1;
14 } else {
15 break;
16 }
17 }
18 /**
19 *index实际上是从0开始的,但初始化为1,因为如果初始化为0,
20 *无论怎么移还是0,所以相当于往左多移了一步,这里再往右移一步
21 */
22 index = index >>> 1;
23 int[] result = new int[2];
24 for (int i = 0; i < array.length; i++) {
25 if (((array[i] >> index) & 1) == 1)
26 result[0] ^= array[i];
27 else
28 result[1] ^= array[i];
29 }
30 return result;
31}
我只把结果写上,具体逻辑大家自己去分析
2,从数组中找出只出现一次的三个数,数组中其他数都出现两次。(就是有3个数只出现一次,其他的数都出现了两次)
1private static int[] findThree(int[] array) {
2 return findThree(array, 1);
3}
4
5private static int[] findThree(int[] array, int index) {
6 int lastOneBit = index;
7 if (array.length <= 3)
8 return array;
9 int res = 0;
10 for (int i = 0; i < array.length; i++) {
11 //计算只出现一次的三个数^的结果
12 res ^= array[i];
13 }
14 while (res != 0) {
15 if ((res & 1) == 0) {
16 res = res >> 1;
17 lastOneBit = lastOneBit << 1;
18 } else {
19 break;
20 }
21 }
22 /**
23 * 如果第一次调用当res为0的时候,就表示res的二进制位中是没有1的,
24 * 那么这样再根据res上是否为1来判断就没有意义了,所以如果出现这种
25 * 情况他是根据最右边的0或1来划分的,如果不幸又分到一组了,那么继
26 * 续递归调用,同时++index,从最右边第二位开始,以此类推,直到这
27 * 3个数没有分到同一组为止。
28 */
29 /**
30 *这里lastOneBit往右移一位的原因是,应该是从最右边第一位开始判断的,
31 * index应该是0,但在第一次调用的时候传的是1,所以这里要往右移一位,
32 * 第一次调用的时候能不能传0,这个是不行的,因为在执行上面的while循环
33 * 的时候0无论往左移多少位都还是0,所以不能传0。
34 */
35 lastOneBit = lastOneBit >>> 1;
36 int[] result = new int[3];
37 List part1 = new ArrayList();//根据0和1分为两部分
38 List part2 = new ArrayList();
39 for (int i = 0; i < array.length; i++) {
40 if (((array[i] >> lastOneBit) & 1) == 1) {
41 part1.add(array[i]);
42 result[0] ^= array[i];
43 } else {
44 part2.add(array[i]);
45 result[1] ^= array[i];
46 }
47 }
48 /**
49 * 下面分4种情况讨论,当然0和1的顺序可以打乱
50 * 1,1^1^1==1
51 * 2,0^0^0==0
52 * 3,0^1^1==0
53 * 4,0^0^1==1
54 */
55 int[] temp;
56 if ((part1.size() & 1) == 1) { //有奇数个数,是上面的1,4
57 if (result[1] == 0) {//情况1
58 return findThree(listToArray(part1), ++index);
59 } else {//情况4
60 temp = findTwo(listToArray(part2));
61 }
62
63 } else {//有偶数个数,是上面的2,3
64 if (result[0] == 0) {//情况2
65 return findThree(listToArray(part2), ++index);
66 } else {//情况3
67 temp = findTwo(listToArray(part1));
68 }
69
70 }
71 result[1] = temp[0];
72 result[2] = temp[1];
73 return result;
74}
75
76private static int[] listToArray(List<Integer> mList) {
77 int[] array = new int[mList.size()];
78 for (int i = 0; i < mList.size(); i++) {
79 array[i] = mList.get(i);
80 }
81 return array;
82}
上面代码都有注释,我就不再分析。下面在分析一种解法
这三个数中只要我们找到一个,其他两个就迎刃而解了,我们换个思路再来分析一下,假如这三个数是a,b,c,他们异或的结果为x,则x=a^b^c;进一步我们可以推算出a^x=b^c,b^x=a^c,c^x=a^b;我们令A=a^x,B=b^x,C=c^x,因为a,b,c都各不相同,所以A,B,C都不为0,我们令f(x)表示x从右边数遇到的第一个1不变,其他的全部置为0的值,比如f(6)=f(0110)=0010=2,f(11)=f(1011)=0001=1。我们令g=f(A)^f(B)^f(C);我们进一步可以得出g肯定是不为0的,因为A,B,C都不为0,所以f(A),f(B),f(C)也分别都不为0,所以他们二进制位有且仅有一个1,其他都是0,也可以说f(A),f(B),f(C)都是2的n次方。假如f(A)^f(B)等于0,那么0^f(C)肯定不等于0,因为f(C)不等于0,所以g不等于0。假如f(A)^f(B)不等于0,那么他们异或结果的二进制位肯定有两个1,然后再与f(C)异或结果肯定不为0,所以可以肯定的是g不为0。我们还可以进一步得出结论g转化为二进制的时候至少有1个1,至多3个1。我们假设g的二进制位从右边数第m位是1,那么f(A),f(B),f(C)从右边数第m位要么只有一个是1其他两个是0,要么3个都是1。假如3个都是1,那么我们可知x的第m位和a,b,c的第m位相反。假如x的第m位是1,那么a,b,c的第m位都为0,所以a^b^c运算结果的第m位不可能为1,所以不成立。假如x的第m位是0,那么a,b,c的第m位都为1,所以a^b^c运算结果的第m位不可能为0,所以可以得出结论f(A),f(B),f(C)的第m位不可能都是1,只能是一个为1,其他两个为0。所以我们的解决方式就有了,把所有值都异或一遍就得到x,x再与a,b,c分别异或求出A,B,C,然后在求出f(A),f(B),f(C)的值,最后在求出g,后面的就很简单了。
1private static int[] findThree2(int[] array) {
2 int length = array.length;
3 int result[] = new int[3];
4 int temp[] = new int[length + 1];
5 int xorResult = 0;//所有数值异或的结果
6 int i, lastbit1 = 0;
7 for (i = 0; i < length; i++) {
8 //xorResult就是上面分析的x=a^b^c
9 xorResult ^= array[i];
10 temp[i] = array[i];
11 }
12 for (i = 0; i < length; i++) {
13 //lastbit1相当于上面分析的g=f(A)^f(B)^f(C),
14 // xorResult ^ array[i]相当于A,B,C
15 lastbit1 ^= lastBitOf1(xorResult ^ array[i]);
16 }
17 //上面分析的g从右边数第m为是1,其他置为0,因为上面分析的f(A),f(B)
18 // ,f(C)的第m位只有一个是1,其他两个是0,
19 lastbit1 = lastBitOf1(lastbit1);
20
21 for (i = 0; i < length; i++) {
22 // array[i] ^ xorResult相当于A,B,C,在f(A),f(B)f(C)中
23 //第m位为1的分为一组,进行异或运算求得值
24 if (lastBitOf1(array[i] ^ xorResult) == lastbit1)
25 result[0] ^= array[i];//找到第一个值
26 }
27 //第一个值找到后放到数组中让他的个数成为偶数,问题转化为求剩余两个的值
28 temp[length] = result[0];
29 //让xorResult成为剩余两个数异或的结果
30 xorResult ^= result[0];
31 //两个数的时候就可以根据右边第一次出现两个不同的值1和0的时候来划分。
32 lastbit1 = lastBitOf1(xorResult);
33 for (i = 0; i < length + 1; i++) {
34 if ((temp[i] & lastbit1) != 0)
35 result[1] ^= temp[i];//找到第二个值
36 }
37 result[2] = xorResult ^ result[1];//找出第三个
38 return result;
39}
40
41private static int lastBitOf1(int num) {
42 return num & (-num);
43}
3,从数组中找出只出现m次数,数组中其他数都出现n次(使用&运算符解答,并且m不等于n,也不能是n的倍数)
当然这种解法就比较多了,因为m和n都不确定,我们也没有一个万能的公式,我们需要根据具体的m和n来实现不同的算法,我们假如n等于4,有4个重复的,看下怎么写
/**
*
* 4种状态
* 存储 输入 结果
* ab c ab
* 00 1 01
* 01 1 10
* 10 1 11
* 11 1 00
*
* 00 0 00
* 01 0 01
* 10 0 10
* 11 0 11
*
* 所以可以列出下面公式
* a=~a&b&c|a&~b&c|a&~b&~c|a&b&~c
* b= ~a&~b&c|a&~b&c|~a&b&~c|a&b&~c
*/
1public static int findOnce4(int[] array) {
2 int a = 0, b = 0, i = 0, length = array.length;
3 while (i < length) {
4 int c = array[i];
5 int ta = ~a & b & c | a & ~b & c | a & ~b & ~c | a & b & ~c;
6 b = ~a & ~b & c | a & ~b & c | ~a & b & ~c | a & b & ~c;
7 a = ta;
8 i++;
9 }
10 return a | b;
11}
下面的几个只是列出了答案,具体问题大家可以自己分析。如果能研究透,基本上对二进制有关的算法会有一个很大的提升。也希望大家多看多想多练,思考非常重要。当然后面的推送可能要暂停一段时间,真的是希望大家能把这块研究透,如果实在不感兴趣在这段时间可以看看以前的题