【C/C++算法】从浅到深学习--- 位操作算法(图文兼备 + 源码详解)

绪论:冲击蓝桥杯一起加油!!

每日激励:"不设限和自我肯定的心态:I can do all things。 --- Stephen Curry"
**绪论​:
今天总结了下位操作中常见的使用的方法,并且附加许多训练,通过方法 + 训练,位操作算法就将有很大的提升,后续将持续更新前缀和算法。敬请期待~

早关注不迷路,话不多说安全带系好,发车啦(建议电脑观看)。**


位运算

首先了解基本的位运算操作(必备):

  1. <<:左移,所有位数左移(如:1111 << 1 = 1110)
  2. >>:右移,所有位数右移(如:1111 >> 1 = 0111)
  3. ~:取反,将所有位数全部取反(0 -> 1/ 1 -> 0)、如: ~1111 = 0000
  4. 至于 & 、| 、^ 看下图左边运算(001 与 011进行位运算操作,得到的结果)
  5. &:按位与,有0就是0
  6. |:按位或,有1就是1
  7. ^:按位异或,相同为0,相异位1(或者可以看成无进位的加法)
  • 约定下标从右往左的0开始:

  • 这样某位置移动到0位置的移动值就是自己的下标

  • 这里的下标,并不是数组中的真下标,而是我们在记忆位数时它的位置的标识,从0开始到31(并且注意是从右往左的记忆)

  • 对于位运算的优先级,就不记忆了,就经量使用括号

常用的基础位运算操作

  1. 判断一个数的二进制中第x位为0/1
    1. 方法很简单 结合上面的>> 、 &位运算符
    2. 其中注意在位运算中 会经常用到 1,来辅助位运算(因为他的二进制中只有1位:000... 0001,这样就能通过这一位进行操作,也能避免很多复杂问题)
    3. 此处的话我们可以将 某个数进行右移>>x位,再 按位与x>> 上1(因为对与&来说,有0就是0,这样的话,按位与上1的话,若第x位上是0则为0,反之为1的话和按位与的1都没有0,就为1,而对于其他位来说不同管因为1的除了第一位为1其他都是0,按位与后都为0,所以结果只用两种可能 000...0000 / 000...0001)
    4. 具体如下图假设
    5. n为:...0110101001、
    6. 1: ...000...0001
    7. 通过将n右移x位,将x位的值移动第一位,在与000...01进行按位与,即可得到答案

对于后面的就不再细讲了,建议写草稿模拟情况就能很好的理解了,具体有问题可以评论。

  1. 将数 n 的二进制修改成1:n |= (1 << x)

训练题目:leetcode 191 338 461

  1. 将一个数的二进制表示的第x位修改成 0:n &= (~(1<<x))
  2. 位图思想:位图中我们就是通过 位操作进行管理其中的数据,位图内部主要通过0/1来判断该数据是否存在,我们可以用于存储一已知的容器数据但它访问速度可能较慢,那么可以将他在存放到位图中,这样通过 常量级的位操作就能快速的知道数据的状态。
  3. 提取一个二进制数中的最右侧的1:n &= -n
  4. 干掉二进制数中最右侧的1:n &= (n-1)
  5. 一些按位异或的运算律
    1. a ^ 0 = a
    2. a ^ a = a
    3. a ^ b ^ c = a ^ (b ^ c)
  6. 对于a ^ b ^ c来说他的本质是:可以理解为按位异或操作是有交换律的,我们能同时的看三个数,它们的顺序无所谓,那么也就能看成:抵消1的形式快速的算出答案(具体如下图)

相关训练:LeetCode136、260

如何将 a ^ b 划分出来

其中就260比较特别和困难:这里讲一下

  1. 想通过 按位与操作得到两个单独的数据 a ^ b
  2. 现在需要将他给区分开
  3. 对于 a ^ b来说,若要区分可以通过找1,因为按位异或的本质是:相同为0,相异为1
  4. 所以可以找到第一个不同的地方,也就是最右边的1
  5. 通过这个1的进行区分两个数,并将这两个数给分别通过按位异或的方式放到一个数组中
  6. 这样最终就能将这两个数放到两个数组中,也就能单独获得了
cpp 复制代码
class Solution {
public:
    vector<int> singleNumber(vector<int>& nums) {
        int res = 0;
        for(auto c :nums){
            res ^= c;//利用到 res ^ res = 0 & res & 0 = res 这两个按位异或技巧
            //当res 在按位异或的过程中会将出现两次的给异或为0,而res 与 0 异或则还是res
        }
        //此时因为会有两个单独的值,所以res = a ^ b 
        // 1001
        // 0011
        // 1010:a ^ b
        //此时找到 a ^ b 的最右边的1(这个代表他们不同的位)
        //通过这个区分 a b 这两个数,然后再次遍历nums中的元素
        //不过此时加上条件:通过 右移最右边这个数的下标i,后判断是否为1进行区分两数,将两数有分成了两种情况
        //这样 对于 右移最右边这个数的下标i 为1 / 不为1 中,肯定最终只会留下 a 和 b(因为其他的还是重复的然后抵消了)
        int i = 0;
        for(; i < 32;i++){
            if(((res >> i) & 1) == 1){
                break;//代表找到最右边的1了
            }
        }

        int res1 = 0,res2 =0;
        //得到i的位置
        for(auto c : nums){
            if((c >> i) & 1 == 1){
                //一份是:右移i位等于1的,同时也是 a ^ b中的某一位
                res1 ^= c;
            }
            else{
                res2 ^= c;
            }
        }
        vector<int> v{res1,res2};
        return v;
    }
};

将上面8个常见的情况记住并练习,后在进行下面的练习进一步巩固

具体训练:

1. 判定字符是否唯一

题目:

分析题目并提出,解决方法:

  1. 本题不难想到,通过一个hash来快速的确定元素出现的个数

  2. 但题目给了一个小小的提升:能不能不使用额外的空间

  3. 那么本题仅仅26位,我们是不是可以通过位图的思想:用一个整形来直接代替hash

  4. 结合前面的位操作,实现判断某个字符是否已经存在过

  5. 加上鸽巢原理:当字符串长度超过 26 时必定会有重复的字符!!

  6. 修改x位为1:n|= (1 << i)

题解核心逻辑:

cpp 复制代码
class Solution {
public:
    bool isUnique(string astr) {

        if(astr.size() > 26 ) return false;//利用鸽巢原理进行优化
        //使用位图 代替hash
        int bit = 0;

        for(auto c : astr){
            int i = c - 'a';//得到字符的位置: 'a' - 'a' = 0、'b' - 'a' = 1
            //判断字符是否已经出现过:
            if((bit >> i & 1) == 1){
                //进来代表第 i 位为1,并且代表已经出现过一次了
                //所以就错误了,直接返回
                return false;
            }
            bit |= (1 << i);//将第i位设置为 1
        
        }
        return true;
    }
};

2. 丢失的数字

题目:

分析题目并提出,解决方法:

方法1:hash(空间复杂度:O(N))

  1. 先遍历填充hash
  2. 在遍历hash看哪个位置空了

高斯求和法:((首项 + 尾项) * 项数)/ 2 等于所有元素的和,再减去数组中的和,这样就能得到缺失的和(空间复杂度:O(1))

方法3(本题解法):位运算 a ^ a = 0(空间复杂度:O(1))

题解核心逻辑:

本题就主要写位运算的操作:

cpp 复制代码
class Solution {
public:
    int missingNumber(vector<int>& nums) {
        int n = nums.size();
        int res = 0;
        for(int i = 0; i <= n;i++){
            res ^= i;//得出 从 0 ~ n 的所以情况 0 ^ 1 ^ .... ^ n
        }
        for(auto i : nums){
            res ^= i;// res ^ nums = 缺失的
        }
        return res;
    }
};

3. 两整数之和

题目:

分析题目并提出,解决方法:

对于这种不能使用 + - 法的一般就是位运算了

  1. 首先回顾之前的按位异或操作又称无进位相加

按位异或的无进位相加

  1. 那么就需要找到他的进位值:发现当 a & b 时就能表示他是否需要进位(下图右边)

a & b 查看否需要进位

  1. 但这只是找到了他是否要进位的地方,我们还需要将他左移 1 位 从而完成进位后的低位++
  2. 然后再一次进行加法操作(按位异或:无进位加法)将进位值加上得到最终值
  3. 但注意:此时还需要再一次判断是否还有进位的地方:
    1. 若为0则表示不存在进位了
    2. 若不为0则代表仍然有进位,那么就需要再次重复2、3、4、5步(下图黑框框)

题解核心逻辑:

cpp 复制代码
class Solution {
public:
    int getSum(int a, int b) {
        //使用 ^ 的无进位相加
        //再使用 & 判断是否要进位(为0则代表不进位、不为0则代表有进位)
        while((a & b) << 1){
            int tmp = a;
            a = a ^ b;
            b = (tmp & b) << 1;
        }
        return a ^ b;
    }
};

4. 只出现一次的数字 II

题目:

分析题目并提出,解决方法:

题解核心逻辑:

cpp 复制代码
class Solution {
public:
    int singleNumber(vector<int>& nums) {
        //将 nums 中的每个数的每一位 都相加并最终 模3,最终得到的每一位就是 只出现一次的每一位:为0 就是0,为1就是1
        //此处 还能 %n 这样就能 看出就能排除重复出现n次 得到只出现1次的
        int res = 0;
        // n * 32
        for(int i = 0 ; i < 32;i++){
            //使用一个sum计算,判断在该位置的和
            int sum = 0;
            //3n0 + 0 = 0 -%3-> 0
            //3n0 + 1 = 1 -%3-> 1
            //3n1 + 0 = 3n -%3-> 0  
            //3n1 + 1 = 3n + 1 -%3-> 1  
            for(auto c : nums){
                //判断c位置上 i 下标下的值
                if(((c >> i) & 1) == 1){//判断是否为1,为1则++
                    sum++;
                }
            }
            sum %= 3;//判断只出现一次的那个数的下标
            if(sum == 1){
                res |= (1 << i);//给结果下标添加算出来的值 
            } 
        }
        return res;
    }
};

5. 消失的两个数字

题目:

分析题目并提出,解决方法:

分析题目不难通过前面的铺垫想出:

  1. 通过创建下同大小的数组与缺失数组进行 按位异或 操作,最终会得到缺失两个数的 a ^ b
  2. 此时通过 a ^ b 找到它最后一位1的位置
  3. 将原数组和缺失数组都分成两份,并且这两份中会各自有a和b(获取没a和b)
  4. 这样再将分开的数组 相互 按位异或 就得到了分别缺失的两个a和吧

题解核心逻辑:

cpp 复制代码
class Solution {
public:
    vector<int> missingTwo(vector<int>& nums) {
        int n = nums.size() + 2;
        // 创建原数组,这里本质不用创建,直接不断 ^ 存储即可!
        int sum = 0;
        for(int i = 1 ; i <= n ;i++){
            sum ^= i;
        }

        //先与缺数数组相 按位异或
        int absent_value = sum;
        for(auto c : nums) {
            absent_value ^= c;
        }

        //得到了 a ^ b 缺失的值,现在找到第一个不同的位置
        int i = 0;
        for(; i < 32 ;i++){
            if((absent_value >> i) & 1  == 1){
                break;
            }
        }

        //找到了i的位置,将原数组和缺失数组再次分成两份,再分别相异或
        vector<int> arr(2,0);
        for(int j = 1;j <= n;j++){
            if(((j >> i) & 1) == 1){
                arr[0] ^= j;
            }else{
                arr[1] ^= j;
            }
        }

        for(auto c : nums) {
            if(((c >> i) & 1)  == 1){
                arr[0] ^= c;
            }else{
                arr[1] ^= c;
            }
        }

        return arr;
        
        return arr;
    }
};
相关推荐
New_Teen6 分钟前
C++小课堂——变量的声明,赋值和初始化
开发语言·c++·笔记·学习
练习&两年半16 分钟前
C语言:51单片机 程序设计基础
c语言·开发语言·单片机·51单片机
_Itachi__26 分钟前
LeetCode 热题100 21. 合并两个有序链表
算法·leetcode·链表
努力努力再努力wz38 分钟前
【Linux实践系列】:用c语言实现一个shell外壳程序
linux·运维·服务器·c语言·c++·redis
Watink Cpper1 小时前
[MySQL初阶]MySQL(1)MySQL的理解、库的操作、表的操作
linux·运维·服务器·数据库·c++·后端·mysql
李白同学1 小时前
C++:类和对象(下篇)
开发语言·c++
北顾南栀倾寒2 小时前
[杂学笔记]迭代器的原理、进程与线程的区别、.vector的内存管理、vim的命令模式指令集合、多线程的最大问题、HTTP协议与HTPPS协议区别
开发语言·c++·笔记·http·vim
萌の鱼2 小时前
leetcode 240. 搜索二维矩阵 II
数据结构·c++·算法·leetcode·矩阵
虾球xz2 小时前
游戏引擎学习第131天
c++·学习·游戏引擎
@心都2 小时前
机器学习数学基础:37.统计学基础知识1
人工智能·算法·机器学习