只要春天不死,就有迎春的花朵年年岁岁开放,生命讲涅槃,生生不息,并会以另一种形式永存。 – 路遥 《平凡的世界》
(◦′ᆺ‵◦) ♬° ✧❥✧¸.•¨✧♡✧ ℒℴѵℯ ✧♡✧¨•.❥
(◦′ᆺ‵◦) ♬° ✧❥✧¸.•¨✧♡✧ ℒℴѵℯ ✧♡✧¨•.❥
(◦′ᆺ‵◦) ♬° ✧❥✧¸.•¨✧♡✧ ℒℴѵℯ ✧♡✧¨•.❥
一篇文章搞定“位运算”
1 前言
面试中经常会出现一类问题:它们看起来并不复杂,内容也很容易理解,但是它们往往带有一个额外的挑战条件——不允许使用额外的空间,或者说空间复杂度必须保持在O(1)。这类问题通常是为了考察应聘者对算法的深入理解以及对编程语言的掌握程度。
如果涉及到数组或者运算,经常就需要使用位运算来巧妙的解决这些问题。那么接下来我们来学习这个算法
2 位运算
我们熟知的位运算以下几种:
& 按位或
:有 0 就为 0| 按位与
:有 1 就为 1^ 按位异或
:相同为 0 ,不同为 1 / 无进位相加~ 按位取反
:0 变 1 ,1 变 0<< 向左移动
: 把二进制的每一位依次向左移动,右边补0>> 向右移动
: 把二进制的每一位依次向右移动,左边补0- 取负
:先按位取反,然后在加1 。以最右侧的 1 为分割,左边的区域全部变成相反的 ,右边都变成 0- 注意 一定一定要加上括号,位运算的优先级比较复杂。
通过这四种基础的位运算我们可以衍生出一些常用公式:
- 判断一个数 x 的第 n 位是 0 / 1 :
(x << n ) & 1
,计算得到1
那么第 n 位就是 1 ,反之是 0 。 - 将一个数 x 的第 n 位修改为 1 :
x |= (1 << n)
,直接就修改了 - 将一个数 x 的第 n 位修改为 0 :
x & ~(1 << n)
,这个比较复杂,(1 << n)会得到...00100...
,按位取反后变成...11011...
,然后按位或就可以不改变其他位置,就将第 n 位修改为 0了 - 提取一个数 x 二进制的最右侧的 1 (lowbit):
x & -x
,首先-x
会将x先按位取反,然后在加1。比如x是11010
,那么-x
就是 ->00101
->00110
。然后x & -x
就会得到00010
。十分巧妙! - 干掉一个数 x 最右侧的1 :
x & x - 1
,x - 1
这个操作就将最右侧的1左边不变,右边变成相反的。
另外提一个重要的东西:位图。通过比特位来进行判断是否存在。
3 基础题目
Leetcode 191. 位1的个数
Leetcode 338. 比特位计数
Leetcode 461. 汉明距离
Leetcode 136. 只出现一次的数字
这些题目比较基础,读懂题目,然后使用合理的位运算就可以了!
4 进阶题目
4.1 Leetcode 260. 只出现一次的数字 III
链接:Leetcode 260. 只出现一次的数字 III
题目描述
数组里面有两个元素只出现一次,其余出现两次,那么我们需要找到这两个元素
算法思路
首先因为其余元素都是出现两次,那么我们直接进行一次遍历,并依次进行按位异或操作。就会得到一个结果,这个结果是只出现两次的元素的异或和。
又因为这两个元素是不同的,所以异或和必然有一位是 1
,那么就以这个1
来将所有元素进行分类。这样两个不同的元素就必然处于两个不同的组之中,那么就是找只出现一次的数字
了!!!
vector<int> singleNumber(vector<int>& nums) {//先得到异或和unsigned int tmp = 0;for(auto s :nums){tmp ^= s;}//因为要取出一个 1 不妨就取最右边的 1 int lowbit = tmp & -tmp;//进行分类分析vector<int> ans(2);//容器的0 1 分别进行两组的异或处理for(auto s :nums)ans[(s & lowbit) != 0] ^= s;return ans;}
提交:过啦!!!
4.2 面试题 01.01. 判定字符是否唯一
链接:面试题 01.01. 判定字符是否唯一
题目描述
题目非常好理解!
算法思路
这道题有很多解法:哈希表 , 双指针 , 位运算
我们采取位运算的位图来解决问题,让面试官眼前一亮。位图其实就是简略的哈希表(但是运算更快),我们进行储存的时候需要将1
向左移动对应的距离,这个距离是独一无二的ch - 'a'
bool isUnique(string astr) {//抽屉原理优化if(astr.size() > 26 ) return false;//位图int map = 0;//遍历进行位图的读取for( auto ch : astr){//位图的位置如果是 1 ,那么就说明已经有该字母了if(map & ( 1 << (ch - 'a'))) return false;//反正位图该位置变为 1 else map |= ( 1 << (ch - 'a'));}return true;}
提交: 过啦!!!
4.3 Leetcode 268. 丢失的数字
链接:268. 丢失的数字
题目描述
题目很好理解!
算法思路
这道题也有很多算法:哈希表,数学方法,位运算
这里也是采取位运算的方法,我们将[0 , n]的所有元素都进行按位异或。然后再把数组中的元素进行按位异或。得到的两个异或和再进行一次异或,就可以得到没有出现的元素了!因为只有这个元素是单身狗!
int missingNumber(vector<int>& nums) {int n1 = 0 , n2 = 0 ;//一起处理,效率更高for(int i = 1 ; i <= nums.size() ; i++ ) {n1 ^= i;n2 ^= nums[i - 1];} return n1 ^ n2;}
提交:过啦!!!
4.4 Leetcode 371. 两整数之和
链接 :371. 两整数之和
题目描述
题目一如既往的好理解!
算法思路
这道题还是使用位运算奥:
那么如何进行呢???
通过这两个法宝就可以:
^ 按位异或
:相同为 0 ,不同为 1 / 无进位相加& 按位或
:有 0 就为 0 -> 因为都为 1 才得 1 所以可以判断是否需要进位
- 首先按位异或得到没有进位的数 b1
- 然后按位或 在向左移动 一位得到进位 a1
- 在把a1 , b1重复 1 - 2直到没有进位为止!
int getSum(int a, int b) {//位运算while(a){int a1 = 0, b1 = 0;b1 = a ^ b ; //无进位相加a1 = (a & b) << 1;//进位//更新数值a = a1 , b = b1;} return b;}
4.5 Leetcode 137. 只出现一次的数字 II
链接:137. 只出现一次的数字 II
题目描述
题目很好理解!
算法思路
这个与之前出现的只出现一次的数字不同,其他数字出现了3次。这要如何进行?
首先每个int类型数字都是由比特位组合成的:
那么我们就来看每个比特位相加会发生什么:
发现了吗? 无论什么情况,该比特位的数字相加完再余上 3 就变成了只出现一次的数字对应的比特位!!!
那么我们只要报每个比特位都进行如下操作就可以了:
int singleNumber(vector<int>& nums) {//位运算int ans = 0 ;//答案//开始遍历每一位比特位for(int i = 0 ; i < 32 ;i++ ){int bit = 0;//把每个元素的该位都进行相加for(auto s : nums)bit += (s >> i) & 1;//除3得到的余数就是该位的结果bit %= 3;ans |= (bit << i);} return ans;}
提交:过啦!!!
4 精通题目
面试题 17.19. 消失的两个数字
链接:面试题 17.19. 消失的两个数字
题目描述
这是一道困难题,但是在经过了上面的题目后,我们就能发现这道题其实超级简单
算法思路
首先这道题是要求我们找到[1 , N]中缺少的两个数字,那么其实就是:
丢失的数字 + 只出现一次的数字 III
为什么呢?
来看我们把数组的元素当做一个整体 sum
那么[1 , n]的元素相当于 sum + a + b。是不是这样???
分析到这一步就简单了,按照 丢失的数字 + 只出现一次的数字 III
就ok了:
vector<int> missingTwo(vector<int>& nums) {//位运算//[0 , n] 进行按位异或//数组元素进行按位异或//他们再进行按位异或int tmp = 0;for(int i = 1 ; i <= nums.size() + 2 ; i++)tmp ^= i;for(auto x:nums)tmp ^= x;//会得到消失两个数字的按位异或结果//这两个数字是不同的//所以取出最右边一位//分别进行按位异或,就可以得到对应的数字int lowbit = tmp & -tmp;vector<int> ans(2);for(int i = 1 ; i <= nums.size() + 2 ; i++)ans[(lowbit & i) != 0] ^= i;for(auto x:nums)ans[(lowbit & x) != 0] ^= x;return ans;}