算法合集:位运算——就两个数字还想劝退我?


位运算其实是十分简单的算法,但碍于操作符功能较混杂,劝退了一部分的小伙伴,那么这一篇从 实战的角度吃透位运算, 想快速上手的话可以直接跳到第三部分

一、计算机中存储数值的方式

下面以int类型为例来讲解计算机中存储数值的方式
1、原码

正数的原码是其二进制
负数的原码是先得到其正数的二进制,然后在最高位补1(也就是符号位,位于最左侧)

  • 20的原码是 00000000 00000000 00000000 00010100
  • -20的原码是 10000000 00000000 00000000 00010100
  • 2147483647的原码是01111111 11111111 11111111 11111111
  • -2147483647的原码是11111111 11111111 11111111 11111111

但此时有个问题,存在+0-0两种情况,所以原码不能解决问题

2、反码

正数的反码是其原码
负数的反码是其原码的最高位不变,其余位取反

  • 20的反码是 00000000 00000000 00000000 00010100
  • -20的反码是 11111111 11111111 11111111 11101011
  • 2147483647的反码是01111111 11111111 11111111 11111111
  • -2147483647的反码是10000000 00000000 00000000 00000000

仍存在+0-0两种情况,所以反码仍不能解决问题

3、补码

正数的补码是其反码
负数的补码是其反码的最低位+1

  • 20的补码是 00000000 00000000 00000000 00010100
  • -20的补码是 11111111 11111111 11111111 11101100
  • 2147483647的补码是01111111 11111111 11111111 11111111
  • -2147483647的补码是10000000 00000000 00000000 00000001
  • -2147483648的补码是10000000 00000000 00000000 00000000,是一个特殊的存在

此时+0-0就一致成了0,所以在计算机中,数值是以补码的方式来存储的,若要求出其十进制下的数字,得层层反推:补码,反码,原码

4、整理一下

正数的原码,反码,补码均为其二进制
负数的原码为其正数的二进制最高位补1,反码为其原码除最高位取反,补码为其反码最低位+1

然后我们得以解释-2147483648的由来以及为什么int类型是个环

  • 2147483647的补码是 01111111 11111111 11111111 11111111
  • 1的补码是 00000000 00000000 00000000 00000001
  • -1的补码是 11111111 11111111 11111111 11111111
  • 2147483647 + 1 = 10000000 00000000 00000000 00000000 = -2147483648
  • -2147483648 + (-1) = 01111111 11111111 11111111 11111111 = 2147483647

这里-2147483648最高位的1不仅代表的是负号,也代表的是2的32次方(会在>>运算符中讲到)

二、位运算相关运算符

实战归实战,相关运算符还得先看一遍
位运算操作符
1、非运算符~
补码的所有位取反即可

  • 20的补码是 00000000 00000000 00000000 00010100
  • ~20的补码是 11111111 11111111 11111111 11101011 = -21
  • 2147483647的补码是 01111111 11111111 11111111 11111111
  • ~2147483647的补码是 10000000 00000000 00000000 00000000 = -2147483648
  • 0的补码是 00000000 00000000 00000000 00000000
  • ~0的补码是 11111111 11111111 11111111 11111111 = -1

我们可以推出结论,对于一个十进制的数x:~x = -x - 1

2、与运算符&
比较两个数的补码,按位比较,同为1取1,否则取0

  • 20的补码是 00000000 00000000 00000000 00010100
  • -20的补码是 11111111 11111111 11111111 11101100
  • -20 & 20 = 00000000 00000000 00000000 00000100 = 4

3、或运算符|
比较两个数的补码,按位比较,同为0取0,否则取1

  • 20的补码是 00000000 00000000 00000000 00010100
  • -20的补码是 11111111 11111111 11111111 11101100
  • -20 | 20 = 11111111 11111111 11111111 11111100 = -4

4、或运算符^
比较两个数的补码,按位比较,相异取1,否则取0

  • 20的补码是 00000000 00000000 00000000 00010100
  • -20的补码是 11111111 11111111 11111111 11101100
  • -20 ^ 20 = 11111111 11111111 11111111 11111000 = -8

特别的,0 ^ x = xx ^ x = 0

5、左移运算<<
补码每一位左移n,低位补0,高位舍弃

  • 20的补码是 00000000 00000000 00000000 00010100
  • 20 << 1 是 00000000 00000000 00000000 00101000 = 40
  • 20 << 2 是 00000000 00000000 00000000 01010000 = 80
  • 2147483647的补码是 01111111 11111111 11111111 11111111
  • 2147483647 << 1 是 11111111 11111111 11111111 11111110 = -2
  • 1073741824的补码是 01000000 00000000 00000000 00000000
  • 1073741824 << 1 是 10000000 00000000 00000000 00000000 = -2147483648

6、右移运算>>
补码每一位左移n,高位补0,低位舍弃,相当于除以二

  • 20的补码是 00000000 00000000 00000000 00010100
  • 20 >> 1 是 00000000 00000000 00000000 00001010 = 10
  • 20 >> 2 是 00000000 00000000 00000000 00000101 = 5
  • -2147483648的补码是10000000 00000000 00000000 00000000
  • -2147483648 >> 1 是11000000 00000000 00000000 00000000 = -1073741824

也就是说-2147483648最高位的1不仅代表的是负号,也代表的是2的32次方

三、其他进制

通过位运算,可以得到一个数在2的指数进制下的值,比如4进制,8进制,16进制
1、分组
每个int在计算机中是32个bit,所以4进制,6进制,16进制可以如下表示

4进制:分成十六组,每组最大值为3,每个组都代表一个4进制数
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
8进制:分成十一组,除最高位的组外,每组最大值为7,每个组都代表一个8进制数
00 000 000 000 000 000 000 000 000 000 000
16进制:分成八组,每组最大值为15,每组代表一个16进制数
0000 0000 0000 0000 0000 0000 0000 0000

运用整体的思想,把32位拆成组,对每组分别求值
2、求值
求值的过程就是通过位运算将某个组独立出来,再求值

通过(num >> n * k) & p将组独立出来并求值
>>来去除低位的bit,&来去除高位的bit
其中k每一组的bit数,4进制为2,8进制为3,16进制为4,
n为倍数,从32 / k - 1开始,到0结束,目的是从高位到低位取得num的各组
p进制数 - 1,4进制是3(二进制:11),8进制是7(二进制:111),16进制为15(二进制:1111)

举个例子,求41的4进制

  • 41的补码:00000000 00000000 00000000 00101001
  • 41以4进制分组(高位重复的0暂时忽略):00 10 10 01
  • (41 >> 3 * 2) & 3 = (二进制)000(41 >> 2 * 2) & 3 = (二进制)102
  • (41 >> 1 * 2) & 3 = (二进制)102(41 >> 0 * 2) & 3 = (二进制)011
  • 所以41的4进制为(高位重复的0暂时忽略):0221

3、实战题目:LeetCode 405 数字装换成十六进制数
通过实战来讲解
首先分组,取-1举例,16进制将分成8个组

  • -1的补码:11111111 11111111 11111111 11111111
  • -1以16进制分组:1111 1111 1111 1111 1111 1111 1111 1111
  • (-1 >> 7 * 4) & 15 = (二进制)111115,十六进制下为f,得到每一组的结果均为f
  • 则-1的16进制为:ffffffff

具体实现细节不多讲了,LeetCode 405 官方题解写的非常清楚

public String toHex(int num) {
    if (num == 0) return "0";
    StringBuilder builder = new StringBuilder();
    // 分为8组,从最左端开始算
    for (int i = 7; i >= 0; i--) {
        int val = (num >> (4 * i)) & 15; // 0xf
        if (builder.length() > 0 || val > 0)
            builder.append(val <= 9 ? (char)('0' + val) : (char)('a' + val - 10));
    }
    return builder.toString();
}

四、位运算实战操练

尝试取理解位运算的各种应用,以理解代替死记硬背

1、清除x最低位的1

清除x最低位的1:x & (x - 1)

我们假设x的补码,并找到最低位的1(其中a可以是1或0)

  • x的补码是 aaaaaaaa aaaaaaaa aaaaaaaa aaaa1000
  • x - 1的补码是 aaaaaaaa aaaaaaaa aaaaaaaa aaaa0111
  • x & (x - 1) 是 aaaaaaaa aaaaaaaa aaaaaaaa aaaa0000

也就是利用x-1x低位的1和0恰好错开,使得&只能得到0
1)例题一:LeetCode 191 位1的个数
只要n != 0,则它的补码中还有1。我们不断删除最低位的1,每删一次就记录一次,直到n = 0

public int hammingWeight(int n) {
    int ans = 0; 
    while (n != 0) { // 只要n比0大,就还有1
        // 清除最低位的1
        n &= (n - 1);
        ans++;
    }
    return ans;
}

2)例题二:LeetCode 231 2的幂
如果一个数是2的幂次方,它得是一个正数,并且它的补码中只有1个1,也就是说,清除最低位的1后应该为0

public boolean isPowerOfTwo(int n) {
    // 清除最低位的1
    return n > 0 && (n & (n - 1)) == 0;
}

3)实战题目:
LeetCode 338 比特位计数

2、获取 / 修改第n位

获取第n位:(x >> n) & 1
将第n位置1:x | (1 << n),利用|“有1则为1”的性质
将第n位置0:x & (~(1 << n)),利用&的性质,1 & 1或0 = 1或0(不变)0 & 1或0 = 0
将第n位取反:x ^ (1 <<n),利用^的性质0 ^ x = x

我们假设x = 20

  • 20的补码是 00000000 00000000 00000000 00010100
  • 设n = 7,1 << n 的补码是 00000000 00000000 00000000 10000000
  • 设n = 2,1 << n 的补码是 00000000 00000000 00000000 00000100
  • 设n = 2,~(1 << n)的补码是 11111111 11111111 11111111 11111011

1)例题一:LeetCode 190 颠倒二进制位
ans不断左移并加上获取的n的每一位即可,类似于LeetCode 7 整数反转

public int reverseBits(int n) {
    int ans = 0;
    for (int i = 0; i < 32; i++)
    	// n >> i & 1 得到尾数是否为1
        ans = (ans << 1) + (n >> i & 1);
    return ans;
}

2)实战题目:
LeetCode 52 N皇后Ⅱ
LeetCode 36 有效的数独
LeetCode 37 解数独

实战会偏向于硬核,将第n位置0置1通常用在代替boolean

3、异或

0 ^ x = x,x ^ x = 0

1)例题一:LeetCode 136 只出现一次的数字
由于其余数字均出现两次,利用x ^ x = 0可以很快得到答案

public int singleNumber(int[] nums) {
    int ans = 0;
    for (int x : nums)
        ans ^= x;
    return ans;
}

2)实战题目
LeetCode 260 只出现一次的数字Ⅲ

4、高级用法

由于用的次数不多,作为了解理解一下即可

lowbit:x & -x
将n位低位置0:x & (~0 << n)
将n位高位置0:x & (~0 >> n)

lowbit在树状数组中出现,可以参看我的上一篇博客:数据结构:树状数组:姐来展示下什么叫高端前缀和

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值