【ps】本篇有 10 道 leetcode OJ。
目录
一、算法简介
程序中的所有数在计算机内存中都是以二进制的形式储存的,位运算就是直接对整数在内存中的二进制位进行操作,常见有逻辑与(&)、逻辑或(|)、逻辑异或(^)、按位取反(~)等。
【Tips】常见位运算方法总结
(1)区分位运算的优先级:别人工区分了,按题目要求能加括号就加括号!
(2)基础的与、或、异或
(3)位图相关
确定 n 的二进制数的第 x 位是 0 还是 1:右移 x 后与 1
将 n 的二进制数的第 x 位修改成 1:或上 1 的左移 x
将 n 的二进制数的第 x 位修改成 0:与上取反的 1 的左移 x
提取 n 的二进制数最右侧的 1:n 与 -n
删除 n 的二进制数最右侧的 1:n 与 n - 1
位图(哈希表)标识二进制数的比特位
(4)异或运算律
二、相关例题
1)位1的个数
.1- 题目解析
可以依此删除二进制数最右侧的 1,每删除一次都用一个计数器对其进行统计,直到二进制数中的 1 全部被删除。
.2- 代码编写
class Solution {
public:
int hammingWeight(uint32_t n) {
int cnt = 0;
while(n)
{
n &= (n - 1);
++cnt;
}
return cnt;
}
};
2)比特位计数
.1- 题目解析
这道题只是从上道题的“对一个二进制数求位 1 的个数”,变成了“对多个二进制数求位 1 的个数”。
.2- 代码编写
class Solution {
public:
vector<int> countBits(int n) {
vector<int> v(n+1, 0);
for(int i = 1; i <= n; ++i)
{
int cnt = 0, tmp = i;
while(tmp)
{
tmp &= (tmp - 1);
++cnt;
}
v[i] = cnt;
}
return v;
}
};
3)汉明距离
.1- 题目解析
将两个数异或后,两个数相同的比特位会变成 0 ,不同的比特位会变成 1,此时只需统计异或结果的 1 的个数即可。
.2- 代码编写
class Solution {
public:
int hammingDistance(int x, int y) {
int tmp = x ^ y, cnt = 0;
while (tmp)
{
tmp &= (tmp - 1);
++cnt;
}
return cnt;
}
};
4)只出现一次的数字
.1- 题目解析
把所有的数异或到一起,相同的数异或后结果为 0 ,剩余那个只出现了一次的数异或 0 还会等于它本身。
.2- 代码编写
class Solution {
public:
int singleNumber(vector<int>& nums) {
int val=0;
for(auto e:nums)
{
val^=e;
}
return val;
}
};
5)只出现一次的数字 III
.1- 题目解析
假设数组中只有 2 和 4 只出现过一次。将数组中所有数字异或到一起,其他出现两次的数就抵消了,此时相当于 2 异或 4,即 0010 ^ 0100 = 0110.
结合异或的结果和异或前运算数的比特位不难发现,只出现一次的数,其比特位在异或的结果中会被标识出来。由此,我们可以取异或结果最右侧的 1 是在第 cnt 个比特位上,将 1 左移 cnt 个比特位就能得到原来的数,此时再将 1 左移 cnt 个比特位的结果与数组异或一遍,以验证结果是否在数组中存在,若存在即返回该结果。
.2- 代码编写
class Solution {
public:
vector<int> singleNumber(vector<int>& nums) {
//1.将所有数异或到一起
int sum = 0;
for (const auto& e : nums)
{
sum ^= e;
}
//2.找到异或结果最右侧的 1 是第几个比特位
int cnt = 0;
for (int i = 0;i < 32;++i)
{
if (sum & (1 << i)) // 第几位是1,&的结果就是1
{
cnt = i;
}
}
//3.在原数组中验证并返回结果
vector<int> v = { 0,0 }; // C++11的初始化方式
for (const auto& e : nums)
{
if (e & (1 << cnt)) // 第几位是1,&的结果就是1
{
v[0] ^= e; //记录其中一个只出现一次的数
}
else
{
v[1] ^= e; //另一个数只与其他出现过两次的数异或在一起,就得到了另一个数
}
}
return v;
}
};
//虽然吧,这道题用哈希的方式显然更方便,但本篇的专题是位运算呐!
class Solution {
public:
vector<int> singleNumber(vector<int>& nums) {
unordered_map<int, int> freq;
for (auto e: nums) {
++freq[e];
}
vector<int> ans;
for (const auto& [e, occ]: freq) {
if (occ == 1) {
ans.push_back(e);
}
}
return ans;
}
};
6)判定字符是否唯一
.1- 题目解析
不难想到用一个哈希表来统计每个字符出现的频次,但其实我们还可以用一个位图来充当哈希表。
一个整型变量有 32 个比特位,每个比特位上的值要么为 0 ,要么为 1。而所谓位图,就是用一个 32 位的整型变量来对某些情况进行标识,其中,相应比特位上的值为 0 则表示不存在,为 1 则表示存在。
此外,我们还可以对位图标识字符是否出现的这个过程,用鸽巢原理进行优化。
【Tips】 鸽巢原理
若存在 n + 1 只鸽子,n 个巢穴,则至少有一个巢穴中的鸽子数量大于 1。
.2- 代码编写
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; //位图能够顺利统计完所有字符,就说明字符串的所有字符是唯一的
}
};
7)丢失的数字
.1- 题目解析
直接将 [ 0,n ] 中的所有数,和数组中所有数异或在一起,就能找到那个不存在的数了。
.2- 代码编写
class Solution {
public:
int missingNumber(vector<int>& nums) {
int n=nums.size();
int ret=0;
//异或数组中的数
for(auto e:nums)
{
ret^=e;
}
//异或[ 0,n ] 中的数
for(int i=0;i<=n;i++)
{
ret^=i;
}
return ret;
}
};
8)两整数之和
.1- 题目解析
如果是在笔试场上,可以不讲武德,直接 return a + b 即可。
不过,这道题同样可以用异或运算律来解决。
将两个二进制数与在一起的结果左移 1 位,会将它们相加时进位的比特位给标识出来。而两个数异或在一起,能够得到它们相加时未进位的部分,此时只需将左移 1 位的与结果和异或结果再异或一次,就能够得到两个数的和了。
设 a = 13,b = 28,则 a + b = 41,我们只需对 a 和 b 重复上述过程,就能得到它们的和 41。
.2- 代码编写
class Solution {
public:
int getSum(int a, int b) {
//return a+b;
while(b!=0)
{
int x=a^b;
int carry=(a&b)<<1;
a=x;
b=carry;
}
return a;
}
};
9)只出现一次的数字 II
.1- 题目解析
若一个数只在数组中出现一次,其它数恰出现三次,则对于数组中所有的数,将它们的任意一个比特位统一地求和,都可能出现如下几种情况:
如果将每一个比特位的这些情况 % 3,就能得到只出现一次的数的每一个比特位。
.2- 代码编写
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ret = 0;//储存结果
for(int i = 0; i < 32; ++i) // 依次修改ret的32个比特位(0改1)
int sum = 0;
for(auto e : nums)
if(((e >> i) & 1) == 1) //计算nums中所有数第i个比特位之和
++sum;
sum %= 3;
if(sum == 1)
ret |= (1 << i); // 将ret的第i位改为1
return ret;
}
};
10)消失的两个数字
.1- 题目解析
把这消失的两个数看成一个整体,然后将数组中的所有数与 [ 0,n ] 中的所有数异或在一起,就能得到这个整体。此时这个整体也是两个数异或的结果,这个结果中存在为 1 的比特位,就说明该比特位在两个数中必有一个为 1。
由此,我们可以将数组中的数分为两类,一类是该比特位上为 1 的数,另一类是该比特位上不为 1 的数。然后再将这两类数分别异或在一起,就能分别得到消失的两个数了。
.2- 代码编写
class Solution {
public:
vector<int> missingTwo(vector<int>& nums) {
//1.将所有数异或在一起
int tmp=0;
for(auto e:nums)tmp^=e;
for(int i=1;i<=nums.size()+2;i++)
tmp^=i;
//2.找出异或结果中为1的比特位
int diff=0;
while(1)
{
if(((tmp>>diff)&1)==1)
break;
diff++;
}
//3.将所有数分为两类,分别异或得出两个消失的数
int a=0,b=0;
for(auto e:nums)
{
if(((e>>diff)&1)==1)
a^=e;
else
b^=e;
}
for(int i=1;i<=nums.size()+2;i++)
{
if(((i>>diff)&1)==1)
a^=i;
else
b^=i;
}
return {a,b};
}
};