给你一个整数数组nums,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。思路:采用异或运算'^'
1. 题目1
给你一个 非空 整数数组 nums
,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
输入 | nums = [2,2,1] | nums = [4,1,2,1,2] | nums = [1] |
输出 | 1 | 4 | 1 |
思路:使用异或运算'^'
1.1异或运算性质
- 任何数与0异或等于其本身:
a ^ 0 = a
- 任何数与自己异或等于0:
a ^ a = 0
- 满足交换律和结合律:
a ^ b ^ a = (a ^ a) ^ b = 0 ^ b = b
利用这些性质,我们可以将所有元素进行异或操作:
- 出现两次的元素会互相抵消(
a ^ a = 0
) - 只出现一次的元素会保留下来(
a ^ 0 = a
)
1.2 直观理解异或原理
4 ^ 1 ^ 2 ^ 1 ^ 2
= 4 ^ (1 ^ 1) ^ (2 ^ 2) // 交换律
= 4 ^ 0 ^ 0 // a ^ a = 0
= 4 // a ^ 0 = a
1.3 具体代码
class Solution {
public:
int singleNumber(vector& nums) {
int result = 0;
for (int num : nums) {
result ^= num;
}
return result;
}
};
2.题目2
给你一个整数数组 nums
,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。
输入 | nums = [1,2,1,3,2,5] | nums = [-1,0] | nums = [0,1] |
输出 | [3,5]或[5,3] | [-1,0]或[0,-1] | [1,0]或[0,1] |
思路:这个问题是经典"找出只出现一次元素"问题的扩展版。当只有一个元素出现一次时,我们可以简单使用异或运算解决。但这里有两个不同的元素,需要更巧妙的位运算技巧:
2.1 巧用位运算
第一次异或遍历:
- 将所有元素进行异或操作
- 出现两次的元素相互抵消(a^a=0)
- 最终结果等于两个目标元素的异或值(xorsum = a^b)
找到最低有效位(LSB):
- 计算
xorsum & (-xorsum)
获取最低位的1。 - 这个位是两个目标元素不同的最低有效位。
- 特殊处理
INT_MIN
避免溢出。
分组异或:
- 根据LSB将元素分为两组
- 每组包含一个目标元素和若干成对元素
- 分别异或后得到两个目标元素
2.2 LSB详解 xorsum & (-xorsum)
2.2.1 核心原理:补码表示法
在计算机中,整数使用补码表示:
-xorsum
等价于~xorsum + 1
(按位取反再加1)xorsum & (-xorsum)
的结果是:
- 保留
xorsum
最低位的1 - 其他所有位变为0
2.2.2 位运算过程
假设 xorsum = 12
xorsum = 00001100 (12)
~xorsum = 11110011 (按位取反)
-xorsum = 11110100 (~xorsum + 1)
----------------------
xorsum & (-xorsum)
00001100 (12)
&
11110100 (-12)
=
00000100 (4) // 仅保留最低位的1
2.2.3 数学意义
设 x = xorsum
:
-x = ~x + 1
- 在二进制中,
x & (~x + 1)
会:
- 将最低位的1右侧的所有0变成1,最低位的1变为0(通过
~x
),最低位左侧全部取反。 - 加1后使这些位重新变为0,最低为重新变为1
- 最终仅保留最低位的1:最低位左侧全部取反,&后均变为0,最低位右侧仍然全部为0,&后为0。只有最低位置保留1.
2.2.4 INT_MIN的特殊处理
INT_MIN
的二进制为1000...0
-INT_MIN
会溢出(因为|INT_MIN| = |INT_MAX| + 1
)- 当
xorsum == INT_MIN
时,直接使用其本身作为LSB
2.3 具体代码
#include
#include
using namespace std;
class Solution {
public:
vector singleNumber(vector& nums) {
int xorsum = 0;
for (int num : nums) {
xorsum ^= num;
}
// 获取最低有效位(特殊处理INT_MIN避免溢出)
int lsb = (xorsum == INT_MIN) ? xorsum : xorsum & (-xorsum);
int a = 0, b = 0;
for (int num : nums) {
if (num & lsb) {//根据LSB将元素分为两组
a ^= num; // 第一组:该位为1的元素。
} else {
b ^= num; // 第二组:该位为0的元素
}
}//相同的元素肯定分到一组,异或后变为0,最后每组剩余一个单值
return {a, b};
}
};
3. 题目3
给定一个整数数组,其中某个元素只出现一次,其余每个元素都恰好出现三次。要求设计一个线性时间复杂度且仅使用常量额外空间的算法,找出那个只出现一次的元素。
输入 | nums = [2,2,3,2] | nums = [0,1,0,1,0,1,99] |
输出 | 3 | 99 |
解题思路:这个问题是经典"找出唯一元素"问题的变种,但由于其他元素出现三次,简单的异或操作无法直接解决。我们需要更高级的位运算技巧.
3.1 状态机
定义:
有限状态机(Finite-State Machine,FSM),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型,具体点来说,它是一个有向图形,由一组节点和一组相应的转移函数组成。状态机通过响应一系列事件而“运行”。每个事件都在属于“当前” 节点的转移函数的控制范围内,其中函数的范围是节点的一个子集。函数返回“下一个”(也许是同一个)节点。这些节点中至少有一个必须是终态。当到达终态, 状态机停止。
举例:
灯泡只有两个状态,开和关,当它处于关的状态是,你给它一个开的指令,它就会变成开的状态,再给它一个关的的指令,它就会变成关的状态。
(转载https://juejin.cn/post/7131643903987417095)
在本题的应用:以二进制的角度,将出现3次的数对第i位求和,一定是3的倍数,将出现3次的数和出现1次的数第i位置求和sum,对sum求余,则余数是出现1次数的第i位。把所有数字的二进制位上的值都加起来,再分别对 3 求余,那么结果就是只出现了一次的那个数字
3.2 具体代码
class Solution {
public:
int singleNumber(vector& nums) {
int ans = 0;
for (int i = 0; i > i) & 1);//通过数字右移和按位与操作,获取第i位的值
}
if (total % 3) {
ans |= (1 << i);//通过按位或操作设置结果的第i位为1
}
}
return ans;
}
};