Leetcode 06 java
136. 只出现一次的数字
题目
给你一个 非空 整数数组 nums
,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
示例 1 :
输入:nums = [2,2,1]
输出:1
示例 2 :
输入:nums = [4,1,2,1,2]
输出:4
示例 3 :
输入:nums = [1]
输出:1
提示:
1 <= nums.length <= 3 * 104
-3 * 104 <= nums[i] <= 3 * 104
- 除了某个元素只出现一次以外,其余每个元素均出现两次。
我的想法:
也许用hash?哈希map,直接用键值对验证,但其实我写这个不是很熟练,准确的说我并不知道怎么很熟练之写,判定键值对,然后有一个循环计数器,如果已有合适的判定出现了两次就加1,我只是有想法但我不太会写anyway。
hashmap加一个loop。
一针见血的题友:
“如果你读完题目不能第一时间想到异或,就应该好好学一下位运算了,这是个学习位运算的好时机.jpg”
“我真是sb,第一眼哈希没救了”(我也是。。。。。)
1 该算法只使用常量额外空间是什么意思?
在计算机科学中,当一个算法被描述为 “只使用常量额外空间” 时,意味着该算法在执行过程中所需要的额外存储空间不随输入数据规模的增大而增加。换句话说,无论输入数据是小是大,算法额外消耗的内存空间始终保持不变,用大 O 表示法记作 O(1)。
关键概念解析
- 额外空间:指的是除了输入数据本身所占用的内存外,算法在运行过程中临时分配的内存空间。
- 常量:表示这个空间是固定的,不依赖于输入数据的大小 n。
示例对比
假设有一个数组 arr
长度为 n,我们需要对其进行处理:
非 O (1) 空间的算法(线性空间)
python
运行
def double_elements(arr):result = [] # 创建了一个新的列表,空间复杂度为O(n)for num in arr:result.append(num * 2)return result
这个算法需要创建一个与输入数组大小相同的新数组,因此额外空间为 (O(n))。
O (1) 空间的算法(原地修改)
python
运行
def double_elements_inplace(arr):for i in range(len(arr)): # 直接修改原数组,不创建新的额外空间arr[i] = arr[i] * 2return arr
这个算法直接在原数组上进行修改,只使用了固定的几个变量(如 i
),因此额外空间为 (O(1))。
为什么需要常量额外空间?
- 内存效率高:尤其在处理大规模数据时,O (1) 空间的算法能显著减少内存消耗。
- 原地操作:某些场景(如嵌入式系统或内存受限环境)中,无法分配大量临时内存。
常见应用场景
- 排序算法:如冒泡排序、插入排序都是原地排序,空间复杂度为 O (1)。
- 数组 / 链表操作:许多题目要求在不使用额外空间的前提下完成数据处理。
注意事项
- 输入数据本身的空间不计入:O (1) 空间只限制额外空间,不包括存储输入所需的内存。
- 递归调用栈:递归算法可能隐含额外的栈空间开销,需谨慎分析。
理解常量额外空间有助于设计更高效的算法,特别是在资源受限的环境中。
2 按照"我的想法"不符合题目要求,会使用额外的空间!
import java.util.HashMap;
import java.util.Map;public class SingleNumberHashMap {public int singleNumber(int[] nums) {Map<Integer, Integer> map = new HashMap<>();// 统计每个元素出现的次数for (int num : nums) {map.put(num, map.getOrDefault(num, 0) + 1);}// 查找只出现一次的元素for (Map.Entry<Integer, Integer> entry : map.entrySet()) {if (entry.getValue() == 1) {return entry.getKey();}}return -1; // 根据题目条件,不会执行到这里}public static void main(String[] args) {SingleNumberHashMap solution = new SingleNumberHashMap();System.out.println(solution.singleNumber(new int[]{2, 2, 1})); // 输出1System.out.println(solution.singleNumber(new int[]{4, 1, 2, 1, 2})); // 输出4System.out.println(solution.singleNumber(new int[]{1})); // 输出1}
}
3 使用异或运算
使用异或运算是解决这个问题的最优方案,因为它既能满足线性时间复杂度(O (n)),又能满足常量额外空间(O (1))的要求。让我再次强调异或解法的核心思路和优势:
异或解法的关键点
-
异或运算的特性:
a ^ 0 = a
(任何数与 0 异或等于自身)a ^ a = 0
(任何数与自身异或等于 0)- 交换律:
a ^ b ^ a = (a ^ a) ^ b = 0 ^ b = b
-
算法逻辑:
- 初始化结果为 0。
- 遍历数组,将每个元素与结果进行异或运算。
- 由于相同元素异或后抵消为 0,最终结果即为只出现一次的元素。
-
优势:
- 无需额外空间:直接在原数组上操作,空间复杂度 O (1)。
- 高效:只需遍历一次数组,时间复杂度 O (n)。
代码示例(Java)
java
public int singleNumber(int[] nums) {int result = 0;for (int num : nums) {result ^= num; // 异或运算}return result;
}
对比总结
方法 | 时间复杂度 | 空间复杂度 | 是否符合题目要求 |
---|---|---|---|
异或运算 | O(n) | O(1) | ✅ |
哈希表统计 | O(n) | O(n) | ❌ |
为什么面试官喜欢异或解法?
- 空间优化:不使用任何额外数据结构,直接通过位运算解决问题。
- 代码简洁:仅需几行代码,没有复杂逻辑。
- 考察位运算理解:异或运算是位运算中的经典应用,能体现候选人的基础功底。
如果你在面试中遇到类似问题,先提出哈希表的思路(正确性优先),再优化到异或解法(展示算法功底),会是一个很好的加分项!
4 关于位运算
位运算是直接操作二进制位的高效运算,在算法、系统编程、密码学等领域都有广泛应用。以下是常见位运算的梳理和经典应用场景:
一、基本位运算符
1. 按位与(&)
- 规则:对应位都为 1 时结果为 1,否则为 0。
- 示例:
5 & 3
→101 & 011 = 001
→ 结果为 1。 - 应用:
- 判断奇偶:
x & 1 == 1
则为奇数(最低位为 1)。 - 清零特定位:如
x & 0xFFFFFFFE
清除最低位。
- 判断奇偶:
2. 按位或(|)
- 规则:对应位有一个为 1 时结果为 1,否则为 0。
- 示例:
5 | 3
→101 | 011 = 111
→ 结果为 7。 - 应用:
- 设置特定位:如
x | 0x00000001
将最低位设为 1。
- 设置特定位:如
3. 按位异或(^)
- 规则:对应位不同时结果为 1,相同为 0。
- 示例:
5 ^ 3
→101 ^ 011 = 110
→ 结果为 6。 - 特性:
- 自反性:
a ^ a = 0
,a ^ 0 = a
。 - 交换律:
a ^ b ^ a = b
。
- 自反性:
- 应用:
- 交换两数:
a = a^b; b = a^b; a = a^b
。 - 找唯一出现一次的元素(本题核心)。
- 交换两数:
4. 按位取反(~)
- 规则:所有位取反(0 变 1,1 变 0)。
- 示例:
~5
→~0101 = 1010
(补码表示,结果为 - 6)。 - 应用:
- 生成掩码:如
~0
生成全 1 的掩码。
- 生成掩码:如
5. 左移(<<)
- 规则:所有位左移 n 位,右边补 0,相当于乘以 2ⁿ。
- 示例:
5 << 2
→101 << 2 = 10100
→ 结果为 20。 - 应用:
- 快速乘法:
x << n
等价于x * 2ⁿ
。
- 快速乘法:
6. 右移(>> 和 >>>)
- 算术右移(>>):带符号右移,左边补符号位(正数补 0,负数补 1)。
- 逻辑右移(>>>):无符号右移,左边补 0。
- 示例:
-5 >> 1
→-3
(算术右移);-5 >>> 1
→2147483645
(逻辑右移)。 - 应用:
- 快速除法:
x >> n
等价于x // 2ⁿ
。
- 快速除法:
二、位运算经典应用场景
1. 交换两个数
java
a = a ^ b;
b = a ^ b; // 等价于 b = (a^b)^b = a
a = a ^ b; // 等价于 a = (a^b)^a = b
2. 找出唯一元素
- 问题:数组中只有一个元素出现 1 次,其他均出现 2 次。
- 解法:异或所有元素,结果即为唯一元素。
java
int result = 0;
for (int num : nums) result ^= num;
return result;
3. 判断第 n 位是否为 1
java
boolean isSet = (x & (1 << n)) != 0;
4. 设置第 n 位为 1
java
x = x | (1 << n);
5. 清除第 n 位为 0
java
x = x & ~(1 << n);
6. 翻转第 n 位
java
x = x ^ (1 << n);
7. 统计二进制中 1 的个数(汉明重量)
java
int count = 0;
while (x != 0) {count++;x = x & (x - 1); // 清除最低位的1
}
return count;
8. 判断是否为 2 的幂
java
boolean isPowerOfTwo = (x & (x - 1)) == 0 && x > 0;
三、进阶技巧
1. 掩码(Mask)
- 生成低 n 位全 1 的掩码:
(1 << n) - 1
。 - 示例:生成低 3 位掩码:
(1 << 3) - 1 = 000...0111
。
2. 状态压缩
- 场景:用一个整数的每个位表示不同状态(如开关、存在性)。
- 示例:用一个 int 的 32 位表示 32 个布尔值。
3. 子集枚举
- 问题:生成集合的所有子集。
- 解法:用二进制数的每一位表示元素是否存在。
java
for (int mask = 0; mask < (1 << n); mask++) {// mask的每一位表示是否选择对应元素
}
四、常见问题与练习
-
题目推荐:
- 136. 只出现一次的数字(异或)
- 191. 位 1 的个数(汉明重量)
- 231. 2 的幂(位判断)
- 338. 比特位计数(动态规划 + 位运算)
-
学习建议:
- 多手动推导二进制运算过程,培养直觉。
- 遇到数组、数字类问题时,优先思考位运算是否适用。
- 记住经典模板(如异或找唯一元素、清除最低位 1)。
五、注意事项
- 优先级问题:位运算符优先级较低,使用时需注意加括号(如
(x & 1) == 0
)。 - 右移的符号问题:算术右移(
>>
)和逻辑右移(>>>
)的区别。 - 性能与可读性:位运算通常更高效,但可能降低代码可读性,需权衡。
掌握位运算后,你会发现很多问题可以用极简洁的代码解决,比如本题的 “异或解法”,这正是算法的魅力所在!
5 详细题解
一、问题本质分析
题目要求:
- 数组中只有一个元素出现 1 次,其余元素均出现 2 次。
- 需在线性时间(O (n))和常量空间(O (1))内找到这个唯一元素。
难点在于:
- 不能使用额外空间(如哈希表),因此常规的 “计数” 或 “标记” 方法不可行。
二、异或运算(XOR)的核心性质
异或运算用符号 ^
表示,其运算规则是:
- 相同为 0,不同为 1。
例如:5 ^ 3
→101₂ ^ 011₂ = 110₂
→ 结果为 6。
三个关键性质:
-
归零律:
a ^ a = 0
任何数与自身异或,结果为 0。
例如:5 ^ 5 = 0
。 -
恒等律:
a ^ 0 = a
任何数与 0 异或,结果为其本身。
例如:5 ^ 0 = 5
。 -
交换律和结合律:
a ^ b = b ^ a
(交换律)(a ^ b) ^ c = a ^ (b ^ c)
(结合律)
例如:5 ^ 3 ^ 5 = (5 ^ 5) ^ 3 = 0 ^ 3 = 3
。
三、为什么异或能解决这个问题?
假设数组为 [a, b, c, a, b]
,其中 c
是唯一出现一次的元素。
根据异或的交换律和结合律,我们可以重新排列异或顺序:
java
a ^ b ^ c ^ a ^ b
= (a ^ a) ^ (b ^ b) ^ c // 交换律和结合律
= 0 ^ 0 ^ c // 归零律:a^a=0, b^b=0
= c // 恒等律:0^c=c
关键逻辑:
- 出现两次的元素(如
a
和b
)会通过异或自我抵消为 0。 - 唯一元素(如
c
)会与 0 异或,最终保留下来。
四、代码逐行解释
java
public int singleNumber(int[] nums) {int single = 0; // 初始化为0(因为0^任何数=任何数)for (int num : nums) {single ^= num; // 累积异或每个元素}return single; // 最终结果即为唯一元素
}
执行过程示例:
假设输入数组为 [2, 2, 1]
,步骤如下:
single = 0
- 遍历到
2
:single = 0 ^ 2 = 2
- 遍历到
2
:single = 2 ^ 2 = 0
- 遍历到
1
:single = 0 ^ 1 = 1
- 返回
1
。
五、数学归纳法证明
对于任意包含 2m+1
个元素的数组(m
个元素出现两次,1 个元素出现一次):
- 数组元素可表示为:
[a₁, a₁, a₂, a₂, ..., aₘ, aₘ, c]
。 - 全部元素的异或结果为:
java
(a₁ ^ a₁) ^ (a₂ ^ a₂) ^ ... ^ (aₘ ^ aₘ) ^ c = 0 ^ 0 ^ ... ^ 0 ^ c = c
六、复杂度分析
-
时间复杂度:O(n)
只需遍历数组一次,处理每个元素的时间为 O (1)。 -
空间复杂度:O(1)
只使用了一个变量single
存储中间结果,无需额外空间。
七、为什么想不到这个解法?
- 位运算不直观:异或的性质在日常生活中很少用到,需要专门积累。
- 问题转化困难:需要将 “找唯一元素” 转化为 “抵消重复元素” 的数学问题。
- 经验不足:初次遇到此类题很难联想到位运算,多刷题可培养直觉。
八、其他类似问题推荐
- 137. 只出现一次的数字 II(其他元素出现 3 次)
- 260. 只出现一次的数字 III(两个唯一元素)
- 面试题 17.19. 消失的两个数字
九、总结
异或运算的核心应用场景:
- 交换两个数(无需临时变量):
a = a^b; b = a^b; a = a^b
。 - 统计奇偶次数:出现偶数次的元素会被抵消,奇数次的保留。
- 快速判断集合状态:用一个整数的每个位表示元素是否存在。
遇到类似 “唯一元素”“出现次数” 的问题时,优先考虑位运算!