算法学习(五)—— 位运算

关于位运算

①基础位运算

我们前面用到的位运算符有很多,左移(<<),右移(>>),按位取反(~)就不讲了,我们来简单复习下按位与(&),按位或(|),按位异或(^),这三个是刚学位运算时比较容易混淆的三个

  • &:有0就是0(0 & 0 = 00 & 1 = 01 & 1 = 1
  • |:有1就是1(0 | 0 = 0 1 & 0 = 11 | 1 = 1
  • ^:相同为0,不同为1,或者是无进位相加(0  ^ 0 = 01 ^ 0 = 11 ^ 1 = 0

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

假设n是:0 1 1 0 1 0 1 0 0 1,二进制表示中最右边是低位,从右往左开始,一共32位

假设我们要确定上面那个蓝色的位置的位是0还是1(假设蓝色位置的位数我们还不知道),只要将这一位与(&)上一个1就可以了,如果结果为0,表示这位是0;如果结果为1,则表示这位是1

所以我们就要想办法把这个1移到最右边,先n >> x让n右移x位,然后与(&)一个1就可以了

③位运算的优先级

不论是什么操作,能加括号就加括号,以上面的为例,我们要同时计算>>和&,所以直接(n >> x)&1 就可以了

④将一个数n的二进制位的第x位修改成1 

还是以上面的为例,n = 0 1 1 0 1 0 1 0 0 1,我们要修改第三位的0为1,其余的为保持不变,只需要将第三位或(|)上一个1,其余为或上0,就OK了:

那么如何搞呢?只需要 n |= (1 << x) 就可以了 

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

 和上面一样的,只要n &= (~(1 << x)) 

⑥位图的思想

位图我们之前已经讲过了:C++——位图与布隆过滤器_布隆过滤器和位图优缺点-CSDN博客 

上面三个操作都是为了位图服务的,位图的本质就是一个哈希表,只不过将数组和链表形式的哈希表变成了比特位表示的哈希表

位图用一个整型变量的比特位里的0和1来记录信息,这就是位图的主要思想,所以会经常用到前面讲的三个操作

⑦提取一个数n的二进制位中最右侧的1 

加入n是0 1 1 1 0 1 0 1 0 0 ,其中蓝色1就是最右侧的1,根据题目要求后,n就变成了 0 0 0 0 0 0 0 1 0 0 ,除了最右侧的1,其它的全部变成0,这就是这道题表达的意思

所以,如何提取最右边的1呢?只需要 n & -n 即可,我们先理解下 -n 的含义:

可以看到,n按位取反再+1之后,最右侧的1,左边的区域全部变成相反,这也就是 -n 的含义,所以 n & -n 结果就是提取最右侧的1

⑧干掉一个数n二进制位中最右侧的1

其实这道题就是把一个数的二进制位的最右边的1变成0,质押n & (n - 1) 即可,我们先来看下 n - 1 的含义:

所以n -1 的含义就是:将最右边的1的包括它在内右边位全部变成相反,所以n & (n - 1)的含义就是:

到这里,可以通过下面三道题来测试一下:(答案放在文章最后)

191. 位1的个数 - 力扣(LeetCode)

338. 比特位计数 - 力扣(LeetCode)

461. 汉明距离 - 力扣(LeetCode)

⑨异或(^)运算的运算律

  • a ^ 0 = a
  • a ^ a = 0
  • a ^ b ^ c = a ^ (b ^ c)

通过异或运算,下面两道题就能很快解决:

136. 只出现一次的数字 - 力扣(LeetCode)

260. 只出现一次的数字 III - 力扣(LeetCode)

部分OJ题详解 

面试题 01.10. 判定字符是否唯一 

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

我们来分析下这道题:

  • 首先是暴力解法,直接用双指针,将每个单个字符一个一个去异或,如果结果为0,则代表有相同字符,返回false
  • 解法二:就是用把每个字符扔哈希表里去,扔进去之前先判断下,如果value为0,就扔进去,如果为1,那么就返回false,时间和空间复杂度是O(N)
  •  由于只有小写字母,我们其实只需要创建一个26大小的数组即可,用0或1表示是否出现即可。所以我们可以借助位图思想,因为这样可以把空间利用到极致。
  • 优化:鸽巢原理,有26个英文字母,当目标字符串长度>26,必定有一个字符重复
class Solution {
public:
    bool isUnique(string astr) 
    {
        if(astr.size() > 26) return false;
        int x = 0;
        for(int i = 0; i < astr.size(); i++)
        {
            int a = astr[i] - 'a'; //得出字符在32位比特位的位置,(1 << a)相当于将该位置置1
            if(x & (1 << a))
            {
                //走到这里说明就是该字符第二次出现,返回false
                return false;
            }
            else
            {
                //走到这里,说明是第一次出现,将对应的比特位置1,与
                x |= (1 << a);
            }
        }
        return true;
    }
};

268.丢失的数字 

268. 丢失的数字 - 力扣(LeetCode)

解释下题目:题目说给我们一个[0, n]中n个数的数组,按道理来说应该有n + 1个数的,所以有数字缺失,题目就是让我们想办法找出这个数,下面分析下题目:

  • 解法一:先排序,然后和完整数组比较,如果值不相同,就是缺失的数
  • 解法二:用数学办法,高斯求和公式,(首项 + 末项)*项数 / 2,求出总和,再减去数组中所有元素的和,最后得到的结果就是缺失的数字
  • 解法三:异或运算。假设nums = [0, 1, 3, 5, 4],我们构建完整数组,[0, 1, 2, 3, 4, 5],然后把这两坨数全异或在一起,最终结果就是缺失的数字,因为异或的规则就是“相同为0”,那么不同的数就是缺失的数

解法一:

class Solution {
public:
    int missingNumber(vector<int>& nums) 
    {
        sort(nums.begin(), nums.end());
        for(int i = 0; i < nums.size(); i++)
        {
            if(nums[i] != i)
            return i;
        }
        return nums.size();
    }
};

解法二:

class Solution {
public:
    int missingNumber(vector<int>& nums) 
    {
        int len = nums.size();
        int sum = len * (len + 1) / 2;
        for(auto e : nums) sum -= e;
        return sum;
    }
};

解法三:

class Solution {
public:
    int missingNumber(vector<int>& nums) 
    {
        int x = 0;
        for(auto e : nums) x ^= e; //把数组中所有数搞进去
        for(int i = 0; i <= nums.size(); i++)  x ^= i; //异或完整数组
        return x;
    }
};

 371.两整数之和

371. 两整数之和 - 力扣(LeetCode)

题目要求很简单,简直就是把位运算摆在你脸上了,下面我们来分析下这道题:

  • 解法一:异或运算,因为异或运算是“无进位相加”,那么我们只需要想办法变成“进位相加即可”

关于这个我们举个例子:

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;
    }
};

137.只出现一次的数字 Ⅱ

137. 只出现一次的数字 II - 力扣(LeetCode)

我们来分析下这道题:

所有的数都是int类型,所以所有的数都是32位的,根据题目,可以判断出上面所有的数的某一个比特位的,可能会出现下面四种情况:

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

注:如果每个数出现n次,就把最后 %3 的操作变成 %n 就可以了 

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

面试题 17.19. 消失的两个数字

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

我们来分析下这道题:

  •  这道题其实是把“丢失的数字 + 只出现一次的数字 Ⅲ”,解法就是位运算
  • 题目给我们的是nums,那么假设完整数组1 ~ N就是“原nums的值” + a + b,那么我们把nums1~N搞在一起,那么就变成了:a和b出现了一次,其余的数字都出现了两次,这就和“只出现一次的数字Ⅲ了”:260. 只出现一次的数字 III - 力扣(LeetCode)
  • 步骤一:将所有的数异或在一起,然后最终我们得到的结果就是tmp = a^b
  • 步骤二:我们要想办法把这两个数分开,找到tmp中比特位为1的那一位,(因为a和b两个数是不一样的,所以a^b绝对不等于0,所以tmp中肯定有一个位是1),记这个位置为x为,既然tmp的x位为1,那么a和b肯定有一个的x位为0,有一个为1,反正不相同
  • 步骤三:找到x位置后,根据x位的不同,划分成两类异或,一类异或结果就是a,另一类就是b
class Solution {
public:
    vector<int> missingTwo(vector<int>& nums) {
        //1,将所有的数异或在一起,异或两次
        int tmp = 0;
        for(auto e : nums) tmp ^= e;
        for(int i = 0;i <= nums.size() + 2; i++) tmp ^= i;

        //2,找出a和b中比特位不同的那一位,也就是tmp中位为1的位置
        int diff = 0;
        while(true)
        {
            if(((tmp >> diff) & 1) == 1) break;
            else diff++;
        }

        //3,根据diff 位的不同,将所有的数据划分为两类来异或
        int a = 0, b = 0;
        for(int e : nums)
        {
            if(((e >> diff) & 1) == 1) 
                b ^= e;
            else 
                a ^= e;
        }
        for(int i = 0;i <= nums.size() + 2; i++)
        {
            if(((i >> diff) & 1) == 1) 
                b ^= i;
            else 
                a ^= i;
        }
        return {a,b};
    }
};

最开始五道题的答案(按顺序):

class Solution {
public:
    int hammingWeight(int n) 
    {
        int ret = 0;
        for(int i = 0;i < 32; i++)
        {
            if(n & (1 << i))
            {
                ret++;
            }
        }
        return ret;
    }
};
class Solution {
public:
    vector<int> countBits(int n) 
    {
        vector<int> v;
        for(int i = 0;i <= n; i++)
        {
            int a = i, ret = 0;
            while(a > 0)
            {
                a &= (a - 1); //该操作是将a的最右边的1变成0,执行该步骤的次数代表a有多少个1
                ret++;
            }
            v.push_back(ret);
        }
        return v;
    }
};
class Solution {
public:
    int hammingDistance(int x, int y) 
    {
        //异或操作,相同为0,不同为1
        //只要把这两个数异或一下,其中1的数量就是这两个数不相同位的个数
        //然后再统计异或后1的个数就可以得到汉明距离
        int a = x ^ y;
        int ret = 0;
        while(a > 0)
        {
            a &= (a - 1);
            ret++;
        }    
        return ret;
    }
};
class Solution {
public:
    int singleNumber(vector<int>& nums) 
    {
        int ret = 0;
        for(int i = 0;i < nums.size(); i++)
        {
            //异或操作,相同为0,不同为1
            //把数组每个数都相互异或,由于数组只有一个出现一次的数,那么其余重复的数异或之后就全变成0了
            ret ^= nums[i];
        }    
        return ret;
    }
};
class Solution {
public:
    vector<int> singleNumber(vector<int>& nums) {
        unordered_map<int,int> map;
        for(auto e : nums)
        {
            map[e]++;
        }
        vector<int> v;
        for(auto [x, y] : map)
        {
            if(y == 1)
            {
                v.push_back(x);
            }
        }
        return v;
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值