想系统提升编程能力、查看更完整的学习路线,欢迎访问 AI Compass:github.com/tingaicompa… 仓库持续更新刷题题解、Python 基础和 AI 实战内容,适合想高效进阶的你。
📖 第104课:只出现一次的数字
模块:高级技巧 | 难度:Easy ⭐⭐⭐ LeetCode 链接:leetcode.cn/problems/si… 前置知识:无 预计学习时间:15分钟
🎯 题目描述
给你一个非空整数数组 nums,除了某个元素只出现一次外,其余每个元素均出现两次。找出那个只出现了一次的元素。
要求:必须设计并实现线性时间复杂度的算法,且只使用常量级别的额外空间。
示例:
输入:nums = [2,2,1]
输出:1
输入:nums = [4,1,2,1,2]
输出:4
输入:nums = [1]
输出:1
约束条件:
- 1 <= nums.length <= 30000
- -30000 <= nums[i] <= 30000
- 除了某个元素只出现一次外,其余每个元素均出现两次
🧪 边界用例(面试必考)
| 用例类型 | 输入 | 期望输出 | 考察点 |
|---|---|---|---|
| 最小输入 | nums=[1] | 1 | 单元素数组 |
| 顺序排列 | nums=[1,1,2,2,3] | 3 | 有序情况 |
| 乱序排列 | nums=[5,3,5,1,1] | 3 | 无序情况 |
| 负数 | nums=[-1,-1,0] | 0 | 负数和零 |
| 大规模 | n=30000 | — | 性能边界 |
💡 思路引导
生活化比喻
想象你在整理一堆配对的袜子,所有袜子都是成双成对的,只有一只袜子落单了。
🐌 笨办法:把所有袜子一只只拿出来,用笔记本记录每只袜子出现几次,最后找出只出现一次的。这需要一个很大的笔记本(O(n)空间),而且要翻来翻去查找(慢)。
🚀 聪明办法:利用"相同的袜子互相抵消"的魔法!把袜子两两配对时,相同的就"消失",最后剩下的必然是落单的那只。这个"抵消魔法"就是异或运算——相同为0,不同为1。
关键洞察
异或运算的三大性质:
- 任何数和0异或等于它本身:a ⊕ 0 = a
- 任何数和自己异或等于0:a ⊕ a = 0
- 异或运算满足交换律和结合律:a ⊕ b ⊕ a = b
利用性质2和3,所有成对的数字会自动抵消为0,最后只剩下落单的数字!
🧠 解题思维链
Step 1:理解题目 → 锁定输入输出
- 输入:整数数组 nums,除一个元素外其余均出现两次
- 输出:只出现一次的那个元素(整数)
- 限制:必须 O(n) 时间 + O(1) 空间
Step 2:先想笨办法(暴力法)
用哈希表统计每个数字的出现次数,最后找出次数为1的:
- 时间复杂度:O(n)
- 空间复杂度:O(n)
- 瓶颈在哪:需要额外的哈希表存储计数,违反O(1)空间要求
Step 3:瓶颈分析 → 优化方向
核心问题:如何在不使用额外空间的情况下找到答案?
关键观察:
- 所有成对的数字是"冗余信息",需要被"消除"
- 只有落单的数字是"有效信息",需要被"保留"
能否找到一种操作,让成对的数字互相抵消,而不影响落单的数字?
Step 4:选择武器
- 选用:位运算 — 异或(XOR)
- 理由:异或运算天然具备"相同抵消,不同保留"的特性,且是原地操作(O(1)空间)
🔑 模式识别提示:当题目出现"成对出现,找单独"或"奇偶次数问题",优先考虑"异或运算"
🔑 解法一:哈希表统计(直觉法)
思路
用字典统计每个数字的出现次数,最后遍历字典找出次数为1的元素。
图解过程
nums = [4,1,2,1,2]
Step 1: 构建计数字典
遍历数组,统计频率
{4:1, 1:2, 2:2}
Step 2: 找出频率为1的元素
遍历字典,返回值为1的键
→ 4
Python代码
from typing import List
def singleNumber_hashmap(nums: List[int]) -> int:
"""
解法一:哈希表统计
思路:统计每个数字出现次数,找出次数为1的
"""
# 步骤1:构建计数字典
count = {}
for num in nums:
count[num] = count.get(num, 0) + 1
# 步骤2:找出频率为1的元素
for num, freq in count.items():
if freq == 1:
return num
# ✅ 测试
print(singleNumber_hashmap([2, 2, 1])) # 期望输出:1
print(singleNumber_hashmap([4, 1, 2, 1, 2])) # 期望输出:4
print(singleNumber_hashmap([1])) # 期望输出:1
复杂度分析
- 时间复杂度:O(n) — 遍历两次数组,第一次建表,第二次查找
- 具体地说:如果输入规模 n=10000,大约需要 20000 次操作
- 空间复杂度:O(n) — 需要哈希表存储 n 个元素的计数
优缺点
- ✅ 思路清晰,代码易懂
- ✅ 适用于更复杂的变体(比如找出现奇数次的所有元素)
- ❌ 违反题目要求的 O(1) 空间限制
🏆 解法二:异或运算(最优解)
优化思路
利用异或运算的"自反性"(a ⊕ a = 0)和"交换律",把所有数字异或起来,成对的数字会自动抵消为0,最后只剩下单独的数字。
💡 关键想法:把数组中所有数字看作一个"异或链",相同的数字会两两抵消,顺序无关!
图解过程
nums = [4,1,2,1,2]
把所有数字异或起来:
4 ⊕ 1 ⊕ 2 ⊕ 1 ⊕ 2
利用交换律重新排列(相同的放一起):
4 ⊕ (1 ⊕ 1) ⊕ (2 ⊕ 2)
利用自反性(a ⊕ a = 0):
4 ⊕ 0 ⊕ 0
利用性质(a ⊕ 0 = a):
4 ← 答案!
位运算过程(二进制视角):
4: 100
1: 001
⊕ 101
2: 010
⊕ 111
1: 001
⊕ 110
2: 010
⊕ 100 → 4
Python代码
def singleNumber(nums: List[int]) -> int:
"""
解法二:异或运算(最优解)
思路:所有数字异或,成对的抵消为0,剩下单独的数字
"""
result = 0
for num in nums:
result ^= num # 持续异或每个数字
return result
# ✅ 测试
print(singleNumber([2, 2, 1])) # 期望输出:1
print(singleNumber([4, 1, 2, 1, 2])) # 期望输出:4
print(singleNumber([1])) # 期望输出:1
print(singleNumber([-1, -1, 0])) # 期望输出:0
复杂度分析
- 时间复杂度:O(n) — 只需遍历一次数组,每次异或是 O(1) 操作
- 具体地说:如果输入规模 n=10000,大约需要 10000 次异或操作
- 空间复杂度:O(1) — 只需要一个整数变量 result,与输入规模无关
为什么是最优解:
- 时间复杂度 O(n) 已经是理论最优(必须至少看一遍所有元素)
- 空间复杂度 O(1) 满足题目的严格要求
- 一次遍历,无需排序或额外数据结构
🐍 Pythonic 写法
利用 functools.reduce 的函数式写法:
from functools import reduce
from operator import xor
def singleNumber_pythonic(nums: List[int]) -> int:
"""
Pythonic 写法:用 reduce 和 xor 操作符
"""
return reduce(xor, nums)
# 测试
print(singleNumber_pythonic([4, 1, 2, 1, 2])) # 输出:4
解释:
reduce(xor, nums)相当于nums[0] ^ nums[1] ^ nums[2] ^ ...operator.xor是 Python 内置的异或函数- 一行代码完成任务,展示函数式编程功底
⚠️ 面试建议:先写清晰的循环版本展示思路,再提 Pythonic 写法展示语言功底。面试官更看重你的思考过程,而非代码行数。
📊 解法对比
| 维度 | 解法一:哈希表 | 🏆 解法二:异或运算(最优) |
|---|---|---|
| 时间复杂度 | O(n) | O(n) ← 时间最优 |
| 空间复杂度 | O(n) | O(1) ← 空间最优 |
| 代码难度 | 简单 | 简单 |
| 面试推荐 | ⭐ | ⭐⭐⭐ ← 首选 |
| 适用场景 | 通用统计问题 | 本题最优解,满足所有约束 |
为什么异或是最优解:
- 同时达到时间 O(n) 和空间 O(1) 的最优标准
- 代码极简(3行核心代码),易于实现和记忆
- 展示了位运算的巧妙应用,是面试高频考点
面试建议:
- 先用30秒口述哈希表思路(O(n)空间),表明你能想到基本解法
- 立即指出题目要求 O(1) 空间,需要优化
- 重点讲解🏆最优解(异或运算),阐述三大性质和推导过程
- 强调为什么这是最优:时间空间双重最优,无法进一步优化
- 手动模拟一个示例,展示异或的"抵消"过程
🎤 面试现场
模拟面试中的完整对话流程,帮你练习"边想边说"。
面试官:请你解决一下这道题。
你:(审题30秒)好的,这道题要求在数组中找出只出现一次的元素,其他元素都出现两次。我的第一个想法是用哈希表统计每个数字的频率,最后找出频率为1的,时间复杂度是 O(n),但空间复杂度也是 O(n)。
不过我注意到题目明确要求空间复杂度必须是 O(1),所以需要优化。我可以利用异或运算的性质来做到:相同的数字异或为0,而 0 异或任何数等于那个数本身。这样把所有数字异或起来,成对的会自动抵消,最后剩下的就是答案。时间 O(n),空间 O(1)。
面试官:很好,请写一下代码。
你:(边写边说)我用一个 result 变量初始化为0,然后遍历数组,把每个数字异或到 result 上。因为异或满足交换律和结合律,所以顺序无关,成对的数字会自动抵消为0。
def singleNumber(nums):
result = 0
for num in nums:
result ^= num
return result
面试官:测试一下?
你:用示例 [4,1,2,1,2] 走一遍:
- 初始 result = 0
- 0 ^ 4 = 4
- 4 ^ 1 = 5
- 5 ^ 2 = 7
- 7 ^ 1 = 6 (第二个1和第一个1抵消效果)
- 6 ^ 2 = 4 (第二个2和第一个2抵消效果)
- 最终结果:4 ✓
再测一个边界情况 [1],只有一个元素:
- 0 ^ 1 = 1 ✓
高频追问
| 追问 | 应答策略 |
|---|---|
| "如果有三个数字,其中两个出现三次,一个出现一次,怎么办?" | "异或不适用了,因为 a⊕a⊕a = a 不为0。需要改用位统计:统计每个位上1出现的次数,如果次数不是3的倍数,说明单独的数字在该位为1。时间 O(n),空间 O(1)。" |
| "为什么异或运算 a⊕a=0?" | "从二进制角度看,异或是不进位相加:相同位为0,不同位为1。所以两个相同数字每一位都相同,异或后全为0。" |
| "还有其他位运算的经典应用吗?" | "有!比如判断奇偶(n&1),乘以2(n<<1),除以2(n>>1),交换两数(a^=b, b^=a, a^=b),判断2的幂(n&(n-1)==0)。" |
| "能否不用异或,用数学方法?" | "可以!用 2×(集合元素和) - (数组元素和) = 答案。但需要转换为集合(O(n)空间),且可能溢出,不如异或简洁。" |
🎓 知识点总结
Python技巧卡片 🐍
# 技巧1:异或运算符 ^ — 位级异或
a = 5 # 二进制 101
b = 3 # 二进制 011
c = a ^ b # 二进制 110,十进制 6
# 技巧2:快速交换两数(无需中间变量)
a, b = 5, 3
a ^= b # a = 5^3
b ^= a # b = 3^(5^3) = 5
a ^= b # a = (5^3)^5 = 3
# 现在 a=3, b=5
# 技巧3:检查两数是否相等(位级)
def are_equal(a, b):
return (a ^ b) == 0 # 相同则异或为0
# 技巧4:找出不同的位
def diff_bits(a, b):
return bin(a ^ b).count('1') # 统计1的个数
💡 底层原理(选读)
异或运算为什么这么神奇?
从逻辑电路角度看,异或门(XOR gate)是数字电路的基本门电路之一:
- 输入相同 → 输出0
- 输入不同 → 输出1
数学性质:
- 交换律:a ⊕ b = b ⊕ a
- 结合律:(a ⊕ b) ⊕ c = a ⊕ (b ⊕ c)
- 恒等律:a ⊕ 0 = a
- 归零律:a ⊕ a = 0
- 自反律:a ⊕ b ⊕ b = a
应用场景:
- 加密算法(简单的异或加密)
- 奇偶校验(检测数据传输错误)
- 位图操作(图像处理)
- 内存交换(无需临时变量)
- 找出数组中的特殊元素(本题)
Python 中的位运算符:
&按位与(AND):都为1才为1|按位或(OR):有1就为1^按位异或(XOR):不同为1~按位取反(NOT):0变1,1变0<<左移:相当于乘以2>>右移:相当于除以2
算法模式卡片 📐
- 模式名称:异或性质 — 成对抵消
- 适用条件:数组中元素成对出现,需找出单独的元素或奇偶次数问题
- 识别关键词:"只出现一次"、"成对出现"、"奇数次/偶数次"、"O(1)空间"
- 模板代码:
def find_single_element(nums):
"""异或模板:找出只出现一次的元素"""
result = 0
for num in nums:
result ^= num # 所有数字异或,成对抵消
return result
易错点 ⚠️
-
误用其他位运算
- 错误:用按位与(&)或按位或(|)
- 原因:只有异或(^)具有"自反性"(a^a=0)
- 正确:始终使用异或运算符
^
-
忘记初始化为0
- 错误:
result不初始化或初始化为其他值 - 原因:异或的恒等元是0(a^0=a),初始化为0才能保证正确性
- 正确:
result = 0
- 错误:
-
边界情况漏测
- 错误:只测试正数,忘记测试负数和零
- 原因:负数在计算机中用补码表示,异或同样适用
- 正确:测试用例应包含
[-1,-1,0]等
-
复杂度分析错误
- 错误:认为异或运算是 O(log n) 或 O(32)
- 原因:整数异或是固定时间的 O(1) 操作,与数值大小无关
- 正确:整体时间复杂度就是循环的 O(n)
🏗️ 工程实战(选读)
这个算法思想在真实项目中的应用,让你知道"学了有什么用"。
-
场景1:数据完整性校验
- RAID 5 磁盘阵列使用异或运算生成校验盘,任一磁盘损坏可通过异或恢复数据
- 示例:如果有磁盘 A、B、C,校验盘 P = A ⊕ B ⊕ C,若 B 损坏,可通过 B = A ⊕ C ⊕ P 恢复
-
场景2:简单加密
- 异或加密是最简单的对称加密:密文 = 明文 ⊕ 密钥,解密时再异或一次密钥即可
- 示例:消息 M 用密钥 K 加密为 C=M⊕K,解密时 M=C⊕K
-
场景3:网络传输奇偶校验
- 在串口通信中,用所有字节异或结果作为校验和,检测传输错误
- 示例:发送 [0x12, 0x34, 0x56],校验和 = 0x12 ⊕ 0x34 ⊕ 0x56
-
场景4:Git 内部哈希
- Git 使用类似的位运算技巧快速比较文件差异
🏋️ 举一反三
完成本课后,试试这些同类题目来巩固知识:
| 题目 | 难度 | 相关知识点 | 提示 |
|---|---|---|---|
| LeetCode 137. 只出现一次的数字 II | Medium | 位运算 | 其他数字出现3次,用位统计 |
| LeetCode 260. 只出现一次的数字 III | Medium | 异或 + 分组 | 有两个单独的数字,先异或找差异位分组 |
| LeetCode 268. 丢失的数字 | Easy | 异或/数学 | 0到n中缺失的数字,异或所有索引和值 |
| LeetCode 389. 找不同 | Easy | 异或 | 两字符串多了一个字符,异或所有字符 |
| LeetCode 645. 错误的集合 | Easy | 异或/哈希 | 一个数字重复,一个缺失 |
📝 课后小测
试试这道变体题,不要看答案,自己先想5分钟!
题目:给定一个整数数组,其中恰好有两个元素只出现一次,其他元素都出现两次。找出这两个只出现一次的元素。要求 O(n) 时间和 O(1) 空间。
例如:nums = [1,2,1,3,2,5] 返回 [3,5]
💡 提示(实在想不出来再点开)
先把所有数字异或,得到的是那两个单独数字的异或结果(a^b)。然后找到这个结果中任意一个为1的位,说明a和b在这一位不同,以此为依据把数组分成两组,每组分别异或就能得到a和b!
✅ 参考答案
def singleNumber_iii(nums: List[int]) -> List[int]:
"""
LC 260. 只出现一次的数字 III
思路:先全部异或得到 a^b,再用差异位分组
"""
# 步骤1:全部异或,得到 a^b
xor_all = 0
for num in nums:
xor_all ^= num
# 步骤2:找到 a^b 中任意一个为1的位(说明a和b在该位不同)
# 用 xor_all & (-xor_all) 可以快速得到最右边的1
diff_bit = xor_all & (-xor_all)
# 步骤3:根据该位是否为1,把数组分成两组
a, b = 0, 0
for num in nums:
if num & diff_bit:
a ^= num # 该位为1的一组
else:
b ^= num # 该位为0的一组
return [a, b]
# 测试
print(singleNumber_iii([1, 2, 1, 3, 2, 5])) # 输出:[3, 5]
核心思路:
- 全部异或得到
a^b(两个单独数字的异或) - 找到
a^b中为1的任意一位,这一位上a和b必然不同 - 以这一位为标准把数组分成两组,每组分别异或就能分别得到a和b
时间复杂度:O(n),空间复杂度:O(1)
如果这篇内容对你有帮助,推荐收藏 AI Compass:github.com/tingaicompa… 更多系统化题解、编程基础和 AI 学习资料都在这里,后续复习和拓展会更省时间。