【C++】只出现一次的数字 III:位运算的巧妙应用

一、题目分析

问题描述

给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。找出只出现一次的那两个元素。

示例

text

复制代码
输入: [1,2,1,3,2,5]
输出: [3,5]

限制条件

  • 时间复杂度 O(n)

  • 空间复杂度 O(1)(不考虑返回结果占用的空间)

  • 元素的顺序任意

二、位运算基础知识

1. 异或运算 (XOR) 的特性

cpp

复制代码
a ^ a = 0      // 相同数字异或为0
a ^ 0 = a      // 任何数与0异或为本身
a ^ b = b ^ a  // 交换律
a ^ b ^ c = a ^ (b ^ c) = (a ^ b) ^ c  // 结合律

2. 补码和取反操作

cpp

复制代码
// 对于任意整数x:
-x = ~x + 1    // 补码表示
x & (-x)       // 获取x最右边的1

3. 位运算常用技巧

cpp

复制代码
// 1. 判断奇偶
bool isOdd = (x & 1) == 1;

// 2. 清零最低位的1
x = x & (x - 1);

// 3. 获取最低位的1
int lowbit = x & (-x);

// 4. 判断某一位是否为1
bool bitSet = (x >> k) & 1;

// 5. 设置某一位为1
x |= (1 << k);

// 6. 设置某一位为0
x &= ~(1 << k);

三、解题思路详解

问题转换

如果数组中只有一个数字出现一次,我们可以用异或轻松解决:

cpp

复制代码
int singleNumber(vector<int>& nums) {
    int result = 0;
    for (int num : nums) {
        result ^= num;
    }
    return result;
}

但本题有两个只出现一次的数字(设为a和b),直接异或得到的是 a ^ b

核心思路

  1. 获取两个数字的差异位

    • diff = a ^ b 的结果中,为1的位表示a和b在该位不同
  2. 分离这两个数字

    • 找到diff中任意一个为1的位(通常用最右边的1)

    • 根据这个位将数组分成两组

    • 在每组中,相同的数字会被分到同一组,而a和b会被分到不同组

四、代码逐步解析

cpp

复制代码
class Solution {
public:
    vector<int> singleNumber(vector<int>& nums) {
        // 1. 计算两个只出现一次数字的异或值
        long long diff = 0;
        for (int num : nums) {
            diff ^= num;
        }
        // 此时 diff = a ^ b
        
        // 2. 找到最右边为1的位(两个数字在这一位不同)
        diff &= -diff;
        // 例如:diff = 6(110),-diff = -6(010),相与得010
        
        // 3. 根据这一位将数组分成两组,分别异或
        vector<int> result(2, 0);
        for (int num : nums) {
            if ((num & diff) == 0) {
                result[0] ^= num;  // 这一位为0的组
            } else {
                result[1] ^= num;  // 这一位为1的组
            }
        }
        
        return result;
    }
};

详细分析

第一步:获取异或值

cpp

复制代码
long long diff = 0;
for (int num : nums) {
    diff ^= num;
}
  • 出现两次的数字相互抵消(x ^ x = 0

  • 最后 diff = a ^ b,其中a和b是两个只出现一次的数字

第二步:分离关键位

cpp

复制代码
diff &= -diff;

这个操作是算法的核心,让我们详细分析:

假设 a = 3(011), b = 5(101),则:

text

复制代码
a ^ b = 110 (二进制,6的十进制)

计算 diff & -diff

text

复制代码
diff  = 110
-diff = 010 (二进制补码)
相与得: 010

为什么这样做?

  • -diffdiff 的二进制补码(取反加1)

  • 这个操作保留了 diff 中最右边的1,其他位都变为0

  • 这个1的位置就是a和b在二进制表示中第一个不同的位

第三步:分组异或

cpp

复制代码
vector<int> result(2, 0);
for (int num : nums) {
    if ((num & diff) == 0) {
        result[0] ^= num;
    } else {
        result[1] ^= num;
    }
}

以示例 [1,2,1,3,2,5]diff = 2(010)

  • 分组1(第1位为0):[1(001), 1(001), 5(101)]

    • 1 ^ 1 ^ 5 = 5
  • 分组2(第1位为1):[2(010), 3(011), 2(010)]

    • 2 ^ 3 ^ 2 = 3

五、算法正确性证明

为什么能正确分组?

设两个目标数为a和b,diff = a ^ b

  1. diff 中至少有一个1(否则a=b,与题意矛盾)

  2. diff 的最右边1在第k位

  3. 那么a和b在第k位一定不同(一个为0,一个为1)

  4. 所有其他数字出现两次,相同的数字在第k位相同,会被分到同一组

为什么分组后每组只有一个目标数?

  • a和b在第k位不同,所以会被分到不同组

  • 每组中其他数字都成对出现,异或后为0

  • 每组最终只剩下一个目标数

六、复杂度分析

时间复杂度

  • O(n):遍历数组两次

    • 第一次:计算异或值

    • 第二次:分组异或

  • 每个元素只处理一次,位运算操作是O(1)

空间复杂度

  • O(1):只使用了固定大小的额外空间

    • diff:存储异或结果

    • result:存储返回结果(不计入空间复杂度)

七、边界情况处理

cpp

复制代码
// 测试用例示例
vector<int> testCases[] = {
    {1, 2, 1, 3, 2, 5},  // [3, 5]
    {-1, 0},              // [-1, 0]
    {0, 1, 0, 2},         // [1, 2]
    {1, 1, 0, 2147483647} // [0, 2147483647]
};

// 需要注意的特殊情况
// 1. 负数处理:使用long long避免溢出
// 2. 大数处理:异或运算对大数同样适用
// 3. 最小负数:-2147483648的特殊性

八、算法优化与变体

1. 使用int代替long long

cpp

复制代码
int diff = 0;
for (int num : nums) {
    diff ^= num;
}
diff &= -diff;  // 对于int,当diff为INT_MIN时,-diff会溢出

解决方法

cpp

复制代码
// 方法1:转换为unsigned int
unsigned int udiff = static_cast<unsigned int>(diff);
udiff &= -udiff;
diff = static_cast<int>(udiff);

// 方法2:使用其他方法找到最右边的1
int mask = 1;
while ((diff & mask) == 0) {
    mask <<= 1;
}

2. 不使用额外数组

cpp

复制代码
vector<int> singleNumber(vector<int>& nums) {
    int diff = 0;
    for (int num : nums) {
        diff ^= num;
    }
    
    // 找到最右边的1
    int rightmost = diff & (~diff + 1);
    
    int a = 0, b = 0;
    for (int num : nums) {
        if (num & rightmost) {
            a ^= num;
        } else {
            b ^= num;
        }
    }
    
    return {a, b};
}

九、实际应用场景

1. 错误检测与纠正

  • 奇偶校验

  • CRC校验

  • RAID存储系统

2. 加密算法

  • 简单异或加密

  • 流密码基础

3. 数据处理

  • 快速查找数组中缺失的数字

  • 统计不同元素的个数

4. 硬件设计

  • 数字电路中的比较器

  • 校验和计算

十、相关题目

  1. 只出现一次的数字 I:所有数字出现两次,只有一个出现一次

  2. 只出现一次的数字 II:所有数字出现三次,只有一个出现一次

  3. 找出重复和缺失的数字

  4. 交换两个变量的值(不使用临时变量)

cpp

复制代码
// 不使用临时变量交换两个数
void swap(int &a, int &b) {
    a = a ^ b;
    b = a ^ b;
    b = a ^ b;
}

十一、总结

本题展示了位运算在算法设计中的强大威力。通过巧妙的异或操作和位掩码技术,我们能够在O(n)时间复杂度和O(1)空间复杂度内解决问题。

关键点回顾:

  1. 异或运算的消去特性:相同数字异或为0

  2. 补码的性质x & (-x)获取最右边的1

  3. 分组思想:根据某个特征将问题分解

学习建议:

  1. 理解位运算的基本原理和性质

  2. 掌握常用的位运算技巧

  3. 练习相关的位运算题目,培养位运算思维

  4. 注意边界条件和特殊情况处理

位运算不仅是解决算法问题的利器,也是理解计算机底层原理的重要窗口。掌握位运算,能够帮助我们写出更高效、更优雅的代码。

相关推荐
客卿1231 小时前
岛屿问题--bfs的应用--二维网络题目学习
学习·算法·宽度优先
2401_900151542 小时前
代码覆盖率工具实战
开发语言·c++·算法
进击的小头2 小时前
第8篇:PI控制器设计实战演练
c语言·python·mcu·算法
bu_shuo2 小时前
在命令行中编译cpp文件
开发语言·c++·cpp
乌萨奇也要立志学C++2 小时前
【洛谷】图论 图论最短路算法全解:从单源 Dijkstra 到多源 Floyd 模板与实战
算法·图论
AI科技星2 小时前
基于v=c空间本底光速螺旋运动的宏观力方向第一性原理推导:太阳系与地球系统的全维度观测验证
人工智能·线性代数·算法·机器学习·平面
Epiphany.5562 小时前
炸雷!(地址映射+dfs搜索)
算法
Crazyong2 小时前
FreeRTOS-互斥量-2
算法
CAACoder2 小时前
CATIA/3DE CAA二次开发-ScrollWindow滚动窗口
开发语言·c++·mfc·滚动窗口