算法——位运算

本文详细介绍了基础位运算符如右移、左移、按位取反、按位或、按位与和按位异或,以及位图的概念、核心操作和在编程问题中的应用实例,包括计数、查找、汉明距离计算等。
摘要由CSDN通过智能技术生成

目录

1. 基础位运算

1. 右移运算符 (>>)

2. 左移运算符 (<<)

3. 按位取反运算符 (~)

4. 按位或运算符 (|)

5. 按位与运算符 (&)

6. 按位异或运算符 (^)

2. 位图

1. 给定一个数n,确定它的二进制表示中的第x位是0还是1

2. 将一个数n的二进制表示的第x位修改成1

3. 将一个数n的二进制表示的第x位修改成0

4. 位图的思想 

3. 提取与删除二进制中最右侧的1

1. 提取(lowbit)

2. 删除

4. 异或运算(^)的运算律

5. 应用实例

1. 位1的个数

2. 比特位计数

3. 汉明距离

4. 只出现一次的数字

5. 只出现一次的数字Ⅲ

6. 判定字符是否唯一

7. 丢失的数字

解法一:哈希表

解法二:高斯算法求和

解法三:异或运算

8. 两整数之和

9. 只出现一次的数字Ⅱ

10. 消失的两个数字


1. 基础位运算

位运算符是在二进制位级别上对数据进行操作的运算符。以下是一些常见的位运算符:

1. 右移运算符 (>>)

将一个数的所有二进制位向右移动指定的位数。右移运算符 >> 表示将运算符左边的操作数的所有位向右移动右边指定的位数,右边多余的位将被丢弃,左边用符号位填充。

int x = 8;  // 二进制: 0000 1000
int result = x >> 2;  // 二进制: 0000 0010,即十进制的 2

2. 左移运算符 (<<)

将一个数的所有二进制位向左移动指定的位数。左移运算符 << 表示将运算符左边的操作数的所有位向左移动右边指定的位数,左边多余的位将被丢弃,右边用零填充。

int x = 8;  // 二进制: 0000 1000
int result = x << 2;  // 二进制: 0010 0000,即十进制的 32

3. 按位取反运算符 (~)

对一个数的每个二进制位执行取反操作,即将 0 变为 1,将 1 变为 0。

int x = 5;  // 二进制: 0000 0101
int result = ~x;  // 二进制: 1111 1010,即十进制的 -6

4. 按位或运算符 (|)

对两个数的每个二进制位执行按位或操作,只要有一个为 1,结果位就为 1。

int x = 5;  // 二进制: 0000 0101
int y = 3;  // 二进制: 0000 0011
int result = x | y;  // 二进制: 0000 0111,即十进制的 7

5. 按位与运算符 (&)

对两个数的每个二进制位执行按位与操作,只有两个都为 1,结果位才为 1。

int x = 5;  // 二进制: 0000 0101
int y = 3;  // 二进制: 0000 0011
int result = x & y;  // 二进制: 0000 0001,即十进制的 1

6. 按位异或运算符 (^)

对两个数的每个二进制位执行按位异或操作,如果两个位不同则结果位为 1,相同则为 0。或者可以简单记忆为无进位相加。

int x = 5;  // 二进制: 0000 0101
int y = 3;  // 二进制: 0000 0011
int result = x ^ y;  // 二进制: 0000 0110,即十进制的 6

2. 位图

位图(Bitmap)是一种用于表示二进制数据的数据结构,通常用于表示一组二进制位的状态。它的核心思想是用二进制位的值(0或1)来表示某种状态或信息。它的主要核心操作即以下三步

1. 给定一个数n,确定它的二进制表示中的第x位是0还是1

要想判断某一个位置是0还是1只需要对这个位置&上一个1,如果该位置为1则结果为1,反之则结果为0,在这里我们要想快速判断可以使用

(n >> x) & 1

2. 将一个数n的二进制表示的第x位修改成1

要想修改x位置的值为1,只需要对这个位置|上一个1,其余位置|上0,这样就可以将该位置修改成1,即

在这里我们要想快速修改可以使用

(1 << x) | n

3. 将一个数n的二进制表示的第x位修改成0

 要想修改x位置的值为0,只需要对这个位置&上一个0,其余位置&上1,这样就可以将该位置修改成0,即

在这里我们要想快速修改可以使用

(~(1 << x)) & n

4. 位图的思想 

从使用上来说,位图其实与哈希表相似只不过哈希表是创建一个数组来存储信息,而位图则是使用比特位中的0与1来存储信息。

3. 提取与删除二进制中最右侧的1

1. 提取(lowbit)

要想提取最右侧的0,只需要让n & -n,即

可以看到,这个操作的本质就是将最右侧的1的左边区域全部变为相反的数

2. 删除

要想提取最右侧的0,只需要让n & (n - 1),即

可以看到,这个操作的本质是将最右侧的1的右边区域(包含1)全部变为相反数

4. 异或运算(^)的运算律

异或运算律如下

1. a ^ 0 = a

2. a ^ a = 0

3. a ^ (b ^ c) = (a ^ b) ^ c

5. 应用实例

1. 位1的个数

题目链接:191. 位1的个数 - 力扣(LeetCode)

解析:根据题意我们可以使用删除最右侧的1操作来统计个数,代码如下

class Solution 
{
public:
    int hammingWeight(uint32_t n) 
    {
        int num = 0;
        
        while (n)
        {
            num++;
            n &= (n - 1);
        }    

        return num;
    }
};

2. 比特位计数

题目链接:338. 比特位计数 - 力扣(LeetCode)

解析:这道题也可以使用删除最右侧的1策略来统计,代码如下

class Solution 
{
public:
    vector<int> countBits(int n) 
    {
        vector<int> ret;

        for (int i = 0; i <= n; i++)
        {
            int num = 0, cur = i;
            while (cur)
            {
                num++;
                cur &= (cur - 1);
            }
            ret.push_back(num);
        }

        return ret;
    }
};

3. 汉明距离

题目链接:461. 汉明距离 - 力扣(LeetCode)

解析:这里我们需要求得不同的二进制位个数,由于异或操作可以达到相异为1,相同为0的效果,我们只需要将两个数异或并统计异或后1的数量即可,代码如下

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

        return ret;
    }
};

4. 只出现一次的数字

题目链接:136. 只出现一次的数字 - 力扣(LeetCode)

解析:这道题我们可以利用异或的性质,由于只有一个数出现次数为1次,其余数出现次数均为2次,我们只需要将他们全部异或起来就能得到结果,代码如下

class Solution 
{
public:
    int singleNumber(vector<int>& nums) 
    {
        int res = 0;
        
        for (auto e : nums) res ^= e;

        return res;
    }
};

5. 只出现一次的数字Ⅲ

题目链接:260. 只出现一次的数字 III - 力扣(LeetCode)

解析:这道题中,有两个数字只出现一次(这里以3(011)和5(101)举例),那么将所有的数异或起来之后,会得到这两个数的异或结果(6(110)),由于异或的性质是相异为1,所以我们可以提取最右边的1(这个1代表3和5在这个位置不同),以它作为标志将整个数组分为2组,再次分别将所有数据异或便可以得到结果,代码如下

class Solution 
{
public:
    vector<int> singleNumber(vector<int>& nums) 
    {
        int tmp = 0;
        for (int e : nums) tmp ^= e;

        // 取得最后一位1
        // 当tmp为INT_MIN时,即负数的最高位为 1,直接对其取反会导致溢出,需要特殊判断
        int flag = tmp == INT_MIN ? tmp : tmp & (-tmp);
        int e1 = 0, e2 = 0;
        for (int e : nums)
        {
            if (flag & e) e1 ^= e;
            else e2 ^= e;
        }

        return { e1, e2 };
    }
};

6. 判定字符是否唯一

题目链接:面试题 01.01. 判定字符是否唯一 - 力扣(LeetCode)

解析:对于这道题我们常规解决思路是创建一个哈希表,并统计每个字符出现次数,在这里我们可以使用位图的思想来快速判断,由于这里s仅包含小写字符,我们可以使用一个int的比特位来确定每个字符的状态,为0则表示没有出现过,反之。此外,我们还可以使用鸽巢原理来对其进行优化

鸽巢原理(Pigeonhole Principle)是一种基本的数学原理,它描述了在一些有限资源分配到一些有限位置时,至少会有一个位置有多个资源的情况。具体来说,鸽巢原理可用于解决问题,例如如果有11个抽屉,放置12只鸽子,那么必定有一个抽屉里至少有两只鸽子。这是因为鸽巢原理指出,将12只鸽子放入11个抽屉中,必然导致至少有一个抽屉里有两只鸽子。

即如果字符串s长度超过26,则表明它一定有一个重复字符,代码如下

class Solution 
{
public:
    bool isUnique(string astr) 
    {
        // 使用鸽巢原理进行优化
        if (astr.size() > 26) return false;

        int bitmap = 0;
        for (char ch : astr)
        {
            int i = ch - 'a';
            // 若ch位置为1,则表示出现过
            if (((bitmap >> i) & 1) == 1) return false;
            // 未出现就加入位图
            bitmap |= 1 << i;
        }

        return true;
    }
};

7. 丢失的数字

题目链接:268. 丢失的数字 - 力扣(LeetCode)

解析:对于这道题我们可以采取常规的解决思路

解法一:哈希表

我们可以创建一个大小为n+1的哈希表,遍历一遍数组并将对应位置++,到最后查看hash表中value为0的数字,代码如下

class Solution 
{
public:
    int missingNumber(vector<int>& nums) 
    {
        int ret = 0, n = nums.size();
        unordered_map<int, int> hash;
        for (auto e : nums) hash[e]++;

        for (int i = 0; i <= n; i++) 
            if (hash[i] == 0) ret = i;

        return ret;
    }
};

解法二:高斯算法求和

由题意我们可以知道,在所有数字中只有一个数字是缺失的,因此我们只需要计算出总和再减去nums数组的总和即可,代码如下 

class Solution 
{
public:
    int missingNumber(vector<int>& nums) 
    {
        int sum = 0, n = nums.size();
        for (auto e : nums) sum += e;

        int s = (0 + n) * (n + 1) / 2;

        return s - sum; 
    }
};

解法三:异或运算

除了上面两种解法外,我们还可以使用异或运算将0~n的所有数与nums中的所有数异或,这样得到的最终结果就是缺失的数字,代码如下

class Solution 
{
public:
    int missingNumber(vector<int>& nums) 
    {
        int res = 0;
        
        for (int i = 0; i <= nums.size(); i++)
        {
            res ^= i;
            if (i < nums.size()) res ^= nums[i];
        }

        return res;
    }
};

8. 两整数之和

题目链接:371. 两整数之和 - 力扣(LeetCode)

解析:(如果这道题出现在笔试中,我们可以直接return a + b;)×,要想不使用+运算符得到两数之和,我们可以使用异或运算符(^)来做到无进位相加,与运算符(&)来做到统计进位,即

代码如下

class Solution 
{
public:
    int getSum(int a, int b) 
    {
        while (b)
        {
            int t_a = a, t_b = b;
            a ^= b;
            b = (t_a & t_b) << 1;
        }

        return a;
    }
};

9. 只出现一次的数字Ⅱ

题目链接:137. 只出现一次的数字 II - 力扣(LeetCode)

解析:读一下题我们可以知道,每一个元素在nums中出现次数都为三次或一次,那么遍历一遍数组后对于每个比特位来说,出现的情况只可能有4种:0, 1, 3n+0, 3n+1(n为出现三次元素的个数),仔细观察可以发现,我们将出现的所有情况%3(3为每个元素恰巧出现的次数),得到的结果就是出现一次的那个元素的正确比特位,我们可以得到代码如下

class Solution 
{
public:
    int singleNumber(vector<int>& nums) 
    {
        int ret = 0;
       
        for (int i=0; i<32; i++)
        {
            long long sum = 0;
            for(auto e : nums) 
                if (((e >> i) & 1) == 1) sum++;
            sum %= 3;
            if (sum) ret |= (1 << i);
        }
        return ret;
    }
};

10. 消失的两个数字

题目链接:面试题 17.19. 消失的两个数字 - 力扣(LeetCode)

解析:要想解决这道题,我们可以将5题与7题相结合,即先将1~N的数字还有nums的数据都异或起来,这样得到的就是两个缺失的数字异或的结果,这之后我们需要创建一个flag来记录异或结果最右边的1(表示两个缺失的数在这个比特位不同),这之后再次遍历0~N与nums,利用flag将数据分散到两组内,最终找到两个数,代码如下

class Solution 
{
public:
    vector<int> missingTwo(vector<int>& nums) 
    {
        int res = 0, n = nums.size();
        for (int i = 1; i <= n+2; i++)
        {
            res ^= i;
            if (i <= n) res ^= nums[i-1];
        }

        int flag = res & -res;
        int num1 = 0, num2 = 0;
        for (int i = 1; i <= n+2; i++)
        {
            if (i & flag) num1 ^= i;
            else num2 ^= i;

            if (i <= n)
            {
                if (nums[i-1] & flag) num1 ^= nums[i-1];
                else num2 ^= nums[i-1];
            } 
        }

        return {num1, num2};
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值