只出现一次的数字:从暴力美学到位运算神技的进化之路
面试官:"找出数组中那个落单的数字!"
你:😏 "简单,循环计数不就完事了?"
面试官:"要求时间复杂度 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 分组 | 
记住:暴力循环是备用轮胎,位运算是兰博基尼! 🚗💨