位运算力扣题(leetcode)
位运算力扣题(leetcode)
78. 子集
难度:中等
题目:
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
提示:
- \(1 <= nums.length <= 10\)
- \(-10 <= nums[i] <= 10\)
- \(nums 中的所有元素 互不相同\)
代码:
class Solution
{
public:
vector<vector<int>> subsets(vector<int>& nums)
{
vector<vector<int>> res; // 存储所有子集
int n = nums.size(); // 数组长度
int total = 1 << n; // 子集总数:2^n(等价于pow(2, n),位运算更高效)
// 遍历所有状态(0 ~ 2^n - 1),每个状态对应一个子集
for (int state = 0; state < total; state++)
{
vector<int> path; // 存储当前子集
// 遍历当前状态的每一位,判断是否选中对应元素
for (int k = 0; k < n; k++)
{
// 核心:用你的模板提取state的第k位数字
if ((state >> k) & 1)
path.push_back(nums[k]); // 第k位为1,选中nums[k]
}
res.push_back(path); // 将当前子集加入结果
}
return res;
}
};
代码分析
一、先搞懂核心思想:二进制 = 子集的「选择状态」
数组的每个元素只有两种选择:选 或 不选,这刚好对应二进制的「1」和「0」。
比如数组 nums = [1,2,3](长度 3):
- 用 3 位二进制数表示所有选择可能(3 位对应 3 个元素);
- 每一位对应一个元素:第 0 位对应
nums[0]=1,第 1 位对应nums[1]=2,第 2 位对应nums[2]=3; - 二进制位为
1→ 选这个元素;为0→ 不选。
可视化对应关系(最关键!)
| 十进制 state | 二进制(3 位) | 第 2 位(对应 3) | 第 1 位(对应 2) | 第 0 位(对应 1) | 选中的元素(子集) |
|---|---|---|---|---|---|
| 0 | 000 | 0(不选) | 0(不选) | 0(不选) | [] |
| 1 | 001 | 0 | 0 | 1(选) | [1] |
| 2 | 010 | 0 | 1(选) | 0 | [2] |
| 3 | 011 | 0 | 1 | 1 | [1,2] |
| 4 | 100 | 1(选) | 0 | 0 | [3] |
| 5 | 101 | 1 | 0 | 1 | [1,3] |
| 6 | 110 | 1 | 1 | 0 | [2,3] |
| 7 | 111 | 1 | 1 | 1 | [1,2,3] |
可以看到:0~7(共 8 个,即 2³)个十进制数,刚好对应数组 [1,2,3] 的所有 8 个子集。
二、逐行拆解代码(结合上面的例子)
vector<vector<int>> res; // 存储所有子集
int n = nums.size(); // 数组长度(例子中n=3)
int total = 1 << n; // 子集总数:1<<3 = 8(等价于2³,位运算更快)
res:最终要返回的所有子集,比如例子中最后是[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]];total:子集的总数,数组长度为 n 时,子集数是 2ⁿ(每个元素有选 / 不选两种可能)。
for (int state = 0; state < total; state++)
{
vector<int> path; // 存储当前子集(比如state=1时,path=[1])
- 外层循环:遍历所有「选择状态」(例子中 state 从 0 到 7),每个 state 对应一个子集;
path:临时存储当前 state 对应的子集,每次循环都会重新初始化(比如 state=0 时 path 是空,state=1 时 path 装 [1])。
for (int k = 0; k < n; k++)
{
if ((state >> k) & 1)
path.push_back(nums[k]);
}
- 内层循环:检查当前 state 的每一位(对应数组的每个元素),判断是否选中该元素;
- 核心操作
(state >> k) & 1:提取 state 的第 k 位(你的位运算模板),结果是 0 或 1:- 例子 1:state=1(二进制 001),k=0 →
(1 >> 0) & 1 = 1 & 1 = 1→ 选中 nums [0]=1; - 例子 2:state=1,k=1 →
(1 >> 1) & 1 = 0 & 1 = 0→ 不选 nums [1]=2; - 例子 3:state=4(二进制 100),k=2 →
(4 >> 2) & 1 = 1 & 1 = 1→ 选中 nums [2]=3;
- 例子 1:state=1(二进制 001),k=0 →
path.push_back(nums[k]):如果第 k 位是 1,就把 nums [k] 加入当前子集。
res.push_back(path); // 将当前子集加入结果
}
return res;
- 把当前 state 对应的子集(path)加入最终结果 res;
- 循环结束后,res 就包含了所有子集。
三、用一个具体 state 走一遍流程(彻底理解)
以 nums = [1,2,3],state=5(二进制 101)为例:
- 外层循环 state=5,初始化 path 为空;
- 内层循环 k=0:
(5 >> 0) & 1→ 5 的二进制是 101,右移 0 位还是 101,和 1 按位与 → 1 → 选中 nums [0]=1 → path=[1];
- 内层循环 k=1:
(5 >> 1) & 1→ 5 右移 1 位是 10(二进制),和 1 按位与 → 0 → 不选 nums [1]=2;
- 内层循环 k=2:
(5 >> 2) & 1→ 5 右移 2 位是 1(二进制),和 1 按位与 → 1 → 选中 nums [2]=3 → path=[1,3];
- 把 path=[1,3] 加入 res;
- 这就是 state=5 对应的子集 [1,3]。
四、核心疑问解答(新手最常问)
1. 为什么是「state >> k & 1」,而不是其他?
- 「state >> k」:把 state 的第 k 位移到「个位」(比如 state=5=101,k=2 时,右移 2 位变成 1,第 2 位就到了个位);
- 「& 1」:只保留个位(0 或 1),就能知道第 k 位是 0 还是 1(对应不选 / 选)。
2. 为什么子集总数是「1 << n」?
- 「1 << n」是位运算,等价于 2 的 n 次方:
- n=3 时,1<<3 = 8(0~7 共 8 个数);
- n=2 时,1<<2 = 4(0~3 共 4 个数,对应 [1,2] 的 4 个子集:[], [1], [2], [1,2])。
3. 数组长度 n=10 时,state 会很大吗?
- n=10 时,total=1<<10=1024,state 从 0 到 1023,完全在 int 的范围里(int 能存到 2¹⁶左右),不用担心溢出。
五、总结(核心逻辑一句话)
用「十进制数 state」表示子集的选择状态,通过「位运算提取 state 的每一位」判断是否选中对应元素,遍历所有 state 就能生成所有子集。
整个代码的逻辑就是:枚举所有选择状态 → 解析每个状态对应的元素 → 收集所有子集,而位运算是解析状态的「高效工具」。
class Solution
{
public:
vector<vector<int>> subsets(vector<int>& nums)
{
const int n = nums.size(); // 1. 常量优化:避免重复读取size(),编译器可优化
const int total = 1 << n; // 2. 提前计算总数,避免循环内重复计算
// 3. 预分配内存:避免vector频繁扩容(最核心的效率优化)
vector<vector<int>> res;
res.reserve(total); // 直接预留2^n个位置,减少多次扩容的拷贝开销
// 4. 外层循环用int→unsigned int:避免符号位干扰,编译器生成更高效的指令
for (unsigned int state = 0; state < total; ++state)
{
vector<int> path;
// 5. 提前预估path大小(可选):减少path的扩容次数
int cnt = __builtin_popcount(state); // GCC内置函数,O(1)统计1的个数
path.reserve(cnt);
// 6. 内层循环用++k→k++(编译器优化),且k<n改为k!=n(少数编译器更友好)
for (int k = 0; k != n; ++k)
{
// 7. 位运算顺序优化:先&1再判断,逻辑不变但指令更紧凑
if ((state >> k) & 1)
{
path.push_back(nums[k]);
}
}
res.push_back(std::move(path)); // 8. 移动语义:避免path的拷贝,直接转移内存
}
return res;
}
};
相似题目
子集 II中等
字母大小写全排列中等
136. 只出现一次的数字
难度:简单
题目:
给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
示例 1:
输入:nums = [2,2,1]
输出:1
示例 2:
输入:nums = [4,1,2,1,2]
输出:4
示例 3:
输入:nums = [1]
输出:1
提示:
- \(1 <= nums.length <= 3 * 10^4\)
- \(-3 * 10^4 <= nums[i] <= 3 * 10^4\)
- \(除了某个元素只出现一次以外,其余每个元素均出现两次。\)
代码:
class Solution
{
public:
int singleNumber(vector<int>& nums)
{
int res = 0; // 初始化为0,利用n^0=n的性质
for (int num : nums)
res ^= num; // 核心:依次异或所有元素,重复元素抵消为0
return res;
}
};
相似题目
丢失的数字简单
寻找重复数中等
找不同简单
338. 比特位计数
难度:简单
题目:
给你一个整数 n ,对于 0 <= i <= n 中的每个 i ,计算其二进制表示中 1 的个数 ,返回一个长度为 n + 1 的数组 ans 作为答案。
示例 1:
输入:n = 2
输出:[0,1,1]
解释:
0 --> 0
1 --> 1
2 --> 10
示例 2:
输入:n = 5
输出:[0,1,1,2,1,2]
解释:
0 --> 0
1 --> 1
2 --> 10
3 --> 11
4 --> 100
5 --> 101
提示:
- \(0 <= n <= 10^5\)
代码:
class Solution
{
public:
int lowbit(int x)
{
return x & (-x);
}
vector<int> countBits(int n)
{
vector<int> cnt(n + 1, 0);
for (int i = 1; i <= n; ++i)
{
int j = i;
while(j)
{
cnt[i]++ ;
j -= lowbit(j) ;
}
}
return cnt;
}
};
相似题目
位1的个数简单
461. 汉明距离
难度:简单
相关标签:位运算
题目:
两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目。
给你两个整数 x 和 y,计算并返回它们之间的汉明距离。
示例 1:
输入:x = 1, y = 4
输出:2
解释:
1 (0 0 0 1)
4 (0 1 0 0)
↑ ↑
上面的箭头指出了对应二进制位不同的位置。
示例 2:
输入:x = 3, y = 1
输出:1
提示:
- \(0 <= x,y <= 2^{31}-1\)
代码:
class Solution
{
public:
int lowbit(int x)
{
return x & (-x);
}
int hammingDistance(int x, int y)
{
int diff = x ^ y; // 异或:不同位为1,相同位为0
int count = 0; // 统计1的个数(即汉明距离)
// 复用lowbit统计1的个数
while (diff > 0)
{
count++;
diff -= lowbit(diff); // 消去最右边的1
}
return count;
}
};
相似题目
位1的个数简单
汉明距离总和中等

浙公网安备 33010602011771号