【算法:C++】从零掌握位运算:十大经典问题详解

从零掌握位运算:十大经典问题详解

简单介绍和计算:

1.基础位运算

包括:<< >> ~ & | ^ 这些运算符的使用方法

2.给一个数n,确定它的二进制表示中的第x位是0还是一
if((n >> x) & 1)	return 1;
else return 0;
3.将一个数的二进制表示中的第x位修改为0或1
//修改为0
n = n & (~(1 << x))
//修改为1
n = n | (1 << x)
4.位图的思想

将一个变量的每一个二进制位看成一个标志位,这就像STM32中的寄存器一样,每一位存放不同的数代表不同的状态

5.提取一个数(n)二进制中最右边的1
n & (-n);

示例:

 0110101000//n
 1001010111//n的反码
 1001011000//加1之后的补码
&0110101000
 0000001000
6.干掉一个数(n)二进制表示中最右侧的1
n & (n - 1);
7.位运算的优先级

对于位运算的优先级,能用括号就用括号

9,异或运算
//1. a ^ 0 = a;
//2. a ^ a = 0;
//3. a ^ b ^ c = a ^ (b ^ c);

概念题

位1的个数

题目链接:

位1的个数

要点:

  • 核心目标:统计二进制表示中1的个数。
  • 位操作技巧:逐位检查最低位是否为1,然后右移消去已处理的位。

我的代码:

class Solution {
public:
    int hammingWeight(int n) {
        int sum = 0;
        for(int i = 0; i < 31; i++)
        {
            if(n & 1) sum++;
            n = n >> 1;
        }
        return sum;
    }
};

我的思路:

  • 循环处理:遍历32位(int类型位数),每次取最低位判断是否为1。
  • 优化点:使用n & 1代替取余操作,效率更高。

我的笔记:

  • 易错点:未处理负数时可能陷入死循环(但题目约束为无符号整数)。
  • 进阶方法:使用n &= n-1直接消去最低位的1,循环次数等于1的个数。
比特位计数

题目链接:

比特位计数

要点:

  • 动态规划优化:利用已知结果计算当前值(如i & (i-1)消除最低位的1)。
  • 核心公式bits[i] = bits[i & (i-1)] + 1

我的代码:

class Solution {
private:
    vector<int> arr;
public:
    vector<int> countBits(int n) {
        
        arr.push_back(0);
        for(int i = 1; i <= n; i++)
        {
            int sum = 0, j = i;
            while(j & (-j))
            {
                j &= j - 1;
                sum++;
            }
            arr.push_back(sum);
        }
        return arr;
    }
};

我的笔记:

  • 注意不能直接在while循环里面改写i,不然会死循环
  • 使用 j & -j 来判断 i 的二进制是否有1,用j &= j - 1来消除二进制位中最右边的1
汉明距离

题目链接:

汉明距离

要点:

  • 问题转化:两数异或后统计1的个数即为不同位的数量。
  • 异或性质x ^ y的结果中1的位数即为汉明距离。

我的代码:

class Solution {
public:
    int hammingDistance(int x, int y) {
        int z = x ^ y;
        int sum = 0;
        while(z & -z)
        {
            z &= z - 1;
            sum++;
        }
        return sum;
    }
};

我的笔记:

  • 位运算优化:用z &= z-1快速消去最低位的1,减少循环次数。
  • 代码简洁性:结合异或和消位操作,实现高效计算。
只出现一次的数组

题目链接:

只出现一次的数组

要点:

  • 异或性质a ^ a = 00 ^ a = a,异或所有元素后剩余值为唯一出现元素。

我的代码:

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int ret = 0;
        for(auto& x : nums)
        {
            ret ^= x;
        }
        return ret;
    }
};

我的笔记:

  • 线性复杂度:遍历数组一次,异或所有元素。
  • 局限性:仅适用于其他元素出现偶数次的情况。

算法题

判定字符是否唯一

题目链接:

判定字符是否唯一

要点:

  • 位图法:用26位二进制表示字符是否出现过(a-z映射到0-25位)。
  • 鸽巢原理:字符串长度超过26直接返回false

老师代码:

class Solution
{
public:
    bool isUnique(string astr)
    {
        // 利⽤鸽巢原理来做的优化
        if(astr.size() > 26) return false;
        
        int bitMap = 0;
        for(auto ch : astr)
        {
            int i = ch - 'a';
            
            // 先判断字符是否已经出现过
            if(((bitMap >> i) & 1) == 1) return false;
            
            // 把当前字符加⼊到位图中
            bitMap |= 1 << i;
        }
        return true;
    }
};

老师思路:

利⽤「位图」的思想,每⼀个「⽐特位」代表⼀个「字符,⼀个 int 类型的变量的 32 位⾜够表⽰所有的⼩写字⺟。⽐特位⾥⾯如果是 0 ,表⽰这个字符没有出现过。⽐特位⾥⾯的值是 1 ,表⽰该字符出现过。那么我们就可以⽤⼀个「整数」来充当「哈希表」。

我的代码:

class Solution {
public:
    bool isUnique(string astr) {
        if(astr.size() > 26) return false;

        int flag = 0;
        for(int i = 0; i < astr.size(); i++)
        {
            if(flag & (1 << astr[i] - 'a'))
            {
                return false;
            }
            flag |= 1 << astr[i] - 'a';
        }
        return true;
    }
};

我的笔记:

  • 使用位图的思想,其实就像是单片机里面的状态寄存器

  • 优化:鸽巢原理,如果整个字符串的大小大于26,就一定是有字母重复

  • 位操作

  • 关键点:ASCII字符范围限制为小写字母,故可用26位表示。

  • 扩展思考:若字符范围扩大(如Unicode),需改用哈希表。

丢失的数字

题目链接:

丢失的数字

要点:

  • 异或性质:将数组元素与0~n异或,结果即为缺失数字。
  • 高斯求和法:计算n(n+1)/2 - sum(nums)

老师代码:

class Solution
{
public:
    int missingNumber(vector<int>& nums)
    {
        int ret = 0;
        for(auto x : nums) ret ^= x;
        for(int i = 0; i <= nums.size(); i++) ret ^= i;
        return ret;
    }
}

老师思路:

设数组的⼤⼩为 n ,那么缺失之前的数就是 [0, n] ,数组中是在 [0, n] 中缺失⼀个数形成的序列。如果我们把数组中的所有数,以及 [0, n] 中的所有数全部「异或」在⼀起,那么根据「异或」运算的「消消乐」规律,最终的异或结果应该就是缺失的数

我的代码:

class Solution {
public:
    int missingNumber(vector<int>& nums) {
        int ret = 0;
        for(int i = 0; i < nums.size(); i++)
        {
            ret ^= i;//0 ~ n-1
            ret ^= nums[i];//nums[0] ~nums[n-1]
        }
        ret ^= nums.size();//n

        return ret;
    }
};

我的笔记:

  • 其他方法:

    高斯求和:先把0~n数相加,再依次减去数组中的元素,最后剩下的就是缺失的数字

    哈希表:不多赘述

    排序+遍历:将整个数组进行一次排序,之后从前往后遍历与数组下标作比较,出现第一个位置下标不一样就是缺失对应下标的数

两个整数之和

题目链接:

两个整数之和

要点:

  • 异或 ^ 运算本质是「⽆进位加法」;
  • 按位与 & 操作能够得到「进位」;
  • 然后⼀直循环进⾏,直到「进位」变成 0 为⽌。

老师代码:

class Solution
{
public:
    int getSum(int a, int b)
    {
        while(b != 0)
        {
            int x = a ^ b; // 先算出⽆进位相加的结果
            unsigned int carry = (unsigned int)(a & b) << 1; // 算出进位
            a = x;
            b = carry;
        }
    return a;
    }
}

老师这里用unsigned int是因为当a&b == -1时,我们对-1<<1的操作是无定义的,所以我们要对int类型强转为unsigned int

我不知道为什么我的代码没有出现这种情况

我的代码:

class Solution {
public:
    int getSum(int a, int b) {
        int x = 0;
        while(b)//当计算没有进位值时出循环
        {
            x = a;//b要用到a改变之前的值,这里保存一下
            a = a ^ b;//异或也就是无进位加1
            b = (x & b) << 1;//这一步就是用于求进位的
        }
        return a;
    }
};

我的笔记:

  • (x & b) << 1可以算出x和b的进位,注意一定要向前移动一位,这样在之后的异或操作和与运算的时候才是正确的
只出现一次的数字

题目链接:

只出现一次的数字

要点:

  • 位统计法:统计每位出现次数,模3后剩余位即为目标数。

  • 推广思路:若其他数出现n次,模n即可。

  • 只有一个数只出现一次,其余的数都在数组中出现三次

  • 算法的思想就是把数组中的每个数的每一个二进制位分别相加,之后再把这个相加之后的数%3就可以得到那个与众不同的数的二进制位了,之后再把它转换为int类型的数就行了说实话这个想法我是想不到的

老师代码:

class Solution
{
public:
    int singleNumber(vector<int>& nums)
    {
        int ret = 0;
        for(int i = 0; i < 32; i++) // 依次去修改 ret 中的每⼀位
        {
            int sum = 0;
            for(int x : nums) // 计算nums中所有的数的第 i 位的和
                if(((x >> i) & 1) == 1)
                    sum++;
            sum %= 3;
            if(sum == 1) ret |= 1 << i;
        }
        return ret;
    }
};

老师思路:

设要找的数位 ret 。 由于整个数组中,需要找的元素只出现了「⼀次」,其余的数都出现的「三次」,因此我们可以根据所有数的「某⼀个⽐特位」的总和 %3 的结果,快速定位到 ret 的「⼀个⽐特位上」的值是 0 还是 1 。 这样,我们通过 ret 的每⼀个⽐特位上的值,就可以将 ret 给还原出来

我的代码:

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int hash[32] = { 0 };//储存对应二进制位的和
        for(int i = 0; i < nums.size(); i++)
        {
            for(int j = 0; j < 32; j++)
            {
                hash[j] += (nums[i] & (1 << j)) ? 1 : 0;
            }
        }

        int ret = 0;
        for(int i = 0; i < 32; i++)
        {
            if(hash[i] % 3 == 1)
            {
                ret |= 1 << i;
            } 
        }
        return ret;
    }
};

我的笔记:

  • 代码比较好编写,就是思路我想不到
  • 通过这个思路,我们可以求比如一个数组中只有一个数只出现一次,其余的数都在数组中出现n次,我们就可以转换为%n
消失的两个数字

题目链接:

消失的两个数字

要点:

  • 异或分组法
    1. 异或所有元素和完整范围,得到a^b
    2. 找到ab不同的某一位,分组异或求解。

老师代码:

class Solution
{
public:
    vector<int> missingTwo(vector<int>& nums)
    {
    // 1. 将所有的数异或在⼀起
    int tmp = 0;
    for(auto x : nums) tmp ^= x;
    for(int i = 1; i <= nums.size() + 2; i++) tmp ^= i;
    
    // 2. 找出 a,b 中⽐特位不同的那⼀位
    int diff = 0;
    while(1)
    {
        if(((tmp >> diff) & 1) == 1) break;
        else diff++;
    }
    
    // 3. 根据 diff 位的不同,将所有的数划分为两类来异或
    int a = 0, b = 0;
    for(int x : nums)
        if(((x >> diff) & 1) == 1) b ^= x;
        else a ^= x;
    for(int i = 1; i <= nums.size() + 2; i++)
        if(((i >> diff) & 1) == 1) b ^= i;
        else a ^= i;
    return {a, b};
    }
}

我的代码:

class Solution {
private:
    vector<int> ret;
public:
    vector<int> missingTwo(vector<int>& nums) {
        //把数组中的数和1~N全部异或在一个数中N = nums.size() + 2
        int sum = 0;
        for(int i = 0; i < nums.size(); i++)
            sum ^= nums[i];
        for(int i = 1; i <= nums.size() + 2; i++)
            sum ^= i;

        //找出sum二进制中第一个1的位置
        int x = sum & -sum,ret1 = 0, ret2 = 0;
        //根据这个1的位置将数组中的数和1~N分为两部分分别异或求出结果
        for(int i = 0; i < nums.size(); i++)
        {
            if(nums[i] & x)
                ret1 ^= nums[i];
            else
                ret2 ^= nums[i];
        }
        for(int i = 1; i <= nums.size() + 2; i++)
        {
            if(i & x)
                ret1 ^= i;
            else
                ret2 ^= i;
        }
        
        ret.push_back(ret1);
        ret.push_back(ret2);
        return ret;
    }
};

我的笔记:

  • 算法思路:如何分离已经异或在一起的两个数?—> 异或数中的二进制1表示这两个原来的数这一个二进制位是不一样的,将原来的数按这个二进制位是0还是1分为两组数分别异或

  • 注意数组和1~N的边界问题,数组下标是从0开始的,缺了两个数,所以i <= nums.size() + 2,有等号,也要加2

  • 位运算技巧sum & -sum快速找到最低位的1。

  • 分组异或:按某一位是否为1将数分为两组,分别异或得到结果。

  • 关键点:分组依据是两数在该位不同,确保每组各含一个结果。

  • 扩展应用:适用于缺失多个数字的问题(需调整分组逻辑)。

注意事项

C++语法注意

  1. 位移操作:右移负数时补符号位,需用unsigned int避免未定义行为。
  2. 位运算符优先级&|优先级低于比较运算符,需加括号。
  3. 整数溢出:高斯求和法可能溢出,优先用异或法。

算法思路注意

  1. 异或性质:灵活应用a ^ a = 0a ^ 0 = a简化问题。
  2. 位统计法:将问题分解到每个二进制位,独立处理。
  3. 正难则反:如最小删除和转化为最大公共和。
  4. 分组思想:通过某一位的不同将问题拆分为子问题。

常见错误

  • 未处理负数导致死循环(如n >> 1循环终止条件)。
  • 位操作符漏写括号(如(x >> i) & 1)。
  • 忽略鸽巢原理等优化,暴力求解超时。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SubtleByte

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值