详解位元算
概述
位操作是程序设计中对位模式或二进制数的一元和二元操作。在许多古老的微处理器上,位运算比加减运算略快,通常位运算比乘除法运算要快很多。在现代架构中,情况并非如此:位运算的运算速度通常与加法运算相同(仍然快于乘法运算)。实际编程中如果能巧妙运用位元算,将会有许多意想不到的事。
位运算操作基础
符号 | 描述 | 运算规则 |
---|---|---|
& | 位与运算 | 两个位都为1时,结果才为1 |
| | 位或运算 | 两个位都为0时,结果才为0 |
^ | 异或运算 | 两个位相同为0,相异为1 |
~ | 取反运算 | 0变1,1变0 |
<< | 左移运算 | 各二进位全部左移若干位,高位丢弃,低位补0 |
>> | 右移运算 | 各二进位全部右移若干位,对无符号数,高位补0,有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补0(逻辑右移) |
注意:位运算符优先级很低,所以在运用的时候最好加上括号,否则会得到一些很奇怪的结果,这一点在《《C陷阱与缺陷》》一书也特别指明。
位操作技巧
以下讨论均默认为正数
-
判断奇偶
奇偶数只要根据末位是0还是1就可判断,因此可用if (x & 1)
来判断x的奇偶性,条件判断为真即为奇数,反之为偶数。 -
判断一个数是否为2的幂次
如果一个数是2的n次方,那么这个数的二进制形式中只有一位为1,这样,减1之后,为1的那个位变为0,后面的位变为1,两个数相与结果为0;如果数不是2的n次方,那么减1之后再相与,结果肯定不为0。(注:数0需要特判)因此可用if ((x&(x-1))
来实现判断,条件为真不是2的幂次,反之则是。 -
求给定整数的二进制数中1的个数
考虑到 n-1 会把 n 的二进制表示中最低位的1置0并把其后的所有0置1,同时不改变此位置前的所有位,那么n&(n-1)
即可消除这个最低位的1。这样便有了比顺序枚举所有位更快的算法:循环消除最低位的1,循环次数即所求1的个数。此算法的时间复杂度为O(k)
(k为二进制数中1的个数),最坏情况下的复杂度O(n)
(n为二进制数的总位数)。
int count(int x) {
int cnt = 0;
while (x) {
x &= x - 1;
cnt++;
}
return cnt;
}
- 求给定整数的二进制数中0的个数
先线性求出这个整数的二进制数的有效位的个数,再求这个二进制数中1的个数,有效位的个数减去二进制数中1的个数。
int count_bit(int x) { //线性求出整数的二进制表示的有效位
int cnt = 0;
while (x) {
x >>= 1;
cnt++;
}
return nt;
}
int get_leftmost_set_bit(unsigned int n) { //二分查找最高位1的位置
int l, u, m, t1, t2;
l = 0;
u = sizeof(int) * 8 - 1;
while (l <= u) {
m = l + (u - l) / 2;
t1 = n & (~((1 << m) - 1));
t2 = n & (~((1 << (m + 1)) - 1));
if (t1 && !t2) {
return m + 1;
} else if (t1 && t2) {
l = m + 1;
} else {
u = m - 1;
}
}
return 0;
}
-
对2的幂次方取模转化成位运算
x % (2^n)
等价于x & (2^n - 1)
2^n 用二进制表示为在第 n + 1 位(倒着数)为1,其余位为0,如2^3表示为1000,2^n - 1
用二进制表示则为在 1-n 位都为1其余位为0,某数对2^n
取余,转换为对2^n - 1
进行与运算,根据与运算的特性理解此算法。显然的,任意一个x可以用二进制表示,当表示成的二进制位数超过n位,求余时,则高于n位的1将都置为0,因为能整除2^n
,而x表示成的二进制数在 1 - n 位上如果有1,则保留下来,因为这些位表示的数显然不大于2^n. -
变换符号
亦即正数变成负数,负数变成正数。变换符号只需要取反后加1即可。
int SignReversal(int a) {
return ~a + 1;
}
- 求绝对值
先通过移位来取得数的符号位,为0为整数,为-1为负数
int abs(int a) {
int tmp = a >> 31;
return tmp ? (~a + 1) : a;
}
- 利用二进制完成加减乘除
加法:异或是不进位的加法,模拟进位可通过位与运算然后左移,直到进位为0;
int add(int a, int b) { //递归版本
return b ? add(a ^ b, (a & b) << 1) : a;
}
int add(int a, int b) { //迭代版本
int sum;
while (b) {
sum = a ^ b;
b = (a & b) << 1;
a = sum;
}
return sum;
}
另外,可以通过模拟二进制加法运算的方式来模拟十进制加法sum = ((a^b) + ((a&b)<<1));
根据上述代码,我们可以轻易的得出两个整数的平均值的求法average = ((a^b)>>1 + (a&b));
可以这么理解这个平均值求法:sum的二分之一即为a+b的平均值,那么就是sum>>1;
,带入sum = ((a^b) + ((a&b)<<1));
得到average = ((a^b)>>1 + (a&b));
此外,通过这种方式求出的平均值避免了a+b溢出的情况
- 减法:变换减数的符号利用加法完成减法。
int SignReversal(int a) { //get -a
return add(~a, 1);
}
int Minus(int a, int b) {
return add(a, SignReversal(b));
}
- 乘法:原理上还是通过加法计算,将b个a相加,也就是快速乘
int Multi(int a, int b) {
int sum = 0;
while (b) {
if (b & 1) {
sum = add(sum, a);
}
a <<= 1;
b >>= 1;
}
return sum;
}
- 除法:原理上就是乘法运算的逆,看a能减去几个b
int Divide(int a, int b) {
int diff = 0;
while (a >= b) {
a = Minus(a, b);
diff = add(diff, 1);
}
return diff;
}
┆ 凉 ┆ 暖 ┆ 降 ┆ 等 ┆ 幸 ┆ 我 ┆ 我 ┆ 里 ┆ 将 ┆ ┆ 可 ┆ 有 ┆ 谦 ┆ 戮 ┆ 那 ┆ ┆ 大 ┆ ┆ 始 ┆ 然 ┆
┆ 薄 ┆ 一 ┆ 临 ┆ 你 ┆ 的 ┆ 还 ┆ 没 ┆ ┆ 来 ┆ ┆ 是 ┆ 来 ┆ 逊 ┆ 没 ┆ 些 ┆ ┆ 雁 ┆ ┆ 终 ┆ 而 ┆
┆ ┆ 暖 ┆ ┆ 如 ┆ 地 ┆ 站 ┆ 有 ┆ ┆ 也 ┆ ┆ 我 ┆ ┆ 的 ┆ 有 ┆ 精 ┆ ┆ 也 ┆ ┆ 没 ┆ 你 ┆
┆ ┆ 这 ┆ ┆ 试 ┆ 方 ┆ 在 ┆ 逃 ┆ ┆ 会 ┆ ┆ 在 ┆ ┆ 清 ┆ 来 ┆ 准 ┆ ┆ 没 ┆ ┆ 有 ┆ 没 ┆
┆ ┆ 生 ┆ ┆ 探 ┆ ┆ 最 ┆ 避 ┆ ┆ 在 ┆ ┆ 这 ┆ ┆ 晨 ┆ ┆ 的 ┆ ┆ 有 ┆ ┆ 来 ┆ 有 ┆
┆ ┆ 之 ┆ ┆ 般 ┆ ┆ 不 ┆ ┆ ┆ 这 ┆ ┆ 里 ┆ ┆ 没 ┆ ┆ 杀 ┆ ┆ 来 ┆ ┆ ┆ 来 ┆