只出现一次的数字:从暴力美学到位运算神技的进化之路
面试官:"找出数组中那个落单的数字!"
你:😏 "简单,循环计数不就完事了?"
面试官:"要求时间复杂度 O(n),空间复杂度 O(1)。"
你:😱 "等等......容我三思!"
别慌! 这篇博客带你层层解剖这道经典题 (LeetCode 136/137/260) ,从"老实人暴力法"到"位运算魔法",全程幽默+代码注释,保证你笑着学会!👇
问题一:只出现一次的数字
题目 :数组中除一个数字外,其他都成对出现 ,找出那个"单身狗"。 要求:时间复杂度 O(n),空间复杂度 O(1)。
心路历程
- 第一反应:双循环点名法
"每个数字都点名,喊到一次的就是单身狗!"
痛点 :数组长度超 1000?电脑风扇开始直升机模式 → O(n²) 太慢!
- 灵光一闪:空间换时间
"不让双重循环?我用 HashMap 记小本本!"
痛点:时间降到 O(n),代价是空间 O(n),空间复杂度 O(n) 超 1000?内存说:我压力很大!
- 绝地求生:不用额外空间?
"Set 开关灯法:单身狗进来,情侣狗出去!"
痛点:Set 也算额外空间!题目白纸黑字**"O(1) 空间"** → Set 也算额外空间!💢
- 终极顿悟:位运算降临
"异或操作:成对数字相互抵消,单身狗闪耀登场!"
- 神技原理 :
a ^ a = 0
(情侣相见湮灭成灰 ☠️)a ^ 0 = a
(单身狗闪耀登场 🌟)- 空间 O(1):只用一个变量,内存感动哭了 😭
🐢 1. 双重循环法:老实人的暴力美学
思路:每个数字全员点名喊到!喊到一次的就是落单的。
java
class Solution {
public int singleNumber(int[] nums) {
for (int i = 0; i < nums.length; i++) {
int count = 0; // 计数器:统计 nums[i] 出现次数
for (int j = 0; j < nums.length; j++) {
if (nums[i] == nums[j]) count++; // 全员点名:相同数字就计数+1
}
if (count == 1) return nums[i]; // 只喊到1次?恭喜,单身狗就是你!
}
return -1; // 单身狗不存在?离谱!
}
}
- 优点:逻辑直白,幼儿园小朋友都能懂 👍
- 缺点 :慢得像乌龟赛跑!🐢 数组长一点就卡成 PPT(时间复杂度 O(n²) ) 适用场景: 数组长度 ≤10,或者你想体验计算机的愤怒 😈
📦 2. 哈希表法:空间换时间的土豪
思路:给每个数字发"签到牌"🎫,签到时计数,最后找只签一次的数字。
java
class Solution {
public int singleNumber(int[] nums) {
Map<Integer, Integer> countMap = new HashMap<>(); // 签到柜台:数字 → 出现次数
for (int num : nums) {
// getOrDefault:如果 num 没签过,默认0;签过就+1
countMap.put(num, countMap.getOrDefault(num, 0) + 1);
}
for (Map.Entry<Integer, Integer> entry : countMap.entrySet()) {
if (entry.getValue() == 1) return entry.getKey(); // 找到只签1次的靓仔!
}
return -1;
}
}
- 优点 :速度起飞 🚀 时间复杂度 O(n)
- 缺点 :额外开了个"签到柜台"(空间复杂度 O(n) ),内存说:我压力很大! 适用场景: 数据量大但内存管够,典型土豪行为 💰
⚖️ 3. 集合法:反复横跳的魔术师
思路:像开关灯💡,数字出现一次就开灯,出现两次就关灯,最后亮着的就是单身狗!
java
import java.util.HashSet;
class Solution {
public int singleNumber(int[] nums) {
Set<Integer> set = new HashSet<>();
for (int num : nums) {
if (set.contains(num)) set.remove(num); // 有对象?踢出去!
else set.add(num); // 单身?进来!
}
return set.iterator().next(); // 最后剩下的就是天选之子!
}
}
- 优点 :代码简洁如诗 📜 空间比哈希表省(最坏 O(n))
- 缺点 :反复添加删除,Set 表示很忙 🤹 骚操作点: 像不停开关灯,最后亮着的房间就是目标!💡
🧮 4. 求和法:数学课代表的推理
思路 :单身狗 = 2 × (所有不重复数字和) - 数组总和
java
class Solution {
public int singleNumber(int[] nums) {
Set<Integer> set = new HashSet<>();
int arraySum = 0;
for (int num : nums) {
arraySum += num; // 数组总和
set.add(num); // 所有数字进集合(去重)
}
int setSum = 0;
for (int num : set) setSum += num; // 集合总和
return 2 * setSum - arraySum; // 魔法公式!✨
}
}
- 原理 :
单身狗 = 2*(所有不重复数字和) - 数组总和
- 为什么? :因为成对的数字在
数组总和
里算了两次,减去后刚好抵消! - 缺点:代码啰嗦,数学不好容易绕晕 😵💫,且可能整数溢出(后面解决)
✨ 5. 异或法:终极位操作の神技!
思路 :用异或操作 ^
------ 成对数字相互抵消
java
class Solution {
public int singleNumber(int[] nums) {
int single = 0;
for (int num : nums) {
single ^= num; // 全程高能异或!
}
return single; // 结束,撒花🎉
}
}
-
优点:
- 速度 O(n) ,空间 O(1) !(原地工作,内存狂喜)
- 代码只有 3 行,面试官看了直呼内行!🤯
-
原理 :异或(
^
)有三定律:a ^ a = 0
(自己搞自己=消失)a ^ 0 = a
(和 0 搞=还是自己)交换律:a ^ b = b ^ a
所有成对的数字异或后全变 0,最后剩下单身狗!
-
面试装逼必备:一行代码秒杀 LeetCode 136!
java
class Solution {
public int singleNumber(int[] nums) {
return Arrays.stream(nums).reduce(0, (a, b) -> a ^ b);
}
}
🏆 终极对决:方法大乱斗(表格版)
方法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
---|---|---|---|---|
双重循环 🔄 | O(n²) | O(1) | 逻辑简单 | 慢到怀疑人生 |
哈希表 📦 | O(n) | O(n) | 速度稳 | 内存警告 |
集合 ⚖️ | O(n) | O(n) | 代码优雅 | 依赖集合特性 |
求和法 🧮 | O(n) | O(n) | 数学之美 | 啰嗦且可能溢出整数 |
异或法 ✨ | O(n) | O(1) | 又快又省 | 位运算抽象难懂 |
🚦 怎么选?看这里!
- 小白入门 👶:先玩 集合法 (代码好懂),再试 哈希表(理解计数)
- 效率狂魔 ⚡:无脑选 异或法!面试装逼必备 👔
- 数学爱好者 🎓:求和法 让你重温数学荣光(然后还是得用异或)
💡 记住这个灵魂总结
🌟 成对数字的克星 = 异或操作! 🌟 一行代码干翻全场:
java
Arrays.stream(nums).reduce(0, (a, b) -> a ^ b);
下次面试官问你找"单身狗",别再说暴力循环了,甩出异或大法,TA 的眼神会告诉你:
这波稳了! 😎
进阶一:137. 只出现一次的数字 II
题目进阶,要求其余元素出现次数由 2次 增加到 3次。
也就是说过了很久单身狗还是单身狗,但其他的都变成一家三口了
1.依旧是暴力美学(代码不变,但更慢了!)
依然基础思路,先双循环,统计每个数字出现次数。
代码不需要有任何修改(想想这是为什么)
java
class Solution {
public int singleNumber(int[] nums) {
for (int i = 0; i < nums.length; i++) {
int count = 0;
for (int j = 0; j < nums.length; j++) {
if (nums[i] == nums[j]) count++; // 每个数都全员点名!
}
if (count == 1) return nums[i]; // 喊到1次的出列!
}
return -1; // 单身狗不存在?离谱!
}
}
2.哈希的艺术(代码不变,内存警告升级 ⚠️)
这个的代码也不需要做任何修改,和之前的代码是一样的。
java
class Solution {
public int singleNumber(int[] nums) {
Map<Integer, Integer> countMap = new HashMap<>();
for (int num : nums) {
countMap.put(num, countMap.getOrDefault(num,0) + 1);
}
for (Map.Entry<Integer, Integer> entry : countMap.entrySet()) {
if (entry.getValue() == 1) {
return entry.getKey();
}
}
return -1;
}
}
3.集合的艺术 (这个时候的集合法其实是不太适用的,相对的,求和法只需改变最后的参数即可)
关键点 :在具体测试的过程中,我发现,会存在整数溢出的问题。
所以先把数组中的元素都转成
long
类型,再进行计算。最后返回的时候做一个类型的强制转化,
(int)
。
java
class Solution {
public int singleNumber(int[] nums) {
Set<Integer> set = new HashSet<>();
long arraySum = 0;
for (int num : nums) {
arraySum += num; // 使用 long 类型存储数组总和
set.add(num); // 所有数字进集合(去重)
}
long setSum = 0;
for (int num : set)
setSum += num; // 使用 long 类型存储集合总和
return (int) ((3 * setSum - arraySum) / 2); // 魔法公式!✨
}
}
4.位运算的艺术
思路:统计所有数字每个二进制位"1"出现的次数。
- 如果某位"1"出现次数 不是 3 的倍数 → 单身狗在该位是 1!
java
class Solution {
public int singleNumber(int[] nums) {
int[] count = new int[32]; // 存储每个二进制位"1"出现的次数
for (int num : nums) {
for (int i = 0; i < 32; i++) { // 遍历32位(int类型)
if (((num >> i) & 1) == 1) { // 检查第 i 位是否为 1
count[i]++; // 如果是,计数+1
}
}
}
int result = 0;
for (int i = 0; i < 32; i++) {
if (count[i] % 3 != 0) { // 不是3的倍数?单身狗在此位是1!
result |= (1 << i); // 将此位设为1
}
}
return result;
}
}
进阶二:260. 只出现一次的数字 III
题目 :有 两个 数字只出现一次,其他都出现两次。
1.依旧是双重暴力循环
java
class Solution {
public int[] singleNumber(int[] nums) {
int[] result = new int[2];
int index = 0;
for (int i = 0; i < nums.length; i++) {
int count = 0;
for (int j = 0; j < nums.length; j++) {
if (nums[i] == nums[j]) count++; // 每个数都全员点名!
}
if (count == 1) result[index++] = nums[i];// 喊到1次的出列!
}
return result; // 单身狗不存在?离谱!
}
}
2.哈希的艺术
java
class Solution {
public int[] singleNumber(int[] nums) {
Map<Integer, Integer> countMap = new HashMap<>();
for (int num : nums) {
countMap.put(num, countMap.getOrDefault(num, 0) + 1);
}
int[] result = new int[2];
int index = 0;
for (Map.Entry<Integer, Integer> entry : countMap.entrySet()) {
if (entry.getValue() == 1) {
result[index++] = entry.getKey();
}
}
return result;
}
}
3.位运算神技升级:分组异或
思路:
- 所有数字异或 → 得到
diff = dog1 ^ dog2
。- 找到
diff
中任意一个为 1 的位(比如最低位)。- 根据该位是 0 或 1,将数字分成两组 → 每组各含一只狗!
java
class Solution {
public int[] singleNumber(int[] nums) {
int xor = 0;
for (int num : nums) {
xor ^= num; // Step 1: 得到 dog1 ^ dog2
}
int diff = xor & (-xor); // Step 2: 取最低位的1(用于分组)
int[] result = new int[2]; // 存放两只狗
for (int num : nums) {
if ((num & diff) == 0) { // 分组:该位为0的进组1
result[0] ^= num; // 组内异或 → 得到 dog1
} else { // 该位为1的进组2
result[1] ^= num; // 组内异或 → 得到 dog2
}
}
return result;
}
}
🎯 终极总结:单身狗三连击指南
问题 | 推荐方法 | 核心技巧 |
---|---|---|
单只狗(136) | 异或法 ✨ | a ^ a = 0 ,a ^ 0 = a |
三胎家庭(137) | 位计数 | 统计每位1的个数 % 3 ≠ 0 |
两只狗(260) | 分组异或 | 用 diff & -diff 分组 |
记住:暴力循环是备用轮胎,位运算是兰博基尼! 🚗💨