算法:位运算类型题目练习与总结

前言

这几天连着做了不少位运算的题目,那么这篇博客,就来总结一下这些位运算的题目吧。

位运算的常用小技巧

基础位运算

按位与 & :有0为0,无0为1

按位或 | :有1为1,无1为0

按位异或 ^ :相同为1,不同为0

按位取反 ~ :0变成1,1变成0

左移 << : 二进制序列向左移

右移 >> :二进制序列向右移

判断一个数n的二进制表示中的第x位是0还是1

n & (1 << x)

n >> x 将n的第x位放到最右边第0位

与1 按位与(1的二进制序列为000000...... 1)

即可判断第x位是0还是1

将一个数n的二进制表示中的第x位变成1

n |= (1 << x)

要将第x位变成1,需要第x位按位或1

1 << x 造出了第x位为1,其他位为0的二进制序列

将一个数n的二进制表示中的第x位变成0

n &= ~(1 << x)

要将第x位变成0,需要第x位按位与0

1 << x 造出了第x位为1,其他位为0的二进制序列

对该序列取反,就早出了第x位为0,其他位为1的二进制序列

提取一个数n的二进制表示中的最右边的1

n & (-n)

-n 的本质是将一个二进制数最右边的1的左边区域全部变成相反的数

-n = ~n + 1

eg:

n = 11010001010101100000

-n = 00101110101010011111 + 1

= 00101110101010100000

干掉一个数n的二进制表示中的最右边的1(变成0)

n & (n - 1)

n - 1 的本质是将一个二进制数的最右边的1的右边区域(包括自己)全部取反

eg

n = 11010001010101100000

n - 1 = 11010001010101011111

按位异或(^) 运算律

a ^ a = 0

a ^ 0 = a

a ^ b ^ c = a ^ c ^ b

题目练习

题目一:判定字符是否唯一

题目

链接:

判定字符是否唯一

思路分析

大家第一眼,这不就hash吗,一分钟搞定,

but,请看限制的第三点:如果不使用额外的数据结构

我们来想想,不使用额外的数据结构,

emm,那就来暴力查找,绷不住了,肯定要想一个时间复杂度优秀的算法啊!

OK,正式开始解决这道题目:

要想时间复杂度优秀,必然是要使用某些数据结构来帮忙的,但是又不允许使用额外的数据结构,

所以,我们可以尝试使用位图的思想

我们把一个比特位作为信息的载体,一个比特位可以存0或1,

我们就用0或1来代表字符串中的字符有没有在之前出现过,

这不就相当于模拟实现了一个hash吗?

一旦想到了这个思想,那这道题就无比简单了

细节:这道题目中只有小写字母,也就是只会出现26种情况,那一个int32位肯定足够存储,

所以位图就选择int,

a --- 第0位

b --- 第1位

......

依次类推即可

具体代码

c 复制代码
    bool isUnique(string astr) {
        int a = 0;
        for(auto& e:astr)
        {
            int i = e - 'a';
            cout << i << endl;
            cout << (a & (1 << i)) << endl;
            if(a & (1 << i)) // 判断第i位是否为1
            {
                return false;
            }
            else
            a |= (1 << i); // 注意这个地方,我想要修改,肯定是 |= 而不是|
        }

        return true;
    }

题目二:丢失的数字

题目

链接:

丢失的数字

思路分析

经典位运算的题目 --- 单身狗。

直接根据按位异或的规律,就可以很简答的解决这道题目。

a ^ a = 0

a ^ 0 = a

a ^ b ^ c = a ^ c ^ b

我们把0~n的所有数字和数组里面的所有数字,全部按位异或一遍,最后得到的结果就是丢失的数字。

具体代码

c 复制代码
int missingNumber(vector<int>& nums) {
        int n = nums.size();
        int ret = 0;
        for(int i = 0;i < nums.size();++i)
        {
            ret ^= i;
            ret ^= nums[i];
        }

        return ret ^= n;
    }

题目三:137. 只出现一次的数字 II

题目

链接

137. 只出现一次的数字 II

思路分析

上一道题是单身狗最基础的版本,现在这道题来了一个单身狗的进阶版本。

那么这道题目该怎么做呢?

观察题目,我们可以发现:
3次,1次,那么所有数字的同一位上加起来也肯定是3的倍数啊,
如果不是三个倍数,说明啥,说明1次的这个数字的这一位上也被加进来了,也就是说,出现一次的这个数字的该位上是1,

所以,现在,思路已经很明确了,就是把每一位上1出现的次数全部加起来,看看是不是三的倍数,如果不是三的倍数,就把该位设置为1,

也就是 x |= (1<<pos)

最后遍历完32位,就可以得出只出现一次的这个数字了。

具体代码

c 复制代码
int singleNumber(vector<int>& nums) {
        int ret = 0;

        for(int i = 0;i < 32;++i)
        {
            int sum = 0;
            for(auto& e:nums)
            {
                if(e & (1 << i)) sum++;
            }

            //说明这一位是仅出现一次
            if(sum % 3 != 0) ret |= (1 << i);
        }

        return ret;
    }

题目四:消失的两个数字

题目

链接

消失的两个数字

思路分析

诶,单身狗又升级了,单身狗2.0版本来了。

这道题又很麻烦了,如果我依旧是把所有的元素全部按位异或,那最后得到的结果不就是两个数的按位异或值吗?

这咋分开啊?

不用担心,肯定是有办法的!

最后两个数肯定是不相同的,所以最后剩下的异或值一定不是0,也就是说最后剩下的异或值最少有一位上是1,

这说明啥?
说明最后的两个数该位上的比特位值不相同,一个0,一个1,

这有啥用啊?

不是很显然吗?

我们就可以根据这一位上的不同,把所有的元素分为两个部分,

每一个部分都仅有一个元素只出现了一次,这不就又回到了基础单身狗吗?

再按位异或一次呗。

具体代码

c 复制代码
    vector<int> missingTwo(vector<int>& nums) {
        if(nums.size() == 0) return {1,2};

        int n = nums.size() + 2;
        int tmp = 0;
        for(int i = 0;i < nums.size();++i)
        {
            tmp ^= (i + 1);
            tmp ^= nums[i];
        } 

        tmp ^= n;
        tmp ^= (n - 1);
        
        //首先获得特殊的两个数字的异或值,就能知道两者哪一位不同
        //进而就能把数据分为两类
        int pos = 0;
        for(int i = 0;i < 32;++i)
        {
            if(tmp & (1 << i) ) 
            {
                pos = i;
                break;
            }
        }
        cout << pos << endl;

        int ret1 = 0,ret2 = 0;
        for(int i = 0;i < nums.size();++i)
        {
            if((i + 1) & (1 << pos)) ret1 ^= (i + 1);
            else ret2 ^= (i + 1);

            //i和1不要写反了
            if(nums[i] & (1 << pos)) ret1 ^= nums[i];
            else ret2 ^= nums[i];
        }

        if(n & (1 << pos)) ret1 ^= n;
        else ret2 ^= n;
        
        if((n-1) & (1 << pos)) ret1 ^= (n-1);
        else ret2 ^= (n-1);

        return {ret1,ret2};
    }

题目五:371. 两整数之和

题目

链接

371. 两整数之和

思路分析

这道题目,说实话得对硬件比较了解,

要是笔试真遇到了,同学们还是见仁见智吧,直接不讲武德,return a + b;

好了,开个玩笑,玩归玩,闹归闹,真到了学习的时候,还是认真学习吧。

首先,我们思考一下,按位异或扮演了一个什么样子的角色呢?

1 ^ 1 = 0,

0 ^ 0 = 0,

1 ^ 0 = 1,

0 ^ 1 = 1

嗯?得到的结果不就是不考虑进位的结果吗?

所以按位异或的本质就是无进位相加。

a ^ b,也就获得了无进位相加的信息。

于是,我们现在有了加法了,但是我们还缺少进位信息,这咋办。

别慌,我们再来看看&运算

1 & 1 = 1,

0 & 0 = 0,

1 & 0 = 0,

0 & 1 = 0

进位这不就来了吗?只有两位都是1,才得到了1,这个1就是进位的1,

当然,由于是进位,所以,肯定要往高位上加,

所以 (a & b) << 1,也就获得了进位信息

直接 无进位相加的信息和进位信息一起相加,不就是最后的结果吗?

但是还是没有加法啊,所以"进位相加的信息和进位信息一起相加" 中的相加,依然是^ 和 &配合执行,

直到最后没有进位信息,就得到了真正的结果。

具体代码

c 复制代码
    int getSum(int a, int b) {
        while(b)//用b来存储进位信息
        {
            int carry = (a & b) << 1;//获取进位信息,并保存
            a ^= b;//无进位相加,用a来存储结果
            b = carry;
        }

        return a;
    }
相关推荐
百***97642 小时前
【语义分割】12个主流算法架构介绍、数据集推荐、总结、挑战和未来发展
算法·架构
代码不停2 小时前
Java分治算法题目练习(快速/归并排序)
java·数据结构·算法
bubiyoushang8883 小时前
基于MATLAB的马尔科夫链蒙特卡洛(MCMC)模拟实现方法
人工智能·算法·matlab
玖剹3 小时前
穷举 VS 暴搜 VS 深搜 VS 回溯 VS 剪枝
c语言·c++·算法·深度优先·剪枝·深度优先遍历
李兆龙的博客3 小时前
从一到无穷大 #57:Snowflake的剪枝方案
算法·剪枝
啊我不会诶4 小时前
01BFS学习笔记
笔记·学习·算法
Ch_ty4 小时前
leetcode解题思路分析(一百六十八)1452 - 1458 题
算法·leetcode·哈希算法
哼?~4 小时前
算法学习--离散化
算法
AI科技星4 小时前
引力编程时代:人类文明存续与升维
数据结构·人工智能·经验分享·算法·计算机视觉