
**🎬 博主名称**:迷途之人不知返
🔥 个人专栏 : 《C语言》、《数据结构》、《C++》、《Linux》
🗂️ Gitee仓库 : 《C语言》、《数据结构》、《C++》、《Linux》
</> 算法专栏 : 《算法精选集》
位运算
- [1 ···> 预备知识](#1 ···> 预备知识)
-
- 1.1、基础位运算符号
- [1.2、给一个数n,确定它的二进制表示中的第x位是 0 / 1](#1.2、给一个数n,确定它的二进制表示中的第x位是 0 / 1)
- 1.3、运算符的优先级
- 1.4、将一个数n的二进制表示的第x位修改成1
- 1.5、将一个数n的二进制表示的第x位修改成0
- 1.6、位图的思想
- 1.7、提取一个数n二进制表示中最右侧的1
- 1.8、干掉一个数n二进制表示中最右侧的1
- 1.9、异或(^)运算的运算律
- 1.10、补充应该先做的题
- [2 ···> 判断字符是否唯一](#2 ···> 判断字符是否唯一)
- [3 ···> 丢失的数字](#3 ···> 丢失的数字)
- [4 ···> 两整数之和](#4 ···> 两整数之和)
- [5 ···> 只出现一次的数字(Ⅱ)](#5 ···> 只出现一次的数字(Ⅱ))
- [6 ···> 消失的两个数字](#6 ···> 消失的两个数字)
讲解位运算的算法题之前,我们先来补充一点预备知识。
1 ···> 预备知识
1.1、基础位运算符号
| 符号 | 运算法则 |
|---|---|
| << | 整数的二进制位左移一位 |
| (>>) | 二进制位右移一位 |
| ~ | 按位取反 |
| & | 按位与:有0就是0,全1才是1 |
| (竖线我打不出来) | 按位或:有1就是1,全0才是0 |
| ^ | 按位异或:相同为0,不同为1 (无进位相加) |
1.2、给一个数n,确定它的二进制表示中的第x位是 0 / 1
首先,我们约定,一个数的二进制位,是从0开始,即第0位、第1位、第2位......一直到第31位。

要想确定一个数的二进制表示的第x位,到底是0还是1,我们可以先把这第x位右移到第0位上,再过滤掉除第0位以外所有位上的1,这时我们想到按位与(&):
(n >> x) & 1
确定第x位是0或1的式子得出。验证一个数:
c
#include <stdio.h>
int Func(int n, int x)
{
return (n >> x) & 1;
}
int main()
{
int n = 0, x = 0;
while (~scanf("%d %d", &n, &x))
printf("%d\n", Func(n, x));
return 0;
}
5的二进制表示:0 1 0 1,第2位:

1.3、运算符的优先级
这里我们不需要背那些复杂的运算符优先级表,只需记住:想让哪一步运算先执行,就给哪一步加上小括号。
1.4、将一个数n的二进制表示的第x位修改成1
将1左移x位,然后按位或n即可。由于我们做的是修改,所以运算结果要反过来写入n。
n |= (1 << x)
例如,213修改第3位成"1"后结果是221:
c
#include <stdio.h>
int Func(int n, int x)
{
return n |= (1 << x);
}
int main()
{
int n = 0, x = 0;
while (~scanf("%d %d", &n, &x))
printf("%d\n", Func(n, x));
return 0;
}

1.5、将一个数n的二进制表示的第x位修改成0
将1左移x位,然后按位取反,再按位与上n即可。
n &= (~(1 << x))
因为我们要将第x位修改成"0",所以我们就需要保留其它位的"1"。这时我们会想到:"1"按位与上"1"的结果还是"1",而"0"按位与上任何数的结果都是"0"。
例如,89的第3位修改成0后结果是81:
c
#include <stdio.h>
int Func(int n, int x)
{
return n &= (~(1 << x));
}
int main()
{
int n = 0, x = 0;
while (~scanf("%d %d", &n, &x))
printf("%d\n", Func(n, x));
return 0;
}

1.6、位图的思想
所谓位图,本质上就是一个缩小版的哈希表。由于有些信息可以转化为0和1,
- 哈希表还要创建一个很大的数据结构,来存储信息;
- 而位图只需要借助一个整型变量(
int, unsigned long),就可以在整型变量的二进制表示中标记0和1。
位图的使用,大大降低了空间复杂度。甚至我们在Linux的"大O(1)调度算法"中,借助位图遍历不同优先级的进程队列,由遍历140次降低到遍历5次,然后通过常数级效率的运算确定当前优先级的进程队列是否为空,还能一定程度上缩短运行时间,提高运行效率。
1.7、提取一个数n二进制表示中最右侧的1
给出计算式:
n & (-n)
我们可以举例观察一下:
c
// int整型有32位
// n: 0000 0000 0000 0000 0000 0001 0110 1000
// 取负操作: 按位取反,再+1
//~n: 1111 1111 1111 1111 1111 1110 1001 0111
//-n: 1111 1111 1111 1111 1111 1110 1001 1000
我们发现,以最右侧的"1"为界,
- n与-n的右侧,都是"0";
- n与-n的左侧,每一位都不同;意味着每一位在n或-n上,一定有一个"0"。
所以n & (-n),
- 对于左侧:"0"与"0"按位与,结果是"0";
- 对于右侧:"0"与"1"按位与,结果是"0"。
自然就过滤出最右侧的"1"。
1.8、干掉一个数n二进制表示中最右侧的1
给出计算式:
n = n & (n - 1)
我们以举例观察一下:
c
// int整型有32位
// n: 0000 0000 0000 0000 0000 0001 0110 1000
// n - 1: 0000 0000 0000 0000 0000 0001 0110 0111
所以 n - 1 的本质是:将n二进制表示式的右侧,连同最右侧的1,一同取反。
接着只要 n & (n - 1) ,就可以干掉最右侧的1了。
1.9、异或(^)运算的运算律
| 文字描述 | 符号表示 |
|---|---|
| 0与任何数异或,结果都是这个数 | a ^ 0 = a |
| 自己与自己异或,结果是0(消消乐) | a ^ a = 0 |
| 结合律 | a ^ b ^ c = a ^ (b ^ c) |
根据结合律,可以推断出:所有数异或在一起,结果是唯一的。
我们可以简单演示一下三个数连续异或的计算方法:

c
#include <stdio.h>
int main()
{
printf("%d\n", 90 ^ 21 ^ 81);
return 0;
}
1.10、补充应该先做的题
都在leetcode上,
191、位1的个数
其实就是不断出最右侧的"1",然后统计出了多少次:
cpp
class Solution {
public:
int hammingWeight(int n) {
int ret = 0;
while (n)
{
n &= (n - 1);
++ret;
}
return ret;
}
};
338、比特位计数
这道题很容易可以想到用循环嵌套的方法来做,即遍历 0 ~ n ,对每一个数都用循环统计"1"的个数。
但是这道题可以用动态规划的思想来优化,我得先好好学一下动态规划,再来自己好好攻克一下。
461、汉明距离
就是求两个数的二进制表示中,有多少位不同。
我们就可以将两个数按位异或起来得到val,这样一来相同的位变成0,不同的位变成1,问题就转化成统计val中"1"的个数即可。
cpp
class Solution {
public:
int hammingDistance(int x, int y) {
int val = x ^ y;
int ret = 0;
while (val)
{
val &= val - 1;
++ret;
}
return ret;
}
};
136、只出现一次的数
只需按位异或在一起,就可以过滤掉所有出现两次的数。
cpp
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ret = 0;
for (auto& i : nums)
ret ^= i;
return ret;
}
};
260、只出现一次的数Ⅲ
这道题与上面一道题的区别在于,这道题多了一个只出现一次的数需要我们找出来。
在这里,我们不妨把整个数组,分成各包含一个出现一次的数,的两个数组。分离的方法是:
- 所有数按位异或到一起,得到val;
- 对于val的二进制表示,我们看最右侧的"1"
- 一定有一批出现两次的数,以及其中一个出现一次的数,相同位置上也是"1";
- 剩下的出现两次的数,以及另一个出现一次的数,相同位置上就是"0"。
- 我们按照(2)中的规律,再分别将两个数组中的数按位异或起来,就能得到结果。
cpp
class Solution {
public:
vector<int> singleNumber(vector<int>& nums) {
int val = 0;
for (auto& i : nums) val ^= i;
int left = 0, right = 0, order = val & (-(unsigned int)val);
for (auto& i : nums)
{
if (order & i) left ^= i;
else right ^= i;
}
return {left, right};
}
};
2 ···> 判断字符是否唯一
我们很容易会想到使用哈希表:
- 遇到不存在的字母,入哈希表;
- 遇到存在的字母,返回
false; - 遍历能够结束,返回
true。
但是直接创建一个哈希表,空间浪费还是比较大。
小写英文字母有26个,而int类型变量占32个比特位。我们不妨只用一个int变量代替哈希表,即使用位图。
cpp
class Solution {
public:
bool isUnique(string astr) {
int ret = 0;
for (auto& c : astr)
{
int local = c - 'a';
if ((ret & (1 << local)) != 0) return false;
else ret |= (1 << local); // 记录存在与否,不是&!!!
}
return true;
}
};
3 ···> 丢失的数字
假设数组的长度为x。
原来的数组,应该是:
0, 1, 2, 3, 4, ..., x
现在缺了一个数字。那么现在数组的下标排成的一个序列是:
0, 1, 2, 3, 4, ..., (x - 1)
我们只需初始化一个返回值ret为x,然后遍历现数组,将值与下标按位异或在一起,就能将问题转化为"只出现一次的数字"进行求解。
cpp
class Solution {
public:
int missingNumber(vector<int>& nums) {
int ret = nums.size();
for (int i = 0; i < nums.size(); ++i)
ret ^= i ^ nums[i];
return ret;
}
};
4 ···> 两整数之和
题目意思很简单,就是计算给出两个整数的加和。但是,我们不能使用"+""-"完成两个整数的加和。
对于两整数相加,我们很容易想到按位异或,即无进位相加。比如,
c
// 13: 001101
// 28: 011100
// 13 ^ 28 = 010001
"1"与"1"相加,肯定是要进位的:
- "1"与"1":进一位;
- "1"与"0":不进位;
- "0"与"0":不进位。
我们很容易就能想到按位与。而且,进的一位,肯定是要左移的:
c
// 13: 001101
// 28: 011100
// 13 & 28 = 001100
// (13 & 28) << 1 = 011000
接着,我们重复操作:
- 让^出来的结果充当a;
- 让&并左移的结果充当b;
- 继续上述两个操作,直到b为0,也就是进位没有了,此时a就是返回值。

cpp
class Solution {
public:
int getSum(int a, int b) {
while (b)
{
int x1 = a ^ b, x2 = (a & b) << 1;
a = x1, b = x2;
}
return a;
}
};
小贴士1
笔试场上我们要见机行事,如果允许的话,直接
return a + b;得了。(😄)
5 ···> 只出现一次的数字(Ⅱ)
这道题的特点是:在一个数组中,除只出现一次的数外,其他数都出现了3次。
我们不妨观察数组中每一个数的任意一个比特位:

对于每一个数,我们可以做一个区分:
- 要么是出现一次的数;
- 要么是出现3次的数;
那么对于一个数组中所有的数,在这个特定位置上的比特位,相加之后,有这四种情况:
- 3n个0 + 0 = 0
- 解释一下:其中,"0"表示出现一次的数特定二进制位上是0;"3n个0"表示剩余出现三次的数中,每一种数特定二进制位上都是0。所有0相加,结果还是0。
- 3n个0 + 1 = 1
- 3n个1 + 0 = 3n
- 3n个1 + 1 = 3n + 1
我们再将四个结果%3,就可以得到:
- 0 % 3 = 0
- 1 % 3 = 1
- 3n % 3 = 0
- (3n + 1) % 3 = 1
四个结果,不刚好与出现一次数在特定二进制位置上的比特位表示相对应吗?通过这个规律,我们就可以把结果"从ret = 0开始,一个比特位一个比特位地把结果做出来"。
cpp
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ret = 0;
for (int i = 0; i < 32; ++i)
{
int bitsum = 0;
for (auto& n : nums)
if (((n >> i) & 1) != 0)
bitsum++;
bitsum %= 3;
if (bitsum == 1)
ret |= (bitsum << i);
}
return ret;
}
};
当然,我们可以推广到"除出现一次的数外,其它数都出现n次的情况(n为奇数)":
cpp
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ret = 0;
int times = 3; //
for (int i = 0; i < 32; ++i)
{
int bitsum = 0;
for (auto& n : nums)
if (((n >> i) & 1) != 0)
bitsum++;
bitsum %= times; //
if (bitsum == 1)
ret |= (bitsum << i);
}
return ret;
}
};
6 ···> 消失的两个数字
这道题的解法思路,其实是之前我们做过的两道题的思路的集合:
丢失的数字 + 只出现一次的数字(Ⅲ)
(看到这里,也许你可以自己试试看)
对于 1 ~ N 的整数,我们可以得到两个部分:

这种情形,很像"只出现一次的数字(Ⅲ)"的情况,即有两种数只出现了一次。
我们可以定义变量order,将原数组所有数与 1 ~ N 按位异或在一起。接着提取出order二进制表示的其中一个为"1"的位(可以是最右边的"1"),根据这个"1"位,将原数组和 1 ~ N 分成两个部分:
- 数a与一批出现两次的数;
- 数b与另一批出现两次的数。
cpp
class Solution {
public:
vector<int> missingTwo(vector<int>& nums) {
int sz = nums.size();
int order = 0;
for (int i = 1; i <= sz + 2; ++i)
order ^= i;
for (auto& n : nums)
order ^= n;
int left = 0, right = 0;
int diff = (order & (-(unsigned int)order));
for (int i = 1; i <= sz + 2; ++i)
{
if ((diff & i) != 0)
left ^= i;
else
right ^= i;
}
for (auto& n : nums)
{
if ((diff & n) != 0)
left ^= n;
else
right ^= n;
}
return {left, right};
}
};
这里有两个细节:
- 位运算一定要按需加括号!(如果你不想背优先级法则的话)
- 讲题老师使用了
while循环找到order的"1"位,而我直接用常数级运算提取出了order最右边的"1"。(我的方法是不是更好?)