前提
异或操作的性质
- a ^ a = 0:任何数与自己异或结果为0。
- a ^ 0 = a:任何数与0异或结果为它自己。
- 异或操作是 交换律 和 结合律 的,也就是说,顺序可以变化,结果不变。
第一题
给你一个 非空 整数数组 nums
,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
class Solution {
public:
int singleNumber(vector<int>& nums) {
int n = 0;
for(auto e : nums) {
n ^= e;
}
return n;
}
};
这个简单,理解了上面异或操作的性质就能做
例子
假设输入的数组是:
vector<int> nums = {4, 1, 2, 1, 2};
- 初始时,
n = 0
。 - 遍历数组并进行异或:
n ^= 4
→n = 4
n ^= 1
→n = 5
(4 ^ 1 = 5)n ^= 2
→n = 7
(5 ^ 2 = 7)n ^= 1
→n = 6
(7 ^ 1 = 6)n ^= 2
→n = 4
(6 ^ 2 = 4)
- 最后
n = 4
,这个就是数组中只出现一次的数字。
第二题
给你一个整数数组 nums
,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
137. 只出现一次的数字 II - 力扣(LeetCode)
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ans = 0; // 用来存储结果
for(size_t i = 0; i < 32; i++) { // 遍历每一位
int t = 0; // 用来统计第 i 位的 1 的数量
for(auto num : nums) { // 遍历数组中的每个元素
t += ((num >> i) & 1); // 判断第 i 位是否为 1
}
if (t % 3) { // 如果该位的 1 的个数不能被 3 整除,说明该位属于唯一的那个数字
ans |= (1 << i); // 将答案的第 i 位设为 1
}
}
return ans; // 返回结果
}
};
算法的核心思想
-
位运算技巧:由于每个数字的每一位在数组中出现的次数都是3次,除了那个只出现一次的数字。通过统计每一位上的 1 的出现次数,我们就能找出只出现一次的数字。
-
逐位统计:我们逐位处理每个数字,从最低位到最高位(32 位,假设为 32 位整数)。对于每一位,统计该位上所有数字为 1 的个数。如果该位的 1 的个数不是 3 的倍数,那么那个只出现一次的数字在这一位上的值就是 1。
-
结果构造:在逐位统计完成后,我们将这些位上的 1 汇聚起来得到最终的结果。
ans
将用于存储最终的结果,即那个只出现一次的数字。- 外层循环遍历每一位,从 0 位到 31 位(假设整数是 32 位)。
num >> i
:将num
右移i
位,将我们关心的第i
位移到最低位。(num >> i) & 1
:通过按位与操作判断该位是否为 1。- 如果是 1,则
t
累加。t % 3
表示第i
位上 1 的个数是否为 3 的倍数。如果不是 3 的倍数,说明这位是唯一的那个数字的对应位。ans |= (1 << i)
:将结果ans
的第i
位设置为 1。- 最终,
ans
存储了只出现一次的数字。
举例分析
假设输入数组是 [2, 2, 3, 2]
:
-
二进制表示:
- 2 的二进制表示是
0000 0010
。 - 3 的二进制表示是
0000 0011
。
- 2 的二进制表示是
-
逐位统计:
- 第 0 位:
t = 1
(只有 3 的最后一位是 1)。 - 第 1 位:
t = 1
(只有 3 的倒数第二位是 1)。
- 第 0 位:
-
ans
最终会是 3。
第三题
给你一个整数数组 nums
,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。
你必须设计并实现线性时间复杂度的算法且仅使用常量额外空间来解决此问题。
260. 只出现一次的数字 III - 力扣(LeetCode)
class Solution {
public:
vector<int> singleNumber(vector<int>& nums) {
// Step 1: 求 XOR 所有元素
int xorsum = 0;
for (int num: nums) {
xorsum ^= num; // XOR 所有数字
}
// Step 2: 找到 xorsum 的最低有效位 (lsb)
// 如果 xorsum 是 INT_MIN (即只有最高位为 1),此时 xorsum == INT_MIN 时最低有效位仍然是 xorsum 自身
int lsb = (xorsum == INT_MIN ? xorsum : xorsum & (-xorsum));
// Step 3: 根据最低有效位将所有数字分成两组
int type1 = 0, type2 = 0;
for (int num: nums) {
if (num & lsb) {
type1 ^= num; // 该位为 1 的组
}
else {
type2 ^= num; // 该位为 0 的组
}
}
// Step 4: 返回结果
return {type1, type2};
}
};
算法的核心思想
-
异或运算:首先,所有数字进行 异或 运算。由于
a ^ a = 0
,0 ^ b = b
,所以最后的结果xorsum
将是那两个只出现一次的数字的 异或 结果。 -
分组:通过分析
xorsum
的最低有效位(LSB, least significant bit),可以把所有数字分成两组:- 一组的数字在该位上为 1;
- 另一组的数字在该位上为 0。
由于这两个唯一出现的数字在该位上肯定是不同的(否则它们会在 XOR 运算中消除),通过该位可以区分这两个数字。
-
分别求解:然后我们根据这个位将数组中的数字分为两组,对每组进行 XOR 操作,就能得到这两个唯一的数字。
xorsum
存储了所有数字的 异或结果。因为所有重复的数字会相互消除(a ^ a = 0
),最后的xorsum
就是那两个只出现一次的数字的 异或。xorsum & (-xorsum)
计算出xorsum
的最低有效位(即值为 1 的最低位)。这个位不同于这两个唯一出现的数字,所以用它来将数字分成两组。- 如果
xorsum
是INT_MIN
(即xorsum
只有最高位为 1),需要特别处理(因为INT_MIN
的补码是它自身)。- 根据最低有效位,将所有元素分为两组:
type1
存储了那些在该位上为 1 的数字的 XOR。type2
存储了那些在该位上为 0 的数字的 XOR。- 每一组内,除了那两个唯一出现的数字之一,其他数字都已经被 XOR 运算消除(因为它们都出现了两次)。
举例分析
假设输入数组是 [1, 2, 1, 3, 2, 5]
。
-
XOR 所有元素:
xorsum = 1 ^ 2 ^ 1 ^ 3 ^ 2 ^ 5 = 3 ^ 5 = 6
,所以xorsum = 6
。
-
找到最低有效位:
xorsum = 6
的二进制表示是0110
。最低有效位是第 1 位(从右数第 1 位)。lsb = 6 & (-6) = 2
。
-
分组 XOR:
- 将所有数字分成两组,第一组是第 1 位为 1 的数字(3 和 5),第二组是第 1 位为 0 的数字(1 和 2)。
- 第一组:
type1 = 3 ^ 5 = 6
。 - 第二组:
type2 = 1 ^ 2 = 3
。
-
返回结果:
- 最终,返回
{3, 5}
,即这两个只出现一次的数字。
- 最终,返回