硅基计划4.0 算法 位运算

硅基计划4.0 算法 位运算


1752388689929



一、回顾

我们一般所见到的位运算符有三个
以下是针对两个数比特位比较的时候的运算法则

  • &:有0结果就是0
  • |:有1结果就是1
  • ^:互为0和1结果就为1,或者说是无进位相加

无进位相加,即不会进行二进制的加法进位,比如:
1 1 0
1 0 0
———
0 1 0

说明:我们以后说的第0位、第1位等等,指的都是一个数的比特位最右边一位是第0位,往左边数一位是第二位,一直到最左边

位图思想:我们之前都是使用哈希表去存储数据,但是我们现在可以使用一个变量的比特位去存储数据,也跟哈希表类似的结构

1. 求一个数二进制位第x位是0还是1

我们仅需让目标数右移x位,然后&1即可,比如我们求1 0 0 1第4位是否是1:
右移4位:0 0 0 1,然后

0 0 0 1
0 0 0 1
————
0 0 0 1

我们就可以得出结果,我们利用率二进制不是0就是1的特性,结合&运算符特性

2. 将一个数二进制位第x位修改为1

比如1 0 0 1这个数,我们让其第二位修改成1,我们可以使用|运算符

1 0 0 1
0 1 0 0
————
1 1 0 1

我们利用|操作符特性运算,对于0 1 0 0这个数,我们可以通过让1左移x位得到

3. 将一个数二进制位第x位修改为0

比如1 0 0 1这个数,我们让其第三位修改成0,我们可以使用&运算符

1 0 0 1
0 1 1 1
————
0 0 0 1

我们利用&操作符特性运算,对于0 1 1 1这个数,我们可以让1左移x位然后取反即可

4. 提取一个数二进制位最右侧的1

我们仅需让这个数&它自己的负数就好
而负数需要将原来的数按位取反后加上1
比如原二进制数0 1 1 1 0 1 0 1 0 0 0,按位取反后1 0 0 0 1 0 1 0 1 1 1
然后加上1后1 0 0 1 0 1 1 0 0 0,你可以发现这个二进制数被我们划分出了两个区域
以第四位为分界线,左侧都是跟原来的二进制位相反的结果1 0 0 1 0 1
而右侧包括第四位是跟原来二进制位保持一致的结果1 0 0 0
因此我们的结果就是0 1 1 1 0 1 0 1 0 0 0 & 1 0 0 1 0 1 1 0 0 0 = 0 0 0 0 0 0 0 1 0 0 0
本质上就是将最右侧的1的左侧区域(不包括自己)变成相反的数位

5. 删去一个数二进制位最右侧的1

我们继续让这个数& (num-1)
比如原二进制数0 1 1 0 1 0 1 0 0,而-1本质上是借位运算
对应原二进制数,要不断向左边借1来运算,当借到最右侧的1的时候就停止借位
0 1 1 0 1 0 0 1 1,然后0 1 1 0 1 0 1 0 0 & 0 1 1 0 1 0 0 1 1 = 0 1 1 0 1 0 0 0 0

6. ^操作符

a ^ 0 = a a ^ a = 0//消消乐,a ^ b ^ c = a ^ (b ^ c)//结合律 为什么说^`是无进位相加,因为它可以把两个1继续抵消

1 0 1 1 0 1 0
0 0 1 0 1 0 1
1 0 1 0 0 0 1
————————
0 0 1 1 1 1 0

而且^还可以用在判断出现奇数次的数,因为偶数次的数都会被抵消掉

二、判断字符是否唯一

题目链接
我们可以利用位图思想,我们使用一个变量,用不同的比特位表示不同字符是否重复出现
用0代表未出现过,1代表出现过,类似于哈希表
image-20250822195340880

还有,根据鸽巢原理,如果一个字符大于26长度(对应26个字母),肯定存在重复字符

class Solution {
    public boolean isUnique(String astr) {
        int ret = 0;
        int length = astr.length();
        if(length > 26){
            return false;
        }
        for(int i = 0;i<length;i++){
            //确定位置
            int pos = 'a'-astr.charAt(i);
            if(((ret >> pos) & 1) == 1){
                //判断是否出现过1次,即二进制位是否是1
                return false;
            }
            //数据存入位图中
            ret = ret | (1 << pos);
        }
        return true;
    }
}

三、丢失的数字

题目链接
这一题我们可以观察到题目中的数组缺失了一个数,而一个数组中存在从0~n的数,因此我们可以根据数组长度描绘出真正的原数组
然后让它们一起^,根据其规则,没有匹配到的就是缺失的数字
比如[0,1,3,5,4] 真正原数组[0,1,2,3,4,5] 然后一起^得2

为什么^不会丢失数据?
因为^是进行累计运算,并非逐个比较,根据交换律进行配对结合,对应中间的结果并不重要,并且^本身就是线性运算

class Solution {
    public int missingNumber(int[] nums) {
        int length = nums.length;
        int [] hash = new int[length+1];
        for(int x:nums){
            hash[x]++;
        }
        for(int i = 0;i<length+1;i++){
            if(hash[i] == 0){
                return i;
            }
        }
        return -1;
    }
}

四、两数之和

题目链接
还记得我们之前说的^运算符吗,它是无进位的相加
那我们是不是只需要找到进位,然后把进位加上,就是真正的加法啦

如何找到进位?我们知道。两个二进制位为1相加才会产生进位
我们仅需要a&b,找出两个数二进制位都是1的地方
由于要进位,因此我们a&b的结果要左移一位

然后去看看结果的二进制位是否全是0
如果不是则要继续进位
我们举个例子来演示

0 0 1 1 0 1 13<–a
0 1 1 1 0 0 18<–b
——————————
0 1 0 0 0 1 <–a^b
0 1 1 0 0 0 <–(a&b)<<1

可以看到(a&b)<<1结果并不是全是0,可以看到第三位和第四位的二进制位是1,说明a,b两个数字的第三位和第四位的二进制位产生进位

因此我们让a^b的结果作为新的a(a&b)<<1的结果作为新的b,继续运算

0 1 0 0 0 1 <–a
0 1 1 0 0 0 <–b
——————————
0 0 1 0 0 1 <–a^b
1 0 0 0 0 0 <–(a&b)<<1

可以看到此时我们a&b的结果的二进制位还是存在1,继续运算

0 0 1 0 0 1 <–a
1 0 0 0 0 0 <–b
——————————
1 0 1 0 0 1 <–a^b
0 0 0 0 0 0 <–(a&b)<<1

此时我们(a&b)<<1的结果的二进制位全是0,运算结束
此时a^b的结果就是我们想要的结果

class Solution {
    public int getSum(int a, int b) {
        while(b != 0){
            int ret1 = a^b;
            int ret2 = (a&b)<<1;
            a = ret1;
            b = ret2;
        }
        return a;
    }
}

五、只出现一次的数字

题目链接
这题我们通过题目可以知道,有一个是只出现了一次,其他数都是3n次
那么我们可以对于所有数的每一个比特位进行累加,然后%3,看看结果是不是0
为什么,每一个比特位的总和(注意是每一个比特位)有以下四种情况

3n个0+0 3n个0+1 3n个1+0 3n个1+1
0 1 3n 3n+1 <–当前比特位求和∑
0 1 0 1 <–当前比特位求和后%3
其中3n+的数就是我们要找的那个数其中的一个比特位

我们看到我们得出的结果的比特位和我们要找的那个数的比特位一模一样
因此我们可以求出所有的数的第0比特位然后%3,结果是0就是0,是1就是
接着在求出所有数的第1比特位然后%3,同样结果是0就是0,是1就是1

class Solution {
    public int singleNumber(int[] nums) {
        int ret = 0;
        for(int i = 0;i<32;i++){//每一个比特位求和
            int sum = 0;
            for(int x : nums){
                if(((x >> i) & 1) == 1){
                    sum++;
                }
            }
            sum %= 3;
            if(sum == 1){
                ret |= (1 << i);
            }
        }
        return ret;
    }
}

六、消失的两个数字——Hard

题目链接
如果数组大小是1,则数组中应该包含1~3这三个数
即如果数组大小是n,要包含1~n+2的数

这一题可以结合消失的数字那道题
image-20250822203612971
不存在异或结果是0的情况,题目中有说明
好,我们可以得到结果tmp = a^b,但问题是怎么把ab分开

因为两个数字的差值是1,因此它们的比特位实际上只有一位不同,即两个数的二进制位中有一对比特位是互为01的,因此我们要找到这个位置,我们记录成第x位

根据这个比特位的不同,我们可以把原来的看成整体的数划分成两个区间

  • a+第x位为0的数,内部进行异或
  • b+第x位为1的数,内部进行异或

最后我们就可以得到ab两个结果,完毕

class Solution {
    public int[] missingTwo(int[] nums) {
        //找出两个数a^b
        int tmp = 0;
        for(int x : nums){
            tmp ^= x;
        }
        for(int i = 1;i<=nums.length+2;i++){
            tmp ^= i;
        }
        //tmp = a^b
        
        //分离a和b,先找到互为0和1的位置
        int diff = 0;
        while(true){
            //不用担心死循环,是一定会找到结果的
            if(((tmp >> diff) & 1) == 1){
                break;
            }else{
                diff++;
            }
        }
        
        //分组,创建结果数组
        int [] ret = new int[2];
        for(int x:nums){
            if(((x >> diff) & 1) == 1){
                ret[1] ^= x;
            }else{
                ret[0] ^= x;
            }
        }
        for(int i = 1;i<=nums.length+2;i++){
            if(((i >> diff) & 1) == 1){
                ret[1] ^= i;
            }else{
                ret[0] ^= i;
            }
        }

        return ret;
    }
}

希望本篇文章对您有帮助,有错误您可以指出,我们友好交流

END
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值