位运算力扣题(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;
  • 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)为例:

  1. 外层循环 state=5,初始化 path 为空;
  2. 内层循环 k=0:
    • (5 >> 0) & 1 → 5 的二进制是 101,右移 0 位还是 101,和 1 按位与 → 1 → 选中 nums [0]=1 → path=[1];
  3. 内层循环 k=1:
    • (5 >> 1) & 1 → 5 右移 1 位是 10(二进制),和 1 按位与 → 0 → 不选 nums [1]=2;
  4. 内层循环 k=2:
    • (5 >> 2) & 1 → 5 右移 2 位是 1(二进制),和 1 按位与 → 1 → 选中 nums [2]=3 → path=[1,3];
  5. 把 path=[1,3] 加入 res;
  6. 这就是 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;
    }
};

相似题目

只出现一次的数字 II中等

只出现一次的数字 III中等

丢失的数字简单

寻找重复数中等

找不同简单

求出出现两次数字的 XOR 值简单

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的个数简单

计算 K 置位下标对应元素的和简单

找出数组中的 K-or 值简单

461. 汉明距离

难度:简单

相关标签:位运算

题目:

两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目。

给你两个整数 xy,计算并返回它们之间的汉明距离。

示例 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的个数简单

汉明距离总和中等

posted @ 2026-03-30 10:42  CodeMagicianT  阅读(4)  评论(0)    收藏  举报