136. 只出现一次的数字
问题描述
给定一个非空整数数组 nums
,其中某个元素只出现一次,其余每个元素均出现两次。找出那个只出现一次的元素。要求:
- 线性时间复杂度(O(n))
- 常数空间复杂度(O(1))
示例:
输入: [2,2,1]
输出: 1
输入: [4,1,2,1,2]
输出: 4
算法思路:位运算(异或)
利用异或运算(XOR)的三个性质:
- 归零律:任何数与自身异或结果为
0
(a ^ a = 0
) - 恒等律:任何数与
0
异或结果为自身(a ^ 0 = a
) - 交换律/结合律:异或操作满足交换律和结合律(
a ^ b ^ a = (a ^ a) ^ b = 0 ^ b = b
)
核心思想:
- 将数组中所有数字进行异或操作
- 成对出现的数字异或结果为
0
- 最终结果即为只出现一次的数字
代码实现
class Solution {
public int singleNumber(int[] nums) {
int result = 0;
// 遍历数组,对所有元素进行异或操作
for (int num : nums) {
result ^= num;
}
return result;
}
}
算法分析
- 时间复杂度:O(n)
- 只需遍历数组一次
- 空间复杂度:O(1)
- 仅使用一个额外变量存储结果
关键点
- 异或运算特性:
- 相同数字异或结果为
0
- 不同数字异或保留差异位
- 相同数字异或结果为
- 成对消除:
- 出现两次的数字相互抵消
- 最终剩余唯一出现一次的数字
- 边界处理:
- 题目保证数组非空,无需额外判空
- 当数组只有一个元素时直接返回该元素
算法过程
输入: [4, 1, 2, 1, 2]
计算过程:
0 ^ 4 = 4
4 ^ 1 = 5 (二进制 100 ^ 001 = 101)
5 ^ 2 = 7 (101 ^ 010 = 111)
7 ^ 1 = 6 (111 ^ 001 = 110)
6 ^ 2 = 4 (110 ^ 010 = 100)
返回: 4
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准示例
int[] nums1 = {2, 2, 1};
System.out.println("Test 1: " + solution.singleNumber(nums1)); // 1
// 测试用例2:标准示例
int[] nums2 = {4, 1, 2, 1, 2};
System.out.println("Test 2: " + solution.singleNumber(nums2)); // 4
// 测试用例3:单个元素
int[] nums3 = {5};
System.out.println("Test 3: " + solution.singleNumber(nums3)); // 5
// 测试用例4:负数
int[] nums4 = {-1, 3, -1, 3, 0};
System.out.println("Test 4: " + solution.singleNumber(nums4)); // 0
// 测试用例5:大数
int[] nums5 = {1000000, 1, 1000000, 2, 2};
System.out.println("Test 5: " + solution.singleNumber(nums5)); // 1
}
常见问题
-
为什么异或能解决这个问题?
- 异或操作满足交换律和结合律,相同数字异或结果为0,最终剩余唯一出现一次的数字。
-
如何处理负数?
- 异或操作直接作用于整数的二进制补码形式,对正负数处理机制相同。
-
如果数组中有多个只出现一次的数字会怎样?
- 题目限定只有一个单独出现的数字,其余都出现两次。若有多个单独数字,算法会返回它们的异或结果(不符合题目要求)。
-
能否用其他位运算解决?
- 异或是最优解。若使用其他方法(如哈希表),空间复杂度会达到O(n),不符合要求。
-
为什么初始化
result=0
?- 因为任何数与0异或等于自身,0作为初始值不影响最终结果(
0 ^ a = a
)。
- 因为任何数与0异或等于自身,0作为初始值不影响最终结果(
137. 只出现一次的数字 II
问题描述
给定一个整数数组 nums
,除某个元素仅出现一次外,其余每个元素都恰出现三次。找出并返回那个只出现一次的元素。
示例:
输入:nums = [2,2,3,2]
输出:3
输入:nums = [0,1,0,1,0,1,99]
输出:99
要求:
- 时间复杂度 O(n)
- 空间复杂度 O(1)
算法思路
方法一:位运算(按位统计)
-
核心思想
对于整数(32位),统计每一位上1出现的总次数:- 出现三次的数字在每一位上1的个数一定是3的倍数
- 只出现一次的数字在每一位上1的个数对3取模即为该位的值
-
操作步骤
- 遍历32位(int类型)
- 对每一位统计所有数字在该位上1的总和
- 若总和 mod 3 ≠ 0,说明只出现一次的数在该位为1
方法二:有限状态自动机
-
核心思想
使用两个变量(ones
、twos
)模拟三种状态:00
:出现0次01
:出现1次10
:出现2次- →
11
无效(自动转为00
)
-
状态转移
ones = (ones ^ num) & ~twos twos = (twos ^ num) & ~ones
- 遇到0时状态不变
- 遇到1时状态按
00→01→10→00
循环
代码实现
方法一:位运算(通用解法)
class Solution {
public int singleNumber(int[] nums) {
int res = 0;
// 遍历32位
for (int i = 0; i < 32; i++) {
int sum = 0; // 统计第i位1的总数
// 遍历所有数字
for (int num : nums) {
// 检查第i位是否为1:右移i位后与1
sum += (num >> i) & 1;
}
// 若该位1的总数不是3的倍数,则只出现一次的数在该位为1
if (sum % 3 != 0) {
// 将结果中该位置1:用或运算
res |= (1 << i);
}
}
return res;
}
}
注释:
(num >> i) & 1
:提取num
的第i
位值(0或1)sum % 3 != 0
:该位上的1无法被3整除,说明目标数该位为1res |= (1 << i)
:将结果中该位设为1
方法二:有限状态自动机(优化解法)
class Solution {
public int singleNumber(int[] nums) {
int ones = 0; // 记录出现1次的位
int twos = 0; // 记录出现2次的位
for (int num : nums) {
/*
* 更新ones:
* 1. 与num异或:记录出现奇数次的位
* 2. & ~twos:确保twos为1的位不参与更新
*/
ones = (ones ^ num) & ~twos;
/*
* 更新twos:
* 1. 与num异或:记录出现偶数次的位
* 2. & ~ones:确保ones更新后的状态不影响当前计算
*/
twos = (twos ^ num) & ~ones;
}
return ones; // 最终ones存储只出现一次的数
}
}
状态转移:
- 初始状态:
(ones, twos) = (00)
- 遇到第一个num:
ones = (00 ^ num) & ~00 = num
twos = (00 ^ num) & ~num = 00
→ 状态01
- 遇到第二个num:
ones = (num ^ num) & ~00 = 00
twos = (00 ^ num) & ~00 = num
→ 状态10
- 遇到第三个num:
ones = (00 ^ num) & ~num = 00
twos = (num ^ num) & ~00 = 00
→ 状态00
算法分析
方法 | 时间复杂度 | 空间复杂度 | 优势 |
---|---|---|---|
位运算 | O(32n)→O(n) | O(1) | 逻辑直观,通用性强 |
状态机 | O(n) | O(1) | 位操作高效,无循环嵌套 |
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 示例测试
int[] nums1 = {2, 2, 3, 2};
System.out.println("Test 1: " + solution.singleNumber(nums1)); // 3
int[] nums2 = {0, 1, 0, 1, 0, 1, 99};
System.out.println("Test 2: " + solution.singleNumber(nums2)); // 99
// 边界测试
int[] nums3 = {1}; // 单个元素
System.out.println("Test 3: " + solution.singleNumber(nums3)); // 1
int[] nums4 = {-1, -1, -1, 2}; // 含负数
System.out.println("Test 4: " + solution.singleNumber(nums4)); // 2
int[] nums5 = {3, 3, 3, 1, 1, 1, 5}; // 多个重复数
System.out.println("Test 5: " + solution.singleNumber(nums5)); // 5
}
关键点
-
位运算核心:
- 利用整数二进制表示独立处理每一位
- 出现次数与位值分离统计
-
状态机精髓:
- 双变量编码三种状态(00/01/10)
- 位操作实现状态转移原子性
-
负数处理:
- Java使用补码表示负数
- 按位操作对所有整数统一处理
-
通用性扩展:
- 方法一可直接扩展至"所有数字出现k次,仅一个出现m次"(修改
mod k
) - 方法二状态机需调整状态转移规则
- 方法一可直接扩展至"所有数字出现k次,仅一个出现m次"(修改
260. 只出现一次的数字 III
问题描述
给定一个整数数组 nums
,其中恰好有两个元素只出现一次,其余所有元素均出现两次。找出只出现一次的那两个元素。要求时间复杂度为 O(n),空间复杂度为 O(1)。
示例:
输入:nums = [1,2,1,3,2,5]
输出:[3,5]
解释:3 和 5 只出现一次,其他数字均出现两次
算法思路
核心思想:位运算 + 分组异或
-
整体异或
对所有元素进行异或操作,得到的结果xorAll
等于两个目标数(假设为a
和b
)的异或值:
xorAll = a ^ b
-
分离关键位
在xorAll
中任意选择一个值为 1 的位(通常选择最低位的 1),该位表明a
和b
在此位的值不同(一个为 0,一个为 1)。 -
分组异或
根据关键位将数组分为两组:- 关键位为 0 的组:包含
a
和所有在此位为 0 的重复元素 - 关键位为 1 的组:包含
b
和所有在此位为 1 的重复元素
分别对两组进行异或操作,即可得到a
和b
。
- 关键位为 0 的组:包含
代码实现
class Solution {
public int[] singleNumber(int[] nums) {
// 步骤1:计算所有数字的异或结果
int xorAll = 0;
for (int num : nums) {
xorAll ^= num;
}
// 步骤2:找到最低位的1(分离关键位)
int lowbit = xorAll & -xorAll;
// 步骤3:初始化两个结果
int a = 0, b = 0;
// 步骤4:分组异或
for (int num : nums) {
// 根据关键位分组
if ((num & lowbit) == 0) {
a ^= num; // 关键位为0的组
} else {
b ^= num; // 关键位为1的组
}
}
return new int[]{a, b};
}
}
注释
-
整体异或计算
int xorAll = 0; for (int num : nums) { xorAll ^= num; // 最终得到 a ^ b }
-
获取最低位的 1
int lowbit = xorAll & -xorAll;
-xorAll
是补码表示(按位取反后加 1)- 示例:若
xorAll = 6(二进制 110)
,则-xorAll = -6(补码 010)
110 & 010 = 010
→ 最低位的 1 是第 2 位
-
分组异或
if ((num & lowbit) == 0) { a ^= num; // 关键位为0的组 } else { b ^= num; // 关键位为1的组 }
- 重复元素会被分到同一组并相互抵消
- 目标数
a
和b
被分到不同组
算法分析
- 时间复杂度:O(n)
遍历数组两次(整体异或 + 分组异或) - 空间复杂度:O(1)
仅使用常数空间(xorAll
、lowbit
、a
、b
)
算法过程
输入:nums = [1,2,1,3,2,5]
-
整体异或:
1^2^1^3^2^5 = (1^1)^(2^2)^3^5 = 0^0^3^5 = 3^5 = 6(二进制 110)
-
分离关键位:
lowbit = 6 & -6 = 2(二进制 010)
-
分组异或:
数字 二进制 & 010
分组 组内异或结果 1 001 000 (0) a组 a=0^1=1
→1^1=0
→0^3=3
2 010 010 (≠0) b组 b=0^2=2
→2^2=0
→0^5=5
1 001 000 (0) a组 3 011 010 (≠0) b组 2 010 010 (≠0) b组 5 101 000 (0) a组 输出: [3, 5]
边界处理
- 负数处理:
Java 的补码机制确保位操作对负数有效 - 数组空值:
题目保证至少有两个元素,无需特殊处理 - 关键位唯一性:
最低位的 1 一定存在(因为a ≠ b
)
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 示例测试
int[] nums1 = {1,2,1,3,2,5};
System.out.println(Arrays.toString(solution.singleNumber(nums1))); // [3,5]
// 负数测试
int[] nums2 = {-1,0,0,-1,2,3};
System.out.println(Arrays.toString(solution.singleNumber(nums2))); // [2,3]
// 大数测试
int[] nums3 = {100000, -100000, 100000, 99999};
System.out.println(Arrays.toString(solution.singleNumber(nums3))); // [-100000,99999]
// 最小数组测试
int[] nums4 = {1, -1};
System.out.println(Arrays.toString(solution.singleNumber(nums4))); // [1,-1]
}
关键点
-
异或性质:
x ^ x = 0
0 ^ x = x
- 异或满足交换律和结合律
-
分组依据:
- 利用
a
和b
在某一二进制位的差异进行分组 - 重复元素因相同位值被分到同一组
- 利用
-
位操作技巧:
x & -x
快速获取最低位的 1(num & lowbit) == 0
判断特定位值
-
通用性扩展:
此方法可扩展至寻找更多个只出现一次的数字(需增加分组维度)