位运算 - 位运算的常见用法/题目
定理1:两个相同的数字做异或(^), 等于0
- 0与任何数字异或还是该数字本身
- 例题:136. 只出现一次的数字 -- 除了一个数字出现一次,其他都出现了两次,让我们找到出现一次的数
- 解法:直接把所有的元素一起异或(^),剩下的就是只出现一次的数字。
定理2:把0的第i位变成1:0 ^ (1<<i)
- 0与任何数字异或还是该数字本身
定理3:把该整数最右边一个1变成0:把该整数减去1,再和原来的整数做与(&)运算
把一个整数减去1,再和原来的整数做与运算,会把该整数最右边一个1变成0。
那么一个整数的二进制表示中有多少个1,就可以进行多少次这样的操作。
eg:
原整数为 1110...
1110 & (1110 - 1) = 1110 & 1101 = 1100
1100 & (1100 - 1) = 1100 & 1011 = 1000
1000 & (1000 - 1) = 1000 & 0111 = 0000
> 判断一个是是不是2的整数次方
如果是,那么2进制中有且只有一位是1 !!!
> 计算要改变多少位,M才能变成N
先计算M和N的异或,得出有多少位不同,也就是多少位1
然后再计算1的个数
> 判断奇偶数 / 判断最右一位是否是1
让该数字直接与1做与运算。如果结果是1,那么该数字最右一位一定是1.
eg:
1101 & 0001 = 0001
1100 & 0001 = 0000
> 依次判断每一位是否为1
n为要输入的数字,flag从1开始,依次左移一位去挨个判断n的每一位
1 while(flag){
2 if(n & flag){
3 //do someting...
4 }
5 flag = flag << 1;
6 }
> 快速乘以2,除以2
左移运算符,<<,等同于乘以2
右移运算符,>>,等同于除以2
位运算比乘除法效率高得多!!!
通解通法:除了一个数字出现一次,其他都出现了N次
例题:137. 只出现一次的数字 II -- 除了一个数字出现一次,其他都出现了三次,让我们找到出现一次的数
统计所有数字中每个位中1出现的总数,那么对于某个位,1出现的次数一定是3的倍数或者是1或者0,那么对这个数%3得到的结果就是目的数字在该位上的值。
也就是说:
- 如果所有数字都出现3次, 那么将所有的这些数字的每一位的0或者1累加起来后,也都能被3整除。
- 如果只有一个数字出现一次,其他所有数字都出现3次, 那么将所有的这些数字的每一位的0或者1累加起来后,不能被3整除的那一位肯定是只出现一次的数字造成的。
class Solution { public: int singleNumber(vector<int>& nums) { int ans = 0; for (int i = 0; i < 32; i++) { // 确定每一位:int整数是32bit int sum = 0; for (int num : nums) { sum += (num >> i) & 1; // 统计nums数组中每一个num,的第i位是不是1,然后加在sum上。>>i 右移操作就是为了和1做与运算,才能计算第i位是不是1 } ans ^= (sum % 3) << i; // 对3取余,然后把结果左移i,和ans取异或,就能将结果放到应该在的第i位 } return ans; } };
通解通法:除了两个数字出现一次,其他出现两次 -- 巧用mask进行分类
例题:
- 645. 错误的集合
- 260. 只出现一次的数字 III -- 除了两个数字出现一次,其他都出现了两次,让我们找到这个两个数
解析:只出现一次的数字 III
由于数组中存在着两个数字不重复的情况,我们将所有的数字异或操作起来,最终得到的结果是这两个数字的异或结果:(相同的两个数字相互异或,值为0)) 最后结果一定不为0,因为有两个数字不重复。
演示:
4 ^ 1 ^ 4 ^ 6 => 1 ^ 6
6 对应的二进制: 110
1 对应的二进制: 001
1 ^ 6 二进制: 111
此时我们无法通过 111(二进制),去获得 110 和 001。
那么当我们可以把数组分为两组进行异或,那么就可以知道是哪两个数字不同了。
我们可以想一下如何分组:
重复的数字进行分组,很简单,只需要有一个统一的规则,就可以把相同的数字分到同一组了。例如:奇偶分组。因为重复的数字,数值都是一样的,所以一定会分到同一组!
此时的难点在于,对两个不同数字的分组。
此时我们要找到一个操作,让两个数字进行这个操作后,分为两组。
我们最容易想到的就是 & 1 操作, 当我们对奇偶分组时,容易地想到 & 1,即用于判断最后一位二进制是否为 1。来辨别奇偶。
你看,通过 & 运算来判断一位数字不同即可分为两组,那么我们随便两个不同的数字至少也有一位不同吧!
我们只需要找出那位不同的数字mask,即可完成分组( & mask )操作。
由于两个数异或的结果就是两个数数位不同结果的直观表现,所以我们可以通过异或后的结果去找 mask!
所有的可行 mask 个数,都与异或后1的位数有关。
为了操作方便,我们只去找最低位的mask(只保留最低位的1):
代码
class Solution { public int[] singleNumbers(int[] nums) { if(nums.length == 2){ return nums; } int subResult = 0; for(int i = 0; i < nums.length; i++){ subResult = subResult ^ nums[i]; } int mask = 1; while( (mask & subResult) == 0){ mask = mask << 1; } int a = 0; int b = 0; for(int i = 0; i < nums.length; i++){ if( (nums[i] & mask) == 0){ a = a ^ nums[i]; }else{ b = b ^ nums[i]; } } return new int[]{a,b}; } }
不用加、减、乘、除...
不用加、减、乘、除,做加法
第一步:先做“异或”操作,相当于先忽略了进位,只计算了每个位的计算
17 (10001) + 5 (00101)
10001
00101
-> 10100
第二步:再做“与”操作,只计算进位
10001
00101
-> 00001
第三步:对第二部的进位,左移一位(因为是进位啊,所以肯定要加到左移一位上面)。再重复第一步+第二步,直到进位为0。
第一步结果 10100
第二步进位 00010 (00001左移了一位)
-> 10110
不用乘、除、mod,做除法
假设计算16/3
方法1:用16每减去3一次,当每次的差依然大于0,结果(商)就加1。
缺点:会超时...除数增加的太慢
方法2:倍数倍增法。
除法的商就是可以从被除数中减去除数的次数而不使其为负的次数。下以成倍次数增加除数并从被除数中减去已经增加的除数次数。
1.比如15 / 3,首先从等于当前除数开始,如果除数左移一位(即增长一倍为6)小于被除数,则除数左移一位同时用变量p记录除数增长倍数,
接着除数在左移一位(即增长一倍为12)小于被除数,则除数左移一位同时p记录除数增长倍数(p左移一位,即表示增长4倍),
除数再左移一位(即增长为24)大于15,则停止内循环,
2.然后从被除数中减去当前除数,同时将增长倍数4加到结果集中,
3.再进行外循环(15 - 12 = 3 >= 3),
4.重复上述过程,当前可以继续左移一位,所以最终结果为4 + 1 = 5。
下面的图表,假设计算16/3,除数每次左移1位,倍增:
---------------------------------
被除数 除数 临时结果 最终结果(商)
------------第一次循环-----------
16 3 1
16 6 2
16 12 4
16 24 NA(16<24) 4
------------第二次循环-----------
16-12=4 3 4
4 3 1
4 6 NA(4<6) 4+1=5
4-3=1 3 NA(1<3)
posted on 2019-03-03 22:47 frank_cui 阅读(1052) 评论(0) 编辑 收藏 举报