位运算实战技巧总结

文章介绍了位运算的基础知识,如与、或、非、异或和移位操作,并展示了它们在计算n+~n=-1、交换数值、取绝对值、判断2的次幂、取余、整数乘除以及判断奇偶性等算法中的应用。此外,还讨论了如何利用位运算进行内存对齐和溢出判断等高级技巧。
摘要由CSDN通过智能技术生成


【注】Stanford的计算机课程中对位运算进行了非常详细的总结,我这里面很多都是参考其中: https://graphics.stanford.edu/~seander/bithacks.html

一、位运算基础

与(&)运算

  • 一个数n与0进行与运算,值为0,n & 0 = 0
  • 一个数n与-1进行与运算,值为n,n & -1 = n
  • 一个数n与自己进行与运算,值为n,n & n = n

或(|)运算

  • 一个数n与0进行或运算,值为n,n | 0 = n
  • 一个数n与-1进行或运算,值为-1,n | -1 = -1

非(~)运算

  • 对二进制的每一位都按位取反。
  • 对于数n,n + (~n) = -1

异或(^)运算

运算的二进位结果,相异为1,相同为0。

  • 一个数n与0异或,值为n,n ^ 0 = n
  • 一个数n与-1异或,值为~nn ^ -1 = ~n
  • 一个数n与自己异或,值为0,n ^ n = 0

左移(<<)和右移(>>)运算

  • 向左进行移位操作,高位丢弃,低位补 0
  • 向右进行移位操作,对无符号数,高位补 0,对于有符号数,高位补符号位

二、算法对n + (~n) = -1应用

设整数n类型为int_8,值为3,则3 + (~3) = 0000 0011 + 1111 1100 = 1111 1111 = -1,所以引出非运算的基础公式n + (~n) = -1,也可以将n ^ -1 = ~n带入。

2.1 位运算实现n+1与n-1

n + (~n) = -1进行等式变换可得:

int n;
~n = -(n + 1);
n + 1 = -~n;
n - 1 = ~-n;	// 假设n = -n,可推出此等式

2.2 取相反数

一个数的相反数等于其按位取反后再加1,对上等式变换推出:

int n;
-n = ~n + 1;

2.3 取绝对值

这一块内容也用到了n ^ 0 = nn ^ -1 = ~n

  • 若n为非负数,则n >> 31 = 0,所以abs = n ^ 0 - 0 = n
  • 若n为负数,则n >> 31 = -1,所以abs = n ^ (-1) + 1 = ~n + 1 = -1 - n + 1 = -n
int abs, n;
abs = (n ^ (n >> 31)) - (n >> 31)

三、算法对n ^ n = 0和n ^ 0 = n应用

3.1 交换两个数的值

不需要第三个临时变量,交换两个数的值

 int a, b;
 a ^= b;	// a = a ^ b;
 b ^= a;	// b = b ^ a = b ^ a ^ b = (b ^ b) ^ a = 0 ^ a = a
 a ^= b;	// a = a ^ b = a ^ a ^ b = 0 ^ b = b

3.2 代替特定的条件赋值

如果x = a,则a ^ b ^ x = 0 ^ b;如果x = b,则a ^ b ^ x = 0 ^ a

所以下列代码可等价于:x = a ^ b ^ x

int a, b, x;

if(x == a) 
    x = b;
else if(x == b)
    x = a;
    
// 上面代码等价于
x = a ^ b ^ x

四、算法对 x&(x-1)应用

x&(x-1)可以消除数字x二进制表示的最后一个1,如:

int x = 0xf6;
printf("%x\n", x);		//0b11110110 
printf("%x\n", x&(x-1));	//0b11110100

4.1 判断一个正数是不是2的次幂

如果一个正数是2的次幂,则这个数的二进制表示中只含有一个1。

int x;
if(x&(x-1)){
	//x至少含有两个1,所以不是2的次幂
} 

4.2 计算一个数的二进制含有多少个1

x中的最后一个1可以通过操作x = x&(x-1)循环消去,当最后x值为0时,便可以求出二进制中1的个数。

int x, total;
while(x > 0){ 
	x = x&(x-1);
    total++;
}

五、算法对”2的次方“应用

5.1 整数对2的乘/除法

整数n向右移一位,相当于将n除以 2;数n向左移一位,相当于将n乘以 2

int n = 2;
n >> 1; // 1
n << 1; // 4

5.2 n对“2的次方”取余

m是2的次方,则其二进制数只有一个1,如 4 => 01008 => 1000m-1之后,原本m二进制的1变成0,原本1后面的0全变成1,如 4-1 = 3 => 00118-1 = 7 => 0111

2 0 = 1 2^0 = 1 20=1
2 1 = 2 2^1 = 2 21=2
2 2 = 4 2^2 = 4 22=4
2 3 = 8 2^3 = 8 23=8
2 4 = 16 2^4 = 16 24=16
2 5 = 32 2^5 = 32 25=32
. . . ... ...

可以看出 2 q + 1 2^{q+1} 2q+1 永远都是 2 q 2^q 2q的整数倍,而 2 q 2^q 2q 2 q − 1 + 2 q − 2 + . . . + 2 0 2^{q-1} + 2^{q-2} + ... + 2^0 2q1+2q2+...+20的和还要大。

假设 m = 2 q m = 2^q m=2q,q为正整数。n的二进制数中,第[n的最高位, q]位的和是m的整数倍,而第[q-1, 0]位的和是n/m的余数,也就是说将n的二进制数的第[q-1, 0]位截取,即可得到n/m的余数。

m = 2 q , m − 1 = 2 q − 1 + 2 q − 2 + . . . + 2 0 = > 00011...1 m = 2^q,m - 1 =2^{q-1} + 2^{q-2} + ... + 2^0 => 00011...1 m=2qm1=2q1+2q2+...+20=>00011...1[q-1, 0]位全为1),所以n & (m - 1)的值为n/m的余数。

int mod, n, m; // m是2的次方,如4,8等
mod = n & (m - 1);

5.3 将n以“2的次方”倍数最小补全

有n和m两数,m为2的次方,找到大于等于n且正好是m整数倍的最小数。看不懂以下内容就先阅读《5.2 n对“2的次方”取余》。

假设 m = 2 q m = 2^q m=2q,则m的倍数的二进制数中,第q位为1,其余位全为0;(m - 1)的二进制数中[q-1, 0]位全为1,其余位全为0。

所以有两种情况:

  • 如果n的二进制数中[q-1, 0]位全为0,则n就是m整数倍的最小数;
  • 如果n的二进制数中[q-1, 0]位不全为0,在第q位上加1,所得结果就是m整数倍的最小数。

现给n加上一个[q-1, 0]位全为1,其余位全为0的数(m-1就是这个数),并将所得结果的[q-1, 0]位全部置为0(结果与~(m - 1)相与即可),便可满足这两种情况。

int min, n, m;
min = (n + m - 1) & ~(m - 1);

【实战】内存对齐运算

函数的作用是将内存值n进行内存对齐,m < 1 || m > 8 || m&(m-1) != 0表示对齐值m只能是4字节、8字节或16字节。

int64 MemAlign(n int64, m int64){
    if m < 1 || m > 16 || r&(r-1) != 0 {
        // 抛出error
        return 0;
    }
    return (n + m - 1) & ~(m - 1)
}

六、综合技巧应用

6.1 判断一个整数的奇偶性

对于数n,若n的二进制数的最低位为0,n为偶数,否则为奇数。

if(0 == (n & 1)){
	// 偶数
} else {
	// 奇数
}

6.2 判断一个数二进制中1的奇偶性

首先可以用《4.2 计算一个数的二进制含有多少个1》的方法,这里介绍一种新的方法。

以十进制数1314520为例,其二进制为0001 0100 0000 1110 1101 1000

第一次异或操作的结果如下:

  0001 0100 0000 1110 1101 1000
^ 0000 1010 0000 0111 0110 1100
= 0001 1110 0000 1001 1011 0100

得到的结果是一个新的二进制数,其中右起第i位上的数表示原数中第i和i+1位上有奇数个1还是偶数个1。比如,最右边那个0表示原数末两位有偶数个1,右起第3位上的1就表示原数的这个位置和前一个位置中有奇数个1。

对这个数进行第二次异或的结果如下:

  0001 1110 0000 1001 1011 0100
^ 0000 0111 1000 0010 0110 1101
= 0001 1001 1000 1011 1101 1001

结果里的每个1表示原数的该位置及其前面三个位置中共有奇数个1,每个0就表示原数对应的四个位置上共偶数个1。

一直做到第五次异或结束后,得到的二进制数的最末位就表示整个32位数里1的奇偶性

x = x ^ (x >> 1);
x = x ^ (x >> 2);
x = x ^ (x >> 4);
x = x ^ (x >> 8);
x = x ^ (x >> 16);

cout << (x & 1) << endl; // 输出 1 为奇数

6.3 判断两个数的符号是否相同

如果两个数x和y符号相同,符号位异或结果为0;符号不同,符号位异或结果为1。
0和正数的符号位都是0,所以0 ^ 正数 > 0。两个相等的值异或,值为零。

bool f = (x ^ y) < 0;	// f = true, x和y符号不相同 
if(f){
	// x和y符号不相同
}

【实战】有符号加法溢出判断

两个正数相加可能会产生正溢出,正溢出结果会变为负数;两个负数相加可能会产生负溢出,负溢出结果会变为正数。所以利用好符号位,就可以判断值是否溢出。

int x, y, s;
s = x + y;
bool f = ((s^x) & (s^y)) < 0
if(f){
	// overflow
}

无符号加法溢出判断虽然与这一块内容无关,但也提一下,详细介绍需阅读无符号数加法。记x+y是两个数的和,2^w是最高权重,如果x,y的和数据溢出,则和模(x+y) mod 2^w比x,y中的任何一个值都小。

6.4 取两数的较 大/小 值

如果a >= b,则a - b >= 0~(a - b) < 0,所以((a - b) >> 31) = 0(~(a - b) >> 31) = -1
如果a < b,则a - b < 0~(a - b) >= 0,所以((a - b) >> 31) = -1(~(a - b) >> 31) = 0

int max(int a, int b){
    return (b & ((a - b) >> 31)) | (a & (~(a - b) >> 31));
}

int min(int a, int b){
    return (a & ((a - b) >> 31)) | (b & (~(a - b) >> 31)
}

6.5 判断一个数的二进制有效位数是否超出

判断一个数x的二进制是否超过b位:

  • 如果x是无符号数,采用x >> b == 0即可判断;
  • 若x是有符号数,则不能这样,因为负数的符号位为1。设b = 12,则x的有效取值范围为[1111 1000 0000 0000, 0000 0111 1111 1111],其中第12位是符号位,往上的高位与符号位保持一致。如果将x>>12,则所得结果为0或-1;如果在x>>12之前,给x的第12位加1,然后再将x>>12,则所得结果为0。
#define LOONGF_U_OK(x, b) (((x) >> (b)) == 0)
#define LOONGF_S_OK(x, b) ((((x) + (1 << (b-1))) >> (b)) == 0)

// 判断x的二进制数是否超过12位
LOONGF_U_OK(x, 12)
LOONGF_S_OK(x, 12)

6.6 将无符号整数(u32/u64)转成浮点数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yelvens

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值