装逼指南 -- 位运算

1、判断奇偶数

传统做法:

if( n % 2) == 1
    // n 是个奇数
}

位运算:

如果把 n 以二进制的形式展示的话,其实我们只需要判断最后一个二进制位是 1 还是 0 就行了,如果是 1 代表是奇数,如果是 0 代表是偶数,代码如下:

if(n & 1 == 1){
    // n 是个奇数。
}

如:n=5  
0 0 0 0  0 1 0 1 
& 
0 0 0 0  0 0 0 1
-------------------
0 0 0 0  0 0 0 1

如:n=6
0 0 0 0  0 1 1 0 
& 
0 0 0 0  0 0 0 1
-------------------
0 0 0 0  0 0 0 0

2、交换两个数

传统做法:

int tmp = x;
x = y;
y = tmp;

如果不允许你使用额外的辅助变量来完成交换呢?代码如下:

x = x ^ y   // (1)
y = x ^ y   // (2)
x = x ^ y   // (3)

在此我解释下:

1. 两个相同的数异或之后结果等于 0,即 n ^ n = 0   

2.任何数与 0 异或等于它本身,即 n ^ 0 = n

所以,解释如下:

把(1)中的 x 带入 (2)中的 x,有 y = x^y = (x^y)^y = x^(y^y) = x^0 = x。 x 的值成功赋给了 y。

对于(3),推导如下:x = x^y = (x^y)^x = (x^x)^y = 0^y = y。

这里解释一下,异或运算支持运算的交换律和结合律哦。

3、找出没有重复的数

给你一组整型数据,这些数据中,其中有一个数只出现了一次,其他的数都出现了两次,让你来找出一个数 。

我们刚才说过,两个相同的数异或的结果是 0,一个数和 0 异或的结果是它本身,所以我们把这一组整型全部异或一下,例如这组数据是:1,  2,  3,  4,  5,  1,  2,  3,  4。其中 5 只出现了一次,其他都出现了两次,把他们全部异或一下,结果如下:

由于异或支持交换律和结合律,所以:

1^2^3^4^5^1^2^3^4 = (1^1)^(2^2)^(3^3)^(4^4)^5= 0^0^0^0^5 = 5。

也就是说,那些出现了两次的数异或之后会变成0,那个出现一次的数,和 0 异或之后就等于它本身。所以代码如下:

int find(int[] arr){
    int tmp = arr[0];
    for(int i = 1;i < arr.length; i++){
        tmp = tmp ^ arr[i];
    }
    return tmp;
}

时间复杂度为 O(n),空间复杂度为 O(1),而且看起来很牛逼。

4、m的n次方

如果让你求解 m 的 n 次方,并且不能使用系统自带的 pow 函数,你会怎么做呢?这还不简单,连续让 n 个 m 相乘就行了,时间复杂度为 O(n),代码如下:

int pow(int m, int n){
    int tmp = 1;
    for(int i = 1; i <= n; i++) {
        tmp = tmp * m;
    }
    return tmp;
}

位运算:例如 n = 13,则 n 的二进制表示为 1101, 那么 m 的 13 次方可以拆解为:

m^1101 = m^0001 * m^0100 * m^1000 = m^1 * m^4 * m^8

我们可以通过 & 1和 >>1 来逐位读取 1101,为1时将该位代表的乘数累乘到最终结果。直接看代码吧,反而容易理解:

int pow(int m, int n){
    int sum = 1;
    int tmp = m;
    while(n != 0){
        if(n & 1 == 1){
            sum *= tmp;
        }
        tmp *= tmp;
        n = n >> 1;
    }

    return sum;
}

如:m 的 13 次方
n=13 第一轮后: sum=m    tmp=m^2  n=6
n=6  第二轮后: sum=m    tmp=m^4  n=3
n=3  第三轮后: sum=m^5  tmp=m^8  n=1 
n=1  第四轮后: sum=m^13 tmp=m^16 n=0 //优化点: n=1时可直接返回sum, 不用再执行后面两个算式
最终sum=m^13

时间复杂度近为 O(logn)

5、找出不大于N的最大的2的幂指数

传统做法:就是让 1 不断着乘以 2,这样做的话,时间复杂度是 O(logn)

int findN(int N){
    int sum = 1;
   while(true){
        if(sum * 2 > N){
            return sum;
        }
        sum = sum * 2;
   }
}

位运算:例如 N = 19,那么转换成二进制就是 0001 0011(这里为了方便,我采用8位的二进制来表示)。那么我们要找的数就是,把二进制中最左边的 1 保留,后面的 1 全部变为 0,即我们的目标数是 0001 0000。那么如何获得这个数呢?相应解法如下:

1、找到最左边的 1,然后把它右边的所有 0 变成 1

2、把得到的数值加 1,可以得到 0010 0000,即 0001 1111 + 0000 0001 = 0010 0000

3、把 得到的 0010 0000 向右移动一位,即可得到 0001 0000,即 0010 0000 >> 1 = 0001 0000

那么问题来了,第一步中把最左边 1 中后面的 0 转化为 1 该怎么弄呢?下面这段代码就可以把最左边 1 中后面的 0 全部转化为 1,

n |= n >> 1;
n |= n >> 2;
n |= n >> 4;

就是通过把 n 右移并且做运算即可得到,这种做法的时间复杂度近似 O(1),代码如下:

int findN(int n){
    n |= n >> 1;  //(1)
    n |= n >> 2;  //(2)
    n |= n >> 4;  //(3)
    n |= n >> 8;  //(4)
    n |= n >> 16; //(5) 
    return (n + 1) >> 1;
}

说明:
(1) 执行完后,左数第 k、k+1 位一定是1,  即:左数从第k位开始,其前2位已经为1
(2) 执行完后,左数第 k+2、k+3 位一定是1,即:左数从第k位开始,其前4位已经是1
(3) ...,                             即:左数从第k位开始,其前8位已经是1
(4) ...,                             即:左数从第k位开始,其前16位已经是1
(5) ...,                             即:左数从第k位开始,其前32位已经是1

ps:(1)~(5) 可以由 32 个 n |= n >> 1; 来替换,即每次只右移1位并作或运算,显然这样执行效率差!!

 转载自:【算法技巧】位运算装逼指南【算法技巧】位运算装逼指南
  位运算的歪门邪道_meet snow happy的博客-CSDN博客
  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值