力扣hot100之136:只出现一次的数字
力扣hot100之136:只出现一次的数字

LeetCode 136:只出现一次的数字(Single Number)题解

Single Number 是 LeetCode Hot 100 里面一道非常经典的数组题。

这道题本身不难,甚至看完题目以后,大部分人第一反应都会想到:

“统计每个数字出现的次数,再找出只出现一次的那个数。”

这个思路完全没问题,也很容易写出来。

不过这道题真正有意思的地方在于:

它还有一种更巧妙、更高效的做法——位运算中的异或

你给出的这两种 Java 代码,正好分别对应了这道题最常见的两类解法:

  1. 哈希表统计次数

  2. 异或运算消除重复元素

本文就结合这两种写法,系统地讲一下这道题的思路。


题目介绍

给你一个非空整数数组 nums,除了某个元素只出现一次以外,其余每个元素都恰好出现两次

要求你找出那个只出现一次的元素。

例如:

nums = [2,2,1]

输出:

1

再比如:

nums = [4,1,2,1,2]

输出:

4

题目还有一个很重要的隐含信息:

  • 只有一个元素出现一次

  • 其他所有元素都出现两次

这个条件决定了异或解法为什么可行。


示例

示例 1

输入:nums = [2,2,1]
输出:1

因为 2 出现了两次,会被“抵消”,最后剩下的就是 1

示例 2

输入:nums = [4,1,2,1,2]
输出:4

数组中:

  • 1 出现两次

  • 2 出现两次

  • 4 只出现一次

所以答案就是 4

示例 3

输入:nums = [1]
输出:1

数组中只有一个元素时,它本身就是答案。


先从最直接的思路开始:哈希表统计次数

很多数组题在第一次做的时候,最容易想到的就是“统计出现次数”。

这道题也一样。

思路

我们可以用一个 HashMap<Integer, Integer> 来记录每个数字出现了几次:

  • 遍历数组

  • 如果当前数字第一次出现,就记为 1

  • 如果已经出现过,就把次数加一

  • 最后再遍历一遍 map,找到次数为 1 的那个数字返回

这个思路非常直观,几乎不需要额外技巧。

代码

class Solution {
    public int singleNumber(int[] nums) {
        Map<Integer, Integer> map = new HashMap<>();
        for (Integer i : nums) {
            Integer count = map.get(i);
            count = count == null ? 1 : ++count;
            map.put(i, count);
        }
        for (Integer i : map.keySet()) {
            Integer count = map.get(i);
            if (count == 1) {
                return i;
            }
        }
        return -1;
    }
}

执行过程分析

假设:

nums = [4,1,2,1,2]

遍历后,map 中保存的数据大概是:

4 -> 1
1 -> 2
2 -> 2

接着再遍历 map

  • 4 的次数是 1

  • 所以直接返回 4

这种写法的优点

  • 容易理解

  • 容易写出来

  • 适合刚开始接触这类题时使用

这种写法的缺点

它虽然好理解,但会使用额外空间。

因为 HashMap 需要把数组中的元素及其出现次数都存下来,所以空间复杂度不会低。


更高效的思路:异或运算

这道题真正的经典解法,是使用异或

先认识异或的几个性质

异或运算符是 ^

它有几个非常重要的性质:

  1. 相同的数异或等于 0

a ^ a = 0
  1. 任何数和 0 异或都等于它本身

a ^ 0 = a
  1. 异或运算满足交换律和结合律

也就是说:

a ^ b ^ a = (a ^ a) ^ b = 0 ^ b = b

这个性质一出来,其实就已经很接近答案了。

为什么这道题可以用异或?

因为题目说得非常明确:

  • 除了一个数字只出现一次

  • 其他数字都出现两次

那么如果我们把整个数组所有元素全部异或起来:

  • 成对出现的数字都会变成 0

  • 最后剩下的,就是那个只出现一次的数字

例如:

[4,1,2,1,2]

异或过程可以理解为:

4 ^ 1 ^ 2 ^ 1 ^ 2
= 4 ^ (1 ^ 1) ^ (2 ^ 2)
= 4 ^ 0 ^ 0
= 4

所以最终结果就是 4


异或解法代码

class Solution {
    public int singleNumber(int[] nums) {
        int res = 0;
        for (int num : nums) {
            res ^= num;
        }
        return res;
    }
}

代码解释

定义一个变量:

int res = 0;

然后遍历数组,把每个数字都和 res 做异或:

res ^= num;

由于重复的数字会互相抵消,最后 res 中剩下的就是目标值。

执行过程演示

还是以:

nums = [4,1,2,1,2]

为例。

初始时:

res = 0

依次异或:

res = 0 ^ 4 = 4
res = 4 ^ 1 = 5
res = 5 ^ 2 = 7
res = 7 ^ 1 = 6
res = 6 ^ 2 = 4

最后得到:

res = 4

这就是答案。


两种解法对比

方法一:哈希表

思路: 统计每个数字出现的次数,然后返回次数为 1 的数字。

时间复杂度:

O(n)

因为需要遍历数组一次统计次数,再遍历哈希表一次找答案,总体仍然是线性级别。

空间复杂度:

O(n)

因为最坏情况下需要用哈希表存储所有不同的数字。

方法二:异或

思路: 利用异或“相同为 0、和 0 异或得本身”的性质,把所有数字异或起来。

时间复杂度:

O(n)

只需要遍历一次数组。

空间复杂度:

O(1)

只用了一个额外变量 res

哪种更推荐?

如果只是为了把题做出来,哈希表解法已经足够。

但如果是从面试、算法优化或者 LeetCode 官方标准解法的角度来看,异或解法显然更值得掌握,因为:

  • 写法更简洁

  • 不需要额外哈希表

  • 空间复杂度更优

  • 充分利用了题目“其余元素都出现两次”的条件

所以这道题最推荐的做法就是:

遍历数组,整体异或。


为什么异或解法不会出错?

很多人在第一次看到异或做法时,会觉得这种方法“很巧”,但又担心它是不是只是碰巧有效。

其实不是。

它成立的根本原因,是因为题目的条件非常严格:

  • 每个重复元素都只出现 两次

  • 只有一个元素出现 一次

于是:

  • 每一对相同元素都会变成 0

  • 最终只剩下那个落单的数

如果题目条件变了,比如:

  • 其他元素出现 三次

  • 或者有 两个 元素只出现一次

那么这个最基础的异或写法就不能直接用了。

这也是为什么刷题时一定要注意题目条件,因为很多“技巧题”的成立都依赖于条件本身。


完整总结

这道题属于那种:

  • 入门时可以用哈希表轻松做出来

  • 进一步优化时可以学到位运算思想

非常适合作为“从普通解法过渡到技巧解法”的经典题。

我们可以这样理解这两种方案:

哈希表方案

优点是直观,容易想到。

核心就是:

  • 先统计

  • 再查找

异或方案

优点是简洁高效。

核心就是利用异或的三个性质:

  • a ^ a = 0

  • a ^ 0 = a

  • 异或满足交换律和结合律

因此,数组中所有成对元素都会被消掉,最后剩下的就是答案。


最后一句

如果这题你已经能写出哈希表版本,那说明你已经把题做出来了。

而如果你进一步理解了异或版本,那你掌握的就不只是这道题本身,而是一种在很多位运算题里都非常重要的思维方式。

这也是这道题最值得学习的地方。

如果对您有帮助的话,能否支持一下博主?
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇