问题:
Given an array of integers, every element appears three times except for one, which appears exactly once. Find that single one.
Note:
Your algorithm should have a linear runtime complexity. Could you implement it without using extra memory?
解决:
① 使用map解决。时间复杂度O(n),空间复杂度O(n)。
class Solution { //13ms
public int singleNumber(int[] nums) {
int res = -1;
Map<Integer,Integer> map = new HashMap<>();
for (int n : nums) {
map.put(n,map.getOrDefault(n,0) + 1);
}
for (Map.Entry<Integer,Integer> entry : map.entrySet()) {
if(entry.getValue() != 3){
res = entry.getKey();
break;
}
}
return res;
}
}
② 对数组排序后,使用一个变量记录其出现的次数。时间复杂度O(nlogn),空间复杂度O(1)。
class Solution { //5ms
public int singleNumber(int[] nums) {
int res = -1;
int count = 1;
Arrays.sort(nums);
if(nums.length == 1) return nums[0];
for (int i = 1;i < nums.length ;i ++ ) {
if (nums[i] == nums[i - 1]) {
count ++;
}else{
if(count < 3){
res = nums[i - 1];
break;
}
count = 1;
}
}
if(count < 3 && res == -1) res = nums[nums.length - 1];
return res;
}
}
③ 位操作,我们可以建立一个32位的数字,来统计每一位上1出现的个数,我们知道如果某一位上为1的话,那么如果该整数出现了三次,对3取余为0,我们把每个数的对应位都加起来对3取余,最终剩下来的那个数就是单独的数字。
class Solution { //3ms
public int singleNumber(int[] nums) {
int res = 0;
for (int i = 0;i < 32 ;i ++ ) {
int sum = 0;
for (int j = 0;j < nums.length ;j ++ ) {
sum += (nums[j] >> i) & 1;
}
res |= (sum % 3) << i;
}
return res;
}
}
④ 进位,掩码清零法。用3个整数来表示INT的各位的出现次数情况,one表示出现了1次,two表示出现了2次。当出现3次的时候该位清零。最后答案就是one的值。时间复杂度O(32n),空间复杂度O(1).【注意】位操作是对32位整型数!!!
class Solution { //1ms
public int singleNumber(int[] nums) {
int one = 0;
int two = 0;
int three = 0;
for (int i = 0;i < nums.length ;i ++ ) {
two |= one & nums[i];//two 积累值
one ^= nums[i];//one不断求反
three = one & two;//第三次的时候one和two都保留了该位的值
one &= ~ three;//清零出现三次的该位的值
two &= ~ three;
}
return one;
}
}
⑤
我们把数组中数字的每一位累加起来对3取余,剩下的结果就是那个单独数组该位上的数字,由于我们累加的过程都要对3取余,那么每一位上累加的过程就是0->1->2->0,换成二进制的表示为00->01->10->00,那么我们可以写出对应关系:
00 (+) 1 = 01
01 (+) 1 = 10
10 (+) 1 = 00 ( mod 3)
那么我们用ab来表示开始的状态,对于加1操作后,得到的新状态的ab的算法如下:
b = b xor r & ~a;
a = a xor r & ~b;
我们这里的a,b就是上面的三种状态00,01,10的十位和个位,刚开始的时候,a和b都是0,当此时遇到数字1的时候,b更新为1,a更新为0,就是01的状态;再次遇到1的时候,b更新为0,a更新为1,就是10的状态;再次遇到1的时候,b更新为0,a更新为0,就是00的状态,相当于重置了;最后的结果保存在b中。
class Solution { //1ms
public int singleNumber(int[] nums) {
int a = 0;
int b = 0;
for (int i = 0;i < nums.length ;i ++ ) {
b = (b ^ nums[i]) & ~a;
a = (a ^ nums[i]) & ~b;
}
return b;
}
}
【解析】 http://www.cnblogs.com/yanliang12138/p/4536189.html
一 逻辑运算符
1. &位与运算符
1) 运算规则
位与运算的实质是将参与运算的两个数据,按对应的二进制数逐位进行逻辑与运算。例如:int型常量4和7进行位与运算的运算过程如下:
4=0000 0000 0000 0100 &7 =0000 0000 0000 0111= 0000 0000 0000 0100
对于负数,按其补码进行运算。例如:例如:int型常量-4和7进行位与运算的运算过程如下: -4=1111 1111 1111 1100 &7 =0000 0000 0000 0111= 0000 0000 0000 0100
2) 典型应用
(1) 清零
清零:快速对某一段数据单元的数据清零,即将其全部的二进制位为0。例如整型数a=321对其全部数据清零的操作为a=a&0x0。
321=0000 0001 0100 0001 &0=0000 0000 0000 0000 = 0000 0000 0000 0000
(2) 获取一个数据的指定位
获取一个数据的指定位。例如获得整型数a的低八位数据的操作为a=a&0xFF。321=
0000 0001 0100 0001 & 0xFF =0000 0000 1111 11111
= 0000 0000 0100 0001
获得整型数a=的高八位数据的操作为a=a&0xFF00。a&0XFF00==
321=0000 0001 0100 0001 & 0XFF00=1111 1111 0000 0000
= 0000 0001 0000 0000
(3)保留数据区的特定位
保留数据区的特定位。例如获得整型数 a 的第7-8位(从0开始)位的数据操作为: 110000000
321=0000 0001 0100 0001 & 384=0000 0001 1000 0000
=0000 0001 0000 0000
2. | 位或运算
1) 运算规则
位或运算的实质是将参与运算的两个数据,按对应的二进制数逐位进行逻辑或运算。例如:int型常量5和7进行位或运算的表达式为5|7,结果如下:
5= 0000 0000 0000 0101 | 7= 0000 0000 0000 0111=0000 0000 0000 0111
2) 主要用途
(1) 设定一个数据的指定位。例如整型数a=321,将其低八位数据置为1的操作为a=a|0XFF。321= 0000 0001 0100 0001 | 0000 0000 1111 1111=0000 0000 1111 1111
逻辑运算符||与位或运算符|的区别
条件“或”运算符 (||) 执行 bool 操作数的逻辑“或”运算,但仅在必要时才计算第二个操作数。 x || y , x | y 不同的是,如果 x 为 true,则不计算 y(因为不论 y 为何值,“或”操作的结果都为 true)。这被称作为“短路”计算。
3. ^ 位异或
1) 运算规则
位异或运算的实质是将参与运算的两个数据,按对应的二进制数逐位进行逻辑异或运算。只有当对应位的二进制数互斥的时候,对应位的结果才为真。例如:int型常量5和7进行位异或运算的表达式为5^7,结果如下:5=0000 0000 0000 0101^7=0000 0000 0000 0111 = 0000 0000 0000 0010
2) 典型应用
(1)定位翻转
定位翻转:设定一个数据的指定位,将1换为0,0换为1。例如整型数a=321,,将其低八位数据进行翻位的操作为a=a^0XFF;
(2)数值交换
数值交换。例如a=3,b=4。在例11-1中,无须引入第三个变量,利用位运算即可实现数据交换。以下的操作可以实现a,b两个数据的交换:
a=a^b;
b=b^a;
a=a^b;
4.~ 位非
位非运算的实质是将参与运算的两个数据,按对应的二进制数逐位进行逻辑非运算。
二.位移运算符
1.位左移
左移运算的实质是将对应的数据的二进制值逐位左移若干位,并在空出的位置上填0,最高位溢出并舍弃。例如int a,b;
a=5;
b=a<<2;
则b=20,分析过程如下:
(a)10=(5)10=(0000 0000 0000 0101)2
b=a<<2;
b=(0000 0000 0001 0100)2=(20)10
从上例可以看出位运算可以实现二倍乘运算。由于位移操作的运算速度比乘法的运算速度高很多。因此在处理数据的乘法运算的时,采用位移运算可以获得较快的速度。
提示 将所有对2的乘法运算转换为位移运算,可提高程序的运行效率。
2.位右移
位右移运算的实质是将对应的数据的二进制值逐位右移若干位,并舍弃出界的数字。如果当前的数为无符号数,高位补零。例如:
int (a)10=(5)10=(0000 0000 0000 0101)2
b=a>>2;
b=(0000 0000 0000 0001)2=(1)10
如果当前的数据为有符号数,在进行右移的时候,根据符号位决定左边补0还是补1。如果符号位为0,则左边补0;但是如果符号位为1,则根据不同的计算机系统,可能有不同的处理方式。可以看出位右移运算,可以实现对除数为2的整除运算。
提示 将所有对2的整除运算转换为位移运算,可提高程序的运行效率。
3.复合的位运算符
在C语言中还提供复合的位运算符,如下:
&=、!=、>>=、<<=和^=
例如:a&=0x11等价于 a= a&0x11,其他运算符以此类推。
不同类型的整数数据在进行混合类型的位运算时,按右端对齐原则进行处理,按数据长度大的数据进行处理,将数据长度小的数据左端补0或1。例如char a与int b进行位运算的时候,按int 进行处理,char a转化为整型数据,并在左端补0。
补位原则如下:
1) 对于有符号数据:如果a为正整数,则左端补0,如果a 为负数,则左端补1。
2) 对于无符号数据:在左端补0。