一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情。
前言:
位操作(Bit Manipulation)是程序设计中对位模式或二进制数的一元和二元操作。在许多古老的微处理器上,位运算比加减运算略快,通常位运算比乘除法运算要快很多。在现代编程语言中,情况并非如此,很多编程语言的解释器都会基本的运算进行了优化,因此我们在实际开发中可以不必做一些编译器已经帮我们做好的优化,而就写出代码本身所要表现的意思。
因此在这里总结一下关于位运算的各种思路与技巧:
正文:
1、位运算简介 思考一下: i & (i - 1) 、i & (-i) 是什么意思?
「位运算」只有6种,C语言里位运算的优先级。
优先级 名称 结合性 2 移位取反 右结合性 6 移位 <<、>> 左结合性 9 按位与 & 左结合性 10 按位异或 ^ 左结合性 11 按位或 | 左结合性 1.1、二进制恢复十进制 给定0b1010,求对应10进制 一般写法:
int a[] = {0,1,0,1} // 低到高 int base = 1; int ans = 0; for (i = 0; i < len; i++) { ans += a[i] * base; base *= 2; // 危险,这里有可能会溢出,建议把base 定义为long long // 改成 base = base << 1; 会更加快一些。如果提交超时,可以试试这种,碰碰运气 } 改进:
int a[] = {1,0,1,0} // 高到低 int ans = 0; for (i = 0; i < len; i++) { ans = (ans << 1) + a[i]; // 优雅 } 1.2、交换两数的值
a = a ^ b; b = a ^ b; a = a ^ b; /* a = 5 = 0b101; b = 11 = 0b1011; a = a ^ b = 5 ^ 11; 携带了两个值的差异信息 b = a ^ b = (5 ^ 11) ^ 11 = 5; a = a ^ b = (5 ^ 11) ^ ((5 ^ 11) ^ 11) = 11; */ 2、左移右移补位 补0、补1
3、原码、补码、反码 见《C语言点滴》(作者:赵岩)个人觉得这里把原码、补码、反码,解释的极为清晰
强推赵老师的这本书,希望大家买来仔细看看,这里做个概括描述。 正数、负数在计算机内存里是怎么存的? < 答:存二进制补码 >
a、原码不适合表示有符号数
问题1:存在两个0 问题2:正数 + 负数 != 0
b、反码:原码基础上,符号位不动,其他位取反
2 + -2 = 0b010 + 0b101 = 0b111 = 0 反码解决了上述问题2,正负数相加为0。但依然没有解决问题1。
c、补码:如果是正数补码 == 原码;如果是负数,原码基础上,符号位不动,其他位取反,再加1
再次强调:
“整型数在计算机中,使用补码表示” —— 赵岩《C语言点滴》 类型一:数1的个数 i & (i - 1) 是什么意思?假设i = 134 = 0b10000110那么 i - 1 = 133 = 0b10000101 那么i & (i - 1) = 10000100 最后一个1变成了0。**
3.1.1、对于1个数x,要知道它的二进制有几个1
/* 方法一 / cnt = 0; while(x) { cnt++; x = x & (x - 1); } / 方法二 */ cnt = 0; while(x) { cnt += x & 1; x = x >> 1; } 3.1.2、求1~n 分别都有几个1,利用已有的计算结果
a、 x 是 (x - 1) + 1求出来的,x 和 x - 1有什么关系吗?再来看看i & (i - 1),也就是说i比i & (i - 1)多了一个1 所以:cnt_x = cnt_(x & (x - 1)) + 1
b、利用移位 x 和 x >> 1 相差的只有 x & 0b1 因此:cnt_x = cnt_(x >> 1) + (x & 1)
见《剑指offer.专项突破版》
- 比特位计数(数字二进制中 1 的个数)
类型二、加减乘除
#include <stdio.h> int CalcAdd(int a, int b) { int sum, carry; while (b) { sum = a ^ b; // 不进位相加:例如十进制 37 + 28 = 55 carry = (a & b) << 1; // 进位:例如十进制 37 + 28,7 + 8产生进位1,并右移1位, 37 + 28 = 10
/* 继续计算 55 + 10 */
a = sum;
b = carry;
}
return a;
}
int CalcMul(int a, int b) { /* 计算 3721 * 1236 如下找规律 * res = (6 * 3721) * (10 ^ 0) + * (3 * 3721) * (10 ^ 1) + * (2 * 3721) * (10 ^ 2) + * (1 * 3721) * (10 ^ 3) * 改成二进制,换个简单的:0b1011 * 0b1101 * res = (1 * 0b1011) * (2 ^ 0) + * (0 * 0b1011) * (2 ^ 1) + * (1 * 0b1011) * (2 ^ 2) + * (1 * 0b1011) * (2 ^ 3) * */ int res = 0; while (b) { if (b & 1 == 1) { res = CalcAdd(res, a); } b = b >> 1; // 依次取b的最后x位,用以乘以a(这里都是二进制,所以直接加法实现) res = res << 1; // 不断乘以 2 ^ (x - 1),x表示b的最后第x位,从1开始数 } return res; }
int CalcDiv(int a, int b) { /* a = 3198 = 0b110001111110 * b = 57 = 0b000000111001 * (b << 6) = 3648、(b << 5) = 1824 也就是说:右移5位(第六位为1),恰好刚小于a,拿到0b100000 * 接着求: a = 3198 - (b * 0b100000) = 1374, b = 57的除法 * (b << 5) = 1824、(b << 4) = 912 也就是说:右移4位,恰好刚小于a,拿到0b10000 * 接着求: a = 1374 - (b * 0b10000) = 462, b = 57的除法 * (b << 3) = 456,b右移3位,恰好刚小于a,拿到0b1000 * 接着求: a = 462 - (b * 0b1000) = 6, b = 57的除法 * (b << 0) = 57,大于a,结束 * 所以: ans = 0b10000 + 0b1000 + 0b100 = 0b111000 * */
// 处理符号
int cnt = 0;
if (a < 0) {
cnt++; // 本函数为简便直接用+、-,也可用CalcAdd函数代替
a = ~(a) + 1;
}
if (b < 0) {
cnt++;
b = ~(b) + 1;
}
int res = 0;
for (int i = 31; i >= 0; i--) {
if ((a >> i) >= b) { // 恰好刚小于a 的移位
res |= 1 << i;
a -= (b << i);
}
// if ((b << i) < a) { // 理论上两种方式等价,但这里会溢出翻转 // res |= 1 << i; // a -= (b << i); // } } return (cnt == 1) ? res : (~(res) + 1); }
int main() { int a = 9; // 0b00000000000000000000000000001001 int b = 18; // 0b00000000000000000000000000010010
/* 计算 a + b */
int sum = CalcAdd(a, b);
/* 计算 a - b */
int sub1 = CalcAdd(a, -b); // 可以达成,但不能使用负号
int c1 = -b; // 0b11111111111111111111111111101110
int c2 = ~b; // 0b11111111111111111111111111101101
int c3 = ~b + 1; // 0b11111111111111111111111111101110
int sub2 = CalcAdd(a, (~(b) + 1)); // a - b
/* 计算 a * b */
int mul = CalcMul(a, b);
/* 除法 */
int div = CalcDiv(3198, 57);
return 0;
} 29. 两数相除(位运算实现)
见《剑指offer.专项突破版》 《程序员代码面试指南·左程云》
注:对于除法中:两个数的符号组合,最大值、最小值的边界处理,可以进一步阅读《程序员代码面试指南·左程云》
类型三、只出现一次的数字 异或:不等为1,具有传递性