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,第一眼哈希没救了”(我也是。。。。。)
在计算机科学中,当一个算法被描述为 “只使用常量额外空间” 时,意味着该算法在执行过程中所需要的额外存储空间不随输入数据规模的增大而增加。换句话说,无论输入数据是小是大,算法额外消耗的内存空间始终保持不变,用大 O 表示法记作 O(1)。
假设有一个数组 arr
长度为 n,我们需要对其进行处理:
python
运行
def double_elements(arr):
result = [] # 创建了一个新的列表,空间复杂度为O(n)
for num in arr:
result.append(num * 2)
return result
这个算法需要创建一个与输入数组大小相同的新数组,因此额外空间为 (O(n))。
python
运行
def double_elements_inplace(arr):
for i in range(len(arr)): # 直接修改原数组,不创建新的额外空间
arr[i] = arr[i] * 2
return arr
这个算法直接在原数组上进行修改,只使用了固定的几个变量(如 i
),因此额外空间为 (O(1))。
理解常量额外空间有助于设计更高效的算法,特别是在资源受限的环境中。
import java.util.HashMap;
import java.util.Map;
public class SingleNumberHashMap {
public int singleNumber(int[] nums) {
Map map = new HashMap<>();
// 统计每个元素出现的次数
for (int num : nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
// 查找只出现一次的元素
for (Map.Entry 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})); // 输出1
System.out.println(solution.singleNumber(new int[]{4, 1, 2, 1, 2})); // 输出4
System.out.println(solution.singleNumber(new int[]{1})); // 输出1
}
}
使用异或运算是解决这个问题的最优方案,因为它既能满足线性时间复杂度(O (n)),又能满足常量额外空间(O (1))的要求。让我再次强调异或解法的核心思路和优势:
异或运算的特性:
a ^ 0 = a
(任何数与 0 异或等于自身)a ^ a = 0
(任何数与自身异或等于 0)a ^ b ^ a = (a ^ a) ^ b = 0 ^ b = b
算法逻辑:
优势:
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) | ❌ |
如果你在面试中遇到类似问题,先提出哈希表的思路(正确性优先),再优化到异或解法(展示算法功底),会是一个很好的加分项!
位运算是直接操作二进制位的高效运算,在算法、系统编程、密码学等领域都有广泛应用。以下是常见位运算的梳理和经典应用场景:
5 & 3
→ 101 & 011 = 001
→ 结果为 1。x & 1 == 1
则为奇数(最低位为 1)。x & 0xFFFFFFFE
清除最低位。5 | 3
→ 101 | 011 = 111
→ 结果为 7。x | 0x00000001
将最低位设为 1。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
。~5
→ ~0101 = 1010
(补码表示,结果为 - 6)。~0
生成全 1 的掩码。5 << 2
→ 101 << 2 = 10100
→ 结果为 20。x << n
等价于 x * 2ⁿ
。-5 >> 1
→ -3
(算术右移);-5 >>> 1
→ 2147483645
(逻辑右移)。x >> n
等价于 x // 2ⁿ
。java
a = a ^ b;
b = a ^ b; // 等价于 b = (a^b)^b = a
a = a ^ b; // 等价于 a = (a^b)^a = b
java
int result = 0;
for (int num : nums) result ^= num;
return result;
java
boolean isSet = (x & (1 << n)) != 0;
java
x = x | (1 << n);
java
x = x & ~(1 << n);
java
x = x ^ (1 << n);
java
int count = 0;
while (x != 0) {
count++;
x = x & (x - 1); // 清除最低位的1
}
return count;
java
boolean isPowerOfTwo = (x & (x - 1)) == 0 && x > 0;
(1 << n) - 1
。(1 << 3) - 1 = 000...0111
。java
for (int mask = 0; mask < (1 << n); mask++) {
// mask的每一位表示是否选择对应元素
}
题目推荐:
学习建议:
(x & 1) == 0
)。>>
)和逻辑右移(>>>
)的区别。掌握位运算后,你会发现很多问题可以用极简洁的代码解决,比如本题的 “异或解法”,这正是算法的魅力所在!
题目要求:
难点在于:
异或运算用符号 ^
表示,其运算规则是:
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
存储中间结果,无需额外空间。
异或运算的核心应用场景:
a = a^b; b = a^b; a = a^b
。遇到类似 “唯一元素”“出现次数” 的问题时,优先考虑位运算!