算法通关村 | 位运算的妙用

颠倒无符号整数

LeetCode190 .颠倒给定的 32 位无符号整数的二进制位。 提示:输入是一个长度为32的二进制字符串。

示例1:
输入:n = 00000010100101000001111010011100
输出:964176192 (00111001011110000010100101000000)
解释:输入的二进制串 00000010100101000001111010011100 表示无符号整数 43261596,
     因此返回 964176192,其二进制表示形式为 00111001011110000010100101000000。

示例2:
输入:n = 11111111111111111111111111111101
输出:3221225471 (10111111111111111111111111111111)
解释:输入的二进制串 11111111111111111111111111111101 表示无符号整数 4294967293,
     因此返回 3221225471 其二进制表示形式为 10111111111111111111111111111111 。

首先这里说是无符号位,那不必考虑正负的问题,最高位的1也不表示符号位,这就省掉很多麻烦。
我们注意到对于 n 的二进制表示的从低到高第 i 位,在颠倒之后变成第 31-i 位( 0≤i<32),所以可以从低到高遍历 n 的二进制表示的每一位,将其放到其在颠倒之后的位置,最后相加即可。
看个例子,为了方便我们使用比较短的16位演示:

原始数据:1001 1111 0000 0110(低位)
第一步:获得n的最低位0,然后将其左移16-1=15位,得到:
reversed: 0*** **** **** **** 
n逻辑右移一位: 0100 1111 1000 0011

第二步:继续获得n的最低位1,然后将其左移15-1=14位,并与reversed相加得到:
reversed:01** **** **** **** 
n逻辑右移一位:0010 0111 1100 0001

继续,一直到n全部变成0:

理解之后,实现就比较容易了。由于 Java不存在无符号类型,所有的表示整数的类型都是有符号类型,因此需要区分算术右移和逻辑右移,在Java 中,算术右移的符号是 >>,逻辑右移的符号是 >>>。

public int reverseBits(int n) {
    int reversed = 0, power = 31;
    while (n != 0) {
        reversed += (n & 1) << power;
        n >>>= 1;
        power--;
    }
    return reversed;
}

本题的解法还有很多,例如还有一种分块的思想, n 的二进制表示有 32 位,可以将 n 的二进制表示分成较小的块,然后将每个块的二进制位分别颠倒,最后将每个块的结果合并得到最终结果。这分治的策略,将 n 的 32 位二进制表示分成两个 16 位的块,并将这两个块颠倒;然后对每个 16 位的块重复上述操作,直到达到 1 位的块。为了方便看清楚,我们用字母代替01,如下图所示。

在这里插入图片描述

32位无符号整数,如 1111 1111 1111 1111 1111 1111 1111 1111 
表示成16进制        f    f    f    f    f    f    f   f
一个16进制的f代表二进制的4位
ffff ffff右移16位,变成 0000 ffff
ffff ffff左移16位,变成 ffff 0000
它们俩相或,就可以完成低16位与高16位的交换

之后的每次分治,都要先与上一个掩码,再进行交换

具体做法是:
下面的代码中,每一行分别将 n 分成16 位、8 位、4 位、2 位、1 位的块,即把每个块分成两个较小的块,并将分成的两个较小的块颠倒。同样需要注意,使用 Java 实现时,右移运算必须使用逻辑右移。由于是固定的32位,我们不必写循环或者递归,直接写:

reverseBits(int n) {
    n = (n >>> 16) | (n << 16);
    n = ((n & 0xff00ff00) >> 8) | ((n & 0x00ff00ff) << 8); //每16位中低8位和高8位交换; 1111是f
    n = ((n & 0xf0f0f0f0) >> 4) | ((n & 0x0f0f0f0f) << 4); //每8位中低4位和高4位交换;
    n = ((n & 0xcccccccc) >> 2) | ((n & 0x33333333) << 2); //每4位中低2位和高2位交换; 1100是c,0011是3
    n = ((n & 0xaaaaaaaa) >> 1) | ((n & 0x55555555) << 1); //每2位中低1位和高1位交换; 1010是a,0101是5
    return n;
}

这种方法在JDK、Dubbo等源码中都能见到,特别是涉及协议解析的场景几乎都少不了位操作。积累相关的技巧,可以方便面试,也有利于阅读源码。

位运算实现加法

LeetCode371 给你两个整数 a 和 b ,不使用 运算符 + 和 - ,计算并返回两整数之和。

示例1:
输入:a = 1, b = 2
输出:3

既然不能使用+和-,那只能使用位运算了。我们看一下两个二进制位相加的情况:

[1] 0 + 0 = 0
[2] 0 + 1 = 1
[3] 1 + 0 = 1
[4] 1 + 1 = 0 (发生了进位,应该是10的)

两个位加的时候,我们无非就考虑两个问题:进位部分是什么,不进位部分是什么。从上面的结果可以看到,对于a和b两个数不进位部分的情况是:相同为0,不同为1,这不就是a ⊕ b吗?
而对于进位,我们发现只有a和b都是1的时候才会进位,而且进位只能是1,这不就是a & b=1吗?然后位数由1位变成了两位,也就是上面的[4]的样子,那怎么将1向前挪一下呢?手动移位一下就好了,也就是(a & b) << 1。所以我们得到两条结论:

  • 不进位部分:用a ⊕ b计算就可以了。
  • 是否进位,以及进位值使用(a & b) << 1计算就可以了。
parame: 00000000 00000000 00000000 00000111(7) + 00000000 00000000 00000000 00000010(2)
expect:00000000 00000000 00000000 00001001(9)

第一轮:
00000000 00000000 00000000 00000111
00000000 00000000 00000000 00000010
----------------------------------- 与运算
00000000 00000000 00000000 00000010 与不为0,证明有需要进位的


00000000 00000000 00000000 00000111
00000000 00000000 00000000 00000010
----------------------------------- 异或运算
00000000 00000000 00000000 00000101 如果与不为0的话,其实异或就是和了,不过本例与不为0,需要进位。


第二轮:
00000000 00000000 00000000 00000100 上一轮与<<1: 进位的办法就是把上一轮与的结果左移一位(进位)
00000000 00000000 00000000 00000101 上一轮异或结果
----------------------------------- 与运算
00000000 00000000 00000000 00000100 与不为0,证明还有需要进位的


00000000 00000000 00000000 00000100 上一轮与<<1
00000000 00000000 00000000 00000101 上一轮异或结果
----------------------------------- 异或运算
00000000 00000000 00000000 00000001 


第三轮:
00000000 00000000 00000000 00001000 上一轮与<<1
00000000 00000000 00000000 00000001 上一轮异或结果
----------------------------------- 与运算
00000000 00000000 00000000 00000000 与为0,证明已经没有进位的必要,异或即为和。


00000000 00000000 00000000 00001000 上一轮与<<1
00000000 00000000 00000000 00000001 上一轮异或结果
----------------------------------- 异或运算
00000000 00000000 00000000 00001001 与为0,异或即为结果

于是,我们可以将整数 a 和 b 的和,拆分为 a 和 b 的无进位加法结果与进位结果的和,代码就是:

public int getSum(int a, int b) {
    while (b != 0) {
        int sign = (a & b) << 1;
        a = a ^ b;
        b = sign;
    }
    return a;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值