位运算(3)-- 高级运用

来源:互联网 发布:sean cody 知乎 编辑:程序博客网 时间:2024/06/11 01:46

注:此文内容来自于对【数据结构与算法之位运算】课程所做的笔记

一、二进制中1的个数

问题:给定一个无符号整型变量,求其二进制表示中“1”的个数。
相似问题:判断整数A转换成整数B需要的次数。(A ^ B,再求“1”的个数)
分析:很容易想到右移,然后判断奇偶性,复杂度与最左侧的1的位置相关。

int oneCount(unsigned int n){    int c = 0;    while(n){        c += n & 1;        n >>= 1;    }    return c;}

优化1.:利用判断一个整数是否是2的幂采用的方法,每一次判断都会找到一个1,直到整数变为0为止。复杂度与1的个数相关。

int oneCount1(unsigned int n){    int c = 0;    while(n){        n &= (n - 1);   // 清除最低位的1        c++;    }    return c;}

优化2.:采用并行化规约思想进行优化。在GPU编程中,为了加速求和操作,会采用并行化规约技术使多个线程参与计算,从而将复杂度由O(n)将为O(logn). 位运算可以看成不同 bit 之间的线程并行。

int oneCount2(unsigned int n){         /*获得偶数位的值*/   /*获得奇数位的值*/        // 常数的二进制表示    n = (n & 0x55555555) + ((n >> 1)  & 0x55555555);    // 0101010101010101...    n = (n & 0x33333333) + ((n >> 2)  & 0x33333333);    // 0011001100110011...    n = (n & 0x0f0f0f0f) + ((n >> 4)  & 0x0f0f0f0f);    // 0000111100001111...    n = (n & 0x00ff00ff) + ((n >> 8)  & 0x00ff00ff);    // 0000000011111111...    n = (n & 0x0000ffff) + ((n >> 16) & 0x0000ffff);    // 0000000000000000...    return n;}

优化3.:编译器内置位运算,GCC下是__builtin_popcount,VS下是__popcnt

int oneCount3(unsigned int n){    return __builtin_popcount(n);   // VS: __popcnt(n)}

二、最左侧1问题

问题:给定一个整数,判断该整数最左侧1的位置 (most significant bit or count leading zeros problem)
way 1.:通过逐次移位获得最高位(左移右移皆可),复杂度较高。

int left_most_one_1(int n){    int pos = -1;    while(n){        n >>= 1;        pos++;    }    return pos;}

way 2.:通过二分法寻找最左侧1,复杂度较way1降低很多。

int left_most_one_2(int n){    if(n == 0) return -1;    int exp = 4;    int pos = (1 << exp);   //16的位置开始二分    while(exp > 0){        exp--;        if(n >> pos) pos += (1 << exp);        else pos -= (1 << exp);    }    return n >> pos != 0 ? pos : pos - 1;}

way 3.:原问题等价于将整数转化成科学记数法的幂。例如,01101000 = 1.101 * 2 ^ 6。首先将整数强转为浮点数(必须用double,防止精度损失),此时浮点数中就包含了指数部分(想想计算机是怎么表示浮点数的),将其解析出来即是最左侧1。解析方法是地址强转:获得浮点数的地址,将地址强转成int型地址,然后移位即可获得指数。

int left_most_one_3(int n){    if(!n) return -1;    double b = n;    /* 右移52位获得双精度浮点数的指数部分,再与上2047(0b11111111111)只获得指数部分,    *  最后减去偏移量(移码),就是最终的指数部分的十进制值    */    return ((*(long long*)&b) >> 52 & 2047) - 1023;}

way 4.:编译器内置位运算,GCC下是__builtin_clz,VS下是__lzcnt

int left_most_one_4(int n){    return 31 - __builtin_clz(n);   // VS: __lzcnt}

三、Single number

I

问题:给定n个整数,除了一个数出现一次之外,其余的整数均出现了两次,找到只出现一次的整数。
分析:利用异或的性质,x ^ x = 0x ^ 0 = x,异或满足交换律。将所有的整数异或,出现两次的整数就变为0,出现一次的整数保持不变。

int singleNumberI(int* A, int len){    int res = 0;    for(int i = 0; i < len; ++i){        res ^= A[i];    }    return res;}

II

问题:给定n个整数,除了一个数出现一次之外,其余的整数均出现了三次,找到只出现一次的整数。
分析:将每一个整数看成是一个长度为32位 的数组,然后统计32位中每一位出现1的次数。如果一个数出现了3次,则其出现1的位肯定也是3次,这时如果某位出现了4次,则意味着出现1次的数在该位也为1。时间复杂度:O(32n)

int singleNumberII(int* A, int len){    int count[32], res;    memset(count, 0, sizeof(count));    for(int i = 0; i < len; ++i){        for(int j = 0; j < 32; ++j){            count[j] += A[i] >> j & 1;        }    }    for(int i = 0; i < 32; ++i){        res |= count[i] % 3 << i;   // 模3再放置到对应的位    }    return res;}

优化:构造真值表,计算逻辑函数表达式。
这里写图片描述
这里写图片描述
这里写图片描述
进一步优化
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

int singleNumberII2(int* A, int len){    int ones = 0, twos = 0;    for(int i = 0; i < len; ++i){        ones = (ones ^ A[i]) & ~twos;        twos = (twos ^ A[i]) & ~ones;    }    return ones;}

III

问题:给定n个整数,除了两个数出现一次外,其余的整数均出现了两次,找到只出现了一次的这两个整数。
分析:将所有的整数异或可获得这两个整数的异或值,还需要有其他的操作把它们分开。两个整数不同,它们的异或值非零,0表示这两个整数在该位相同,1表示不同。我们可以利用它们某一个为1的位对所有的整数进行划分,划分成该位为1的一组和该位为0的一组,进而可以区分这两个数。

int* singleNumberIII(int* nums, int numSize, int* returnSize){    int* res = (int*)malloc(sizeof(int) * 2);    memset(res, 0, sizeof(int) * 2);    int xorsum = 0;    for(int i = 0; i < numSize; ++i){        xorsum ^= nums[i];    }    int low_one = xorsum & -xorsum;     // 获得最低有效位,即最低位1的位置    for(int i = 0; i < numSize; ++i){        /* 分成两类 */        if(low_one & nums[i]){            res[0] = res[0] ^ nums[i];        }        else{            res[1] = res[1] ^ nums[i];        }    }    *returnSize = 2;    return res;}

四、N皇后问题

利用位运算提升回溯法的效率。”利用当前已放置皇后的位置,我们可以推测出下一行哪些位置可行,哪些位置不可行,从而可以避免遍历很多无效的位置。”
如图:
这里写图片描述
其中,row表示准备放置的行的纵列可以放置的位置,ld表示该行左斜对角线可以放置的位置,rd表示该行右斜对角线可以放置的位置。
那么,该位置即为:~(row | ld | rd)

const int QUEEN_NUM = 14;       // 皇后的个数int position[QUEEN_NUM];       // 每个皇后在每一行的位置static int all_queen = (1 << QUEEN_NUM) -1;static int sum = 0;            // 放法// 假设在前 t - 1 行没有冲突的情况下,第t行放置的位置是否冲突bool check(int t){    for(int i = 0; i < t; ++i){        // 行 - 行 == 列 - 列 或者 在同一列        if(abs(t - i) == abs(position[t] - position[i]) || position[i] == position[t])            return false;    }    return true;}// 回溯法void n_queen_backtrace(int row){    // 在第row行放置一个皇后    if(row == QUEEN_NUM){        sum++;        return;    }    for(int i = 0; i < QUEEN_NUM; ++i){        position[row] = i;        if(check(row)) n_queen_backtrace(row + 1);    }}/** 采用位运算的解法* row, ld和rd, 分别表示在纵列和两个对角线方向的限制条件下该行的哪些地方不能放*/void n_queen_bit(int row, int ld, int rd){    if(row == all_queen){        sum++;        return;    }    // pos 表示当前行可以放置皇后的位置    int pos = all_queen & ~(row | ld | rd);    // 挑出这些位置    while(pos){        int p = pos & -pos;        pos -= p;        n_queen_bit(row + p, (ld + p) << 1, (rd + p) >> 1);    }}int main(){    n_queen_backtrace(0);    //n_queen_bit(0, 0, 0);    printf("%d\n", sum);    return 0;}

五、格雷码

问题 I:给定一个二进制数n,获得其对应的格雷码,或者反过来。
分析:二进制转为格雷码:Gi=BiBi+1(n1i0)。即从最高位开始将前两位异或,可转化成公式:G = B ^ (B >> 1)

unsigned int binary2gray(unsigned int n){    return n ^ (n >> 1);}

格雷码转为二进制:Bi=GiBi+1(n1i0)

unsigned int gray2binary(unsigned int g){    unsigned int b = g;    while(g >>= 1){        b ^= g;    }    return b;}

格雷码的特点:最高为在转换前后不变,其余位上下呈镜面对称。

问题 II:把02 ^ (n + m) - 1的数写成2 ^ n * 2 ^ m的矩阵,使得上下左右位置相邻两数的二进制表示只有一位之差。
分析:所有数都是由m位格雷码和n位的格雷码拼接而成,只需要用左移操作和或运算完成。
实现

void print_binary_format(int g){    int bit[32] = {0};    int i = 31;    while(g){        bit[i--] = g % 2;        g /= 2;    }    for(int j = 0; j < 32; ++j){        printf("%d", bit[j]);    }}void gen_gray_matrix(int m, int n){    for(int i = 0; i < (1 << m); ++i){      // 行        int high = (i ^ (i >> 1)) << n; // 高位部分格雷码        for(int j = 0; j < (1 << n); ++j){  // 列            int low = j ^ (j >> 1);     // 低位部分格雷码            print_binary_format(high | low);            printf(" ");        }        printf("\n");    }}
1 0
原创粉丝点击