文章目录

  • 运算性质
    • 异或运算的一些性质
  • 秀秀伸手
    • 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,偶数个1异或在一起结果为0;
  2. 从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]
  3. 偶数个整数在一起,要么总体异或结果为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
```

备注:

  1. 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);
    }
    
  2. -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种。

  1. 与(and)

    • 按位与(&)
    • 如果两个相应的bit都为1,则结果值的该位为1,否则为0
    • eg. 0b0011 & 0b0010 => 0b0010
  2. 或(or)

    • 按位或(|)
    • 如果两个相应的bit有一个为1,则结果值的该位为1,否则为0
    • eg. 0b0011 | 0b0010 => 0b0011
  3. 非(not)

    • 按位取反(~)
    • bit位1变0,0变1
    • eg. ~0b0011 => 0b1100
  4. 异或(xor)

    • 按位异或(^)
    • 如果两个相应的bit位相同,则结果值的该位为0,否则为1
    • eg. 0b0011 ^ 0b0010 => 0b0001
  5. 左移

    • 按位左移(<<)
    • 用来将一个数的各二进制位全部左移指定的位数,右补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位
    
  6. 右移

    • 右移会导致低位被丢弃。

    • 无符号右移(>>>)

    • 用来将一个数的各二进制位全部右移指定的位数,左补0(左边最高位是符号位,为0表示正数)。

    • eg. 0b0011>>>1 => 0b0001

    • 带符号右移(>>)

    • 用来将一个数的各二进制位全部右移指定的位数,左补1(如果原数最左位是1,即为负数),或者补0(如果原数最左位是0)。

    • eg1. 0b1100>>1 => 0b1110

    • eg2. 0b0100>>2 => 0b0010

怎么样,觉得有收获的话,在下方给作者一个赞吧!


感谢阅读。

位运算常用技巧分析汇总(算法进阶)相关推荐

  1. 位运算常用技巧以及练习

    几个有趣的操作 利用或操作|和空格将英文字符转换成小写 // 可以变成小写i := 'a' | ' 'fmt.Printf("%c\n", i)j := 'A' | ' 'fmt. ...

  2. 从labuladong东哥那里看到的位运算小技巧

    从labuladong东哥那里看到的位运算小技巧 1. 利用或操作 `|` 和`空格`将英文字符转换为小写 2. 利用与操作 `&` 和`下划线`将英文字符转换为大写 3. 利用异或操作 `^ ...

  3. C51位运算应用技巧

    位运算应用口诀: 清零取位要用与,某位置一可用或,若要取反和交换,轻轻松松用异或! 移位运算要点 1 它们都是双目运算符,两个运算分量都是整形,结果也是整形. 2 "<<&quo ...

  4. 《算法笔记》第4章常用技巧及排序算法

    文章目录 二. 常用技巧 1. 散列 2. 递归 2.1 全排列问题 2.2 n皇后问题 2.3 回溯法优化n皇后问题 3. 贪心 3.1 简单贪心 3.2 区间贪心 4. 二分 4.1 二分查找 4 ...

  5. c语言或者cpp中位运算的技巧

    简述 在知乎上看到一个题目,解答很有意思,用的是位运算. 这让我觉得位运算有更多的算法可能,但是却还没被我用过. 这种东西都是第一次看,觉得挺牛的,第二次,第三次看的时候就觉得没什么了.So,大佬们轻 ...

  6. java求绝对值absultevalue,位运算常见技巧

    在新浪微博上看到一篇文章写位运算的写的很深入,文章链接见末尾,特此mark. 0.位运算的种类 符号 名称 运算规则 & 与 两个位都为1时,结果才为1,否则都为0 l 或 两个位都是0时,结 ...

  7. 学会这道题,解决位运算,布莱恩·克尼根算法!

    布莱恩·克尼根算法 题目描述1 编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 '1' 的个数(也被称为汉明重量). 提示: 请注意,在某些语言(如 Java) ...

  8. 2020 China Collegiate Programming Contest Changchun F - Strange Memory(dsu on tree + 位运算小技巧)

    题目连接: https://codeforces.com/gym/102832/problem/F 首先写这个题的时候要注意内存的问题 不要瞎几把define int long long 题解: 考虑 ...

  9. 算法笔记(一)位运算、二分、基本递归、排序、基本数据结构

    文章目录 位运算 原码.补码与反码 左移右移`<<` & `>>` 无符号右移 异或运算`^` 位运算常用技巧 取相反数 反转0-1 判断负数与非负数 数组交换两元素位 ...

最新文章

  1. python进阶(十七)正则json(上)
  2. three.js插件实现立体动感视频播放效果
  3. php 后端 轻量 框架,GitHub - 22cloud/mixphp: 轻量 PHP 框架,基于 Swoole 的常驻内存型 PHP 高性能框架 (开发文档完善)...
  4. Qt文档阅读笔记-DTLS server解析
  5. Android和Linux kernel发展史
  6. php 5范例代码查询辞典 pdf,PHP 5范例代码查询辞典
  7. 黑马博客——详细步骤(一)路由跳转和抽取公共部分代码
  8. linux 格式化硬盘_linux系统装进移动硬盘
  9. Hibernate - Query简易
  10. “互联网+”创新创业计划书(二)
  11. OpenWRT安装Home Assistant
  12. matlab simulink单相桥式逆变电路
  13. unable to translate bytes at index from specified code page to unicode
  14. Strut2简单使用
  15. 没钱人做什么投资?1天1元也能成百万富翁
  16. ios:应用发布App Store流程
  17. Phobos病毒家族最新变种.faust后缀勒索病毒活跃传播
  18. 以完整解决方案引领智慧转型,联想在深发布ThinkSystemThinkAgile双品牌新品
  19. rsync 的 “file has vanished” 问题
  20. MacOS装载APFS移动硬盘出现49180错误

热门文章

  1. 2022年深圳市科技型中小微企业贷款贴息资助标准及申报条件,补贴100万
  2. 【工大SCIR】AAAI20 基于Goal(话题)的开放域多轮对话规划
  3. 01.朴素贝叶斯介绍
  4. 公关战之下,分裂的今日头条
  5. 我是一只可可爱爱的小粽子
  6. 王道操作系统网课笔记合集
  7. 初中数学分几个模块_初中数学分成三大模块
  8. LED升压大电流恒流芯片H6911峰值电流检测 调光辉度65536驱动IC方案
  9. 《程序员》11期最新上市:互联网架构集结号
  10. 提供linux下的新世纪五笔的码表和字根口诀,用于ibus。