详解位元算

概述

  位操作是程序设计中对位模式或二进制数的一元和二元操作。在许多古老的微处理器上,位运算比加减运算略快,通常位运算比乘除法运算要快很多。在现代架构中,情况并非如此:位运算的运算速度通常与加法运算相同(仍然快于乘法运算)。实际编程中如果能巧妙运用位元算,将会有许多意想不到的事。

位运算操作基础

符号 描述 运算规则
& 位与运算 两个位都为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;
}
posted @ 2016-08-13 10:58  zxzhang  阅读(670)  评论(0编辑  收藏  举报