位运算常用技巧分析汇总(算法进阶)
文章目录
- 运算性质
- 异或运算的一些性质
- 秀秀伸手
- 1、只用位运算来完成两个整数相加
- 2、不用临时变量,交换a、b两个数的值
- 3、判断一个数是奇数还是偶数
- 3、快速计算2*n、2*n+1和n/2
- 4、`N&(N-1)`是啥?`N&(~N+1)` 是什么?`N&(-N)`是啥?
- 5、我们知道`n>>3`是计算n/8的值,那n%8可以用位运算代替么?可以!对于16,32,...,512等`2^x`的数同理
- 6、`n>0 && (n& (n-1)) == 0` 等价于n为2的某次方。(其中n为正整数)
- 7、遍历二进制形式的子集
- 8、掩码运算
- 几道小题目
- 1、判断一个整数的二进制表示中,为1的位的个数。
- 2、如果给你一堆int整数,告诉你其中仅有一个数出现了奇数次,其他数都出现了偶数次?问怎么找出这个数?
- 3、位运算压轴出场
- 总结
运算性质
以int类型为例,位运算只需单独考虑对应的每一位即可。
异或运算的一些性质
- 奇数个1异或在一起结果为1,偶数个1异或在一起结果为0;
- 从0开始,每连续4个整数一组,每一组异或结果为0,即
4k^(4k+1)^(4k+2)^(4k+3) = 0
衍生性质:
xor[0..n]={n,当nmod4==01,当nmod4==1n+1,当nmod4==20,当nmod4==3xor[0..n] = \left\{\begin{matrix} & n ,当 n\bmod4==0 & \\ & 1 ,当 n\bmod4==1 & \\ & n+1 ,当 n\bmod4==2 & \\ & 0 ,当 n\bmod4==3 & \end{matrix}\right. xor[0..n]=⎩⎨⎧n,当nmod4==01,当nmod4==1n+1,当nmod4==20,当nmod4==3
xor[i..j]=xor[0..j]⊕xor[0..i−1]xor[i..j] = xor[0..j] \oplus xor[0..i-1] xor[i..j]=xor[0..j]⊕xor[0..i−1] - 偶数个整数在一起,要么总体异或结果为0,要么其中必存在某个数xxx,使得除去xxx之外剩余的数异或在一起结果不为0。(反证法,力扣题目:810. 黑板异或游戏)
秀秀伸手
1、只用位运算来完成两个整数相加
力扣题目:371. 两整数之和
public static int add(int num1, int num2) {int sum, carry;do {//两个数异或,二进制的相加,但不进位//比如:01010 ^ 00101 = 01111//因为不涉及到进位,01010 + 00101结果就是01111sum = num1 ^ num2;//计算进位的部分,两个bit都是1才会产生进位,用按位与//是向高位进位,因此左移1位carry = (num1 & num2) << 1;num1 = sum;num2 = carry;} while (carry != 0); //只要计算中产生了进位,还得继续把进位部分累加进来return sum;
}
/**
* 稍微简化一下
*/
public int getSum(int a, int b) {//按位异或 其实就是不进位的加法int ans = a;while(b != 0) {int carry = (ans&b)<<1;//计算出该进位的部分,暂存ans = ans^b;//再通过异或,计算不进位的相加结果b = carry;}return ans;
}
/**
* 更简洁的递归写法
* 异或可以看成是不进位的相加*/
public int add(int a, int b) {if(a==0)return b;if(b==0)return a;// 前一项是不进位的相加结果// 后一项是进位,进位最后始终会变成0return add(a^b, (a&b)<<1);
}
关于异或(^),有一种可能更直观的理解:两个数的二进制位,按最低位对齐(高位补0)之后,分别进行按位相加(但忽略进位)的结果。
2、不用临时变量,交换a、b两个数的值
关于异或运算有2个性质:
a ^ a = 0
0 ^ a = a
```java
a = a ^ b;
b = a ^ b; // (a^b)^b = a ^ (b^b) = a ^ 0 = a
a = a ^ b; // (a^b)^a = a^a ^ b = 0^b = b;
```
(不过,个人倒是觉得没必要用这种方式来交换两个数)
《linux多线程服务端编程》501页分析了,用异或运算交换变量,是错误的行为。并且不能加快运算,也不能节省内存。
参考:用异或来交换两个变量是错误的https://blog.csdn.net/solstice/article/details/5166912
3、判断一个数是奇数还是偶数
//判断最低一个bit是1(奇数)还是0(偶数)
return (n&1)==1;//true: 奇数;false: 偶数
3、快速计算2n、2n+1和n/2
2*n 等价于 n<<1
2*n+1等价于 n<<1|1
n/2 等价于 n>>1
比如,在二分查找中一个惯用的技巧,即求[left, right]的中间位置:
int mid = (left+right)>>1;
不过,为了防止相加后溢出,超出int的表示范围,最好这样写:
int mid = left + ((right - left)>>1); // 这里必须加括号!
4、N&(N-1)
是啥?N&(~N+1)
是什么?N&(-N)
是啥?
这3者的关键是要先理解N&(N-1)
。N-1
是将二级制的N,其最右边部分的10..0(其中0可以有0到多个)
,打散为01..1
(其中1的个数和前边0的个数相同)。那么 N&(N-1)
的结果就是将N的二进制形式中,最低位的1抹掉后的数。
`N&(~N+1)`和`N&(-N)`,这两者其实是等价的。他们的结果是,将`二级制的N,其最右边一个为1的位保留成1,其他位全变成0`的数。当然这是N不为0的情况。
如果N=0呢?
```java
N&(~N+1) = 0 & (-1+1) = 0 //事实上,0与任何数按位与结果是0
```
备注:
java代码验证: -N == (~N+1)
int val = Integer.MAX_VALUE; assert -val == (~val+1); val = Integer.MIN_VALUE; assert -val != (~val+1); for(int i=-1000;i<1000;i++) {assert -i != (~i+1); }
-N == (~N+1)其实是有原因的,java在表示负数的时候,用的是补码,补码就是由相应的正数值按位取反(得到反码),再加1得出。
因此,如果N是一个正数,从二进制形式看,~N+1(即N的补码)就是用来表示-N这个负数的。
//以byte类型为例,来体会一下补码 01111111 //127 00000000 //0 11111111 //-1 由1的二进制0000_0001按位取反1111_1110,再加1得出 10000001 //-127 127的二级制0111_1111按位取反1000_0000,再加1得出 10000000 //-128 128的二级制1000_0000按位取反0111_1111,再加1得出 byte的表示范围为:[-128, 127]Integer.MIN_VALUE == 0x80000000 (即1<<31) Integer.MAX_VALUE == 0x7fffffff
5、我们知道n>>3
是计算n/8的值,那n%8可以用位运算代替么?可以!对于16,32,…,512等2^x
的数同理
n%8 等于 n&7 //即取二进制n的低3位
6、n>0 && (n& (n-1)) == 0
等价于n为2的某次方。(其中n为正整数)
如何理解:
n为2的某次方,等价于n的二进制形式中有且仅有1个1,其余bit为0,那么n& (n-1)) 必然为0。
反过来,如果n不为2的某次方,n的二进制形式中至少会有2个1,那么n-1是将n的二进制中最后1个1打散,即xx..x100..0
变成xx..x011..1
。n& (n-1))必不为0(只是将最低位的1变成了0,至少还留有高位的1)。
小结一下:
** n = n & (n-1)的结果是抹掉n(二进制形式中)最低位的1**
** n = n &(~n+1)的结果是,只保留n二进制中最低位的1,其他位全变为0**
备注:这是力扣第231题:231. 二的幂
7、遍历二进制形式的子集
遍历一个整数的二进制形式中,所有为1的bit组成的集合的子集。
第一种,通过for循环:
int mask = 0b10101;
// 第一种:因为有i>0条件,遍历结果中至少含有1个bit的1
for (int i = mask; i > 0; i = (i - 1) & mask) {System.out.println(Integer.toBinaryString(i));
}
/**输出结果为:* 101010* 101000* 100010* 100000* 1010* 1000* 10*/
第二种,通过do{}while()
循环:
int mask = 0b10101;
int subset = mask;
// 遍历mask的所有子集,包括不含1(即全0)的情况
do {System.out.println(Integer.toBinaryString(subset));subset = (subset - 1) & mask;
} while (subset != mask);
/**输出结果为:* 101010* 101000* 100010* 100000* 1010* 1000* 10* 0*/
解释:第二种遍历,当subset
减为0之后,subset-1
为-1
,而-1
的二进制位全为1,-1 & mask
之后就等于mask
了,于是while退出。
8、掩码运算
笔者最早接触掩码这个词,应该是IP地址的子网掩码。这里说的掩码也是同一个意思。
一句话来描述掩码的含义和用途:一个操作数和提前设计好的某个掩码值进行按位与运算,能够得到需要的结果。
补充一句:将内网IP地址和子网掩码进行按位与运算能够得出网络标识(网络号)。
//举一个简单的例子: 我要判断一个byte数值的某个bit是0还是1
//先定义好各个bit的掩码
public static final byte[] MASK = {(byte)(1<<7),1<<6,1<<5,1<<4,1<<3,1<<2,1<<1,1<<0,
};public static boolean isSet(byte val, int index) {int mask = MASK[index&7];//index的有效范围是0~7return (val&mask) == mask;
}//比如,24这个数
byte val = 0b0001_1000;//24
System.out.println(isSet(val,0));//false,最高位,第0位不是1
System.out.println(isSet(val,3));//true,第3位是1(下标从0开始数的)
System.out.println(isSet(val,4));//true,第4位是1(下标从0开始数的)
几道小题目
1、判断一个整数的二进制表示中,为1的位的个数。
ps: 这是力扣第191题:191. 位1的个数
//第一种写法,每次判断最低一位是否为1,为1计数加1,
//然后n无符号右移1位,再次进行判断;直到n变为0。
public static int bit1Counts(int n) {int rc = 0;while(n!=0) {if((n&1) == 1) {rc++;}n>>>=1;}return rc;
}//第二种写法,循环次数比前一种实现要少(前一种是所有位中的0和1都判断了,这种只判断为1的位)
public static int bit1Counts2(int n) {int count = 0;int right1;while(n!=0) {//取到最右边的1,其他位保留0right1 = n & (~n + 1);count++;//再把n最右边的1变成0,循环继续n ^= right1;}return count;
}//第三种写法,简单直接,最优解
public static int bit1Counts3(int n) {int count = 0;while (n != 0) {//参考上文第5点n&(n-1)的理解://n-1是把n的最右(低)位的1“打散”开来,//因此,n&(n-1)会将n的最低位的1抵消掉n &= (n - 1);count++;}return count;
}// 第四种写法:每4位统计一次数量
public int bit1Counts4(int i) {//bit是0到15转化为二进制时,其中1的个数int bit[] = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4};int count = 0;while (i != 0) {//通过每4位计算一次,求出包含1的个数count += bit[i & 0xf];// i无符号右移4位i >>>= 4;}return count;
}
2、如果给你一堆int整数,告诉你其中仅有一个数出现了奇数次,其他数都出现了偶数次?问怎么找出这个数?
思路:将所有的数异或在一起,得到的结果即为要找的数。
还是异或的性质,相同2个数异或结果为0,0和一个数异或结果是这个数本身。
3、位运算压轴出场
解决n皇后问题:在n*n的棋盘上,放置n个“皇后”,要求棋盘上每一条横线、竖线、斜线(包括向左下方向的斜线,向右下方向的斜线)上都至多只有一个皇后,问有多少种放法。
求解这道题的算法是**O(n^n)
的时间复杂度**。没错,就是这么高的复杂度!!所以,一般求解这个问题的时候,n不会很大。准确地说,n会很小。请脑补一下,如果n达到32,求32的32次方将会是一个多大的数。
关于时间复杂度,可以这么来理解:
- 按照出场顺序第i号皇后放在棋盘的第i行,但是它有n列可以选择。
- 每一个皇后都是这样有n个选择,总共可能性的上限就是n个n相乘,即n^n。
这里请位运算压轴出场来解决这问题,这种解法能极大地优化算法执行的常数时间,绝对是最优解。
整个思路是从上往下,一行一行地尝试放置皇后。请看以下代码和注释。
class Solution {//求n皇后问题有多少种放法public int totalNQueens(int n) {int total = 0;//假设我们只求解n<=32的情况if(n>=1 && n<=32) {//limit的二进制表示中,低n位为1,高位为0(可以对应到棋盘中的一行)int limit = n == 32 ? -1 : ((1 << n) - 1);total = process1(limit, 0, 0, 0);}return total;}// limit: 问题规模为n,limit低n位(bit)为1,其他位为0// leftMask: 记录左斜线方向上是否有限制,并通过左移位将这种限制往更下一行进行传递// rightMask:记录右斜线方向上是否有限制,并通过右移位将这种限制往更下一行进行传递// colMask: 记录列方向上被占用的情况(二级制为1的位已被占用)public int process1(int limit, int leftMask, int colMask, int rightMask) {if(colMask==limit) {//所有列都已被占,说明n个皇后放置结束,得到一种方案return 1;}int res = 0;int pos = limit & ~(leftMask|colMask|rightMask);//得到哪些位置可以放皇后while(pos!=0) {//在pos不为0的情况下,找到pos中所有为1的位,进行放置皇后的尝试//找到pos中最有低位的1,记为mostRightOneint mostRightOne = pos & (~pos+1);//在mostRightOne的位置放皇后pos ^= mostRightOne;//等价于pos -= mostRightOne//继续尝试下一行放置,其leftMask变为(leftMask|mostRightOne)<<1//左移是告诉下一行放置皇后的时候不能占用这个位置//否则跟本行放置的皇后在左斜线上有冲突//rightMask同理res += process1(limit, (leftMask|mostRightOne)<<1,colMask|mostRightOne, (rightMask|mostRightOne)>>>1);}return res;}
}
总结
位运算是计算机里面最快的操作,没有之一。
就因为快,位运算受到很多算法爱好者的热捧。
位运算为什么快呢?
- 位运算直接操作二进制的1和0,对应到逻辑电路里面的高电平和低电平,高电平代表1,低电平代表0。计算机芯片在硬件层面(集成电路)针对高低电平的转换提供了各种各样的逻辑电路门,有与门(and)、或门(or)、非门(not)、**异或门(xor)**等等。
通常在编程的时候用到的位运算有哪些呢?以编程语言java
为例,有6种。
与(and)
- 按位与(&)
- 如果两个相应的bit都为1,则结果值的该位为1,否则为0
- eg.
0b0011 & 0b0010 => 0b0010
或(or)
- 按位或(|)
- 如果两个相应的bit有一个为1,则结果值的该位为1,否则为0
- eg.
0b0011 | 0b0010 => 0b0011
非(not)
- 按位取反(~)
- bit位1变0,0变1
- eg.
~0b0011 => 0b1100
异或(xor)
- 按位异或(^)
- 如果两个相应的bit位相同,则结果值的该位为0,否则为1
- eg.
0b0011 ^ 0b0010 => 0b0001
左移
- 按位左移(<<)
- 用来将一个数的各二进制位全部左移指定的位数,右补0。左移会导致高位被丢弃。
- eg.
0b0011<<2 => 0b1100
// 补充一点,java中左移超过31位的话,实际只移动对32取模的位数 System.out.println(1<<31);// -2147483648 System.out.println(1<<32);//1,左移32%32=0位 System.out.println(1<<33);//2,左移33%32=1位 System.out.println(1<<34);//4,左移34%32=2位
右移
右移会导致低位被丢弃。
无符号右移(>>>)
用来将一个数的各二进制位全部右移指定的位数,左补0(左边最高位是符号位,为0表示正数)。
eg.
0b0011>>>1 => 0b0001
带符号右移(>>)
用来将一个数的各二进制位全部右移指定的位数,左补1(如果原数最左位是1,即为负数),或者补0(如果原数最左位是0)。
eg1.
0b1100>>1 => 0b1110
eg2.
0b0100>>2 => 0b0010
怎么样,觉得有收获的话,在下方给作者一个赞吧!
感谢阅读。
位运算常用技巧分析汇总(算法进阶)相关推荐
- 位运算常用技巧以及练习
几个有趣的操作 利用或操作|和空格将英文字符转换成小写 // 可以变成小写i := 'a' | ' 'fmt.Printf("%c\n", i)j := 'A' | ' 'fmt. ...
- 从labuladong东哥那里看到的位运算小技巧
从labuladong东哥那里看到的位运算小技巧 1. 利用或操作 `|` 和`空格`将英文字符转换为小写 2. 利用与操作 `&` 和`下划线`将英文字符转换为大写 3. 利用异或操作 `^ ...
- C51位运算应用技巧
位运算应用口诀: 清零取位要用与,某位置一可用或,若要取反和交换,轻轻松松用异或! 移位运算要点 1 它们都是双目运算符,两个运算分量都是整形,结果也是整形. 2 "<<&quo ...
- 《算法笔记》第4章常用技巧及排序算法
文章目录 二. 常用技巧 1. 散列 2. 递归 2.1 全排列问题 2.2 n皇后问题 2.3 回溯法优化n皇后问题 3. 贪心 3.1 简单贪心 3.2 区间贪心 4. 二分 4.1 二分查找 4 ...
- c语言或者cpp中位运算的技巧
简述 在知乎上看到一个题目,解答很有意思,用的是位运算. 这让我觉得位运算有更多的算法可能,但是却还没被我用过. 这种东西都是第一次看,觉得挺牛的,第二次,第三次看的时候就觉得没什么了.So,大佬们轻 ...
- java求绝对值absultevalue,位运算常见技巧
在新浪微博上看到一篇文章写位运算的写的很深入,文章链接见末尾,特此mark. 0.位运算的种类 符号 名称 运算规则 & 与 两个位都为1时,结果才为1,否则都为0 l 或 两个位都是0时,结 ...
- 学会这道题,解决位运算,布莱恩·克尼根算法!
布莱恩·克尼根算法 题目描述1 编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 '1' 的个数(也被称为汉明重量). 提示: 请注意,在某些语言(如 Java) ...
- 2020 China Collegiate Programming Contest Changchun F - Strange Memory(dsu on tree + 位运算小技巧)
题目连接: https://codeforces.com/gym/102832/problem/F 首先写这个题的时候要注意内存的问题 不要瞎几把define int long long 题解: 考虑 ...
- 算法笔记(一)位运算、二分、基本递归、排序、基本数据结构
文章目录 位运算 原码.补码与反码 左移右移`<<` & `>>` 无符号右移 异或运算`^` 位运算常用技巧 取相反数 反转0-1 判断负数与非负数 数组交换两元素位 ...
最新文章
- python进阶(十七)正则json(上)
- three.js插件实现立体动感视频播放效果
- php 后端 轻量 框架,GitHub - 22cloud/mixphp: 轻量 PHP 框架,基于 Swoole 的常驻内存型 PHP 高性能框架 (开发文档完善)...
- Qt文档阅读笔记-DTLS server解析
- Android和Linux kernel发展史
- php 5范例代码查询辞典 pdf,PHP 5范例代码查询辞典
- 黑马博客——详细步骤(一)路由跳转和抽取公共部分代码
- linux 格式化硬盘_linux系统装进移动硬盘
- Hibernate - Query简易
- “互联网+”创新创业计划书(二)
- OpenWRT安装Home Assistant
- matlab simulink单相桥式逆变电路
- unable to translate bytes at index from specified code page to unicode
- Strut2简单使用
- 没钱人做什么投资?1天1元也能成百万富翁
- ios:应用发布App Store流程
- Phobos病毒家族最新变种.faust后缀勒索病毒活跃传播
- 以完整解决方案引领智慧转型,联想在深发布ThinkSystemThinkAgile双品牌新品
- rsync 的 “file has vanished” 问题
- MacOS装载APFS移动硬盘出现49180错误