[从零开始面试算法] (04/100) LeetCode 136. 只出现一次的数字:哈希表与位运算的巅峰对决

引言

欢迎来到本系列的第四篇!在前面的文章中,我们已经见识了哈希表在"查找"问题上的威力。今天,我们将面对一个看似简单,却能引出两种截然不同且都极为重要的解题思路的经典题目------LeetCode 136. 只出现一次的数字。

这道题是面试中的高频题,因为它像一个十字路口,既可以通往我们熟悉的数据结构之路(哈希表),又可以引领我们进入一个全新的、充满数学之美的领域------位运算

本文将带你一起:

  1. 用我们熟悉的哈希表方法,轻松解决这个问题。

  2. 深入探讨时间与空间复杂度的真正含义,澄清一些常见的误解。

  3. 揭开"位运算"的神秘面纱,领略其 O(1) 空间复杂度的极致魅力。


一、题目呈现

  • 题目编号:136. 只出现一次的数字

  • 题目难度:简单

  • 题目链接136. 只出现一次的数字 - 力扣 (LeetCode)

  • 题目描述

    给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

    你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。

  • 核心要求

    • 时间复杂度 :线性,即 O(n)

    • 空间复杂度 :常量,即 O(1)

  • 示例

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

    • 输出: 4

二、我的思考:哈希表虽好,但空间超标

看到题目要求"找出唯一的元素",我的第一反应依然是求助于我们的老朋友------哈希表。我们可以用哈希表来统计每个数字出现的次数,最后再找出那个出现次数为 1 的数字。

哈希表解法

这个思路非常直观,而且我们在之前的文章中已经熟练掌握了

cpp 复制代码
#include <vector>
#include <unordered_map>

class Solution_HashMap {
public:
    int singleNumber(std::vector<int>& nums) {
        // 使用哈希表统计频率
        std::unordered_map<int, int> counts;
        for (int num : nums) {
            counts[num]++;
        }

        // 遍历哈希表,找出频率为 1 的数字
        for (auto const& [num, count] : counts) {
            if (count == 1) {
                return num;
            }
        }
        return -1; // 理论上不会执行到
    }
};

复杂度分析:

  • 时间复杂度 :我们遍历了一次 nums 来构建哈希表(O(n)),又遍历了一次哈希表来找结果(最坏情况下 O(n))。所以总时间复杂度是 O(n) + O(n) = O(n)符合要求

  • 空间复杂度 :哈希表需要存储 n/2 + 1 个不同的数字。随着输入数组 n 的增大,哈希表的大小也会线性增大。因此,空间复杂度是 O(n)不符合 O(1) 的要求

虽然哈希表解法在时间上达标了,但在空间上却"超标"了。这迫使我们必须寻找一种全新的、不依赖额外存储空间的解题路径。

三、豁然开朗:位运算的魔法------异或 (XOR)

要实现 O(1) 的空间复杂度,意味着我们不能使用随 n 增长的额外数据结构。我们只能使用有限的几个变量。这听起来似乎不可能,但位运算让它变成了现实。

核心知识点:异或运算 ^

你可能还记得离散数学中的异或,编程中的位运算正是它的延伸。它有三个神奇的性质,是解决此题的关键:

  1. 任何数与 0 异或,结果是它本身: a ^ 0 = a

  2. 任何数与它自己异或,结果是 0: a ^ a = 0

  3. 异或运算满足交换律和结合律:a ^ b ^ a = (a ^ a) ^ b = 0 ^ b = b

魔法发生了!

第三条性质告诉我们:将一堆数字全部异或在一起,那些成对出现的数字会因为 a ^ a = 0 而相互抵消,最终只剩下那个落单的数字!

一个具体的例子:[4, 1, 2, 1, 2]

让我们把数组中所有数字全部异或起来:

result = 4 ^ 1 ^ 2 ^ 1 ^ 2

根据交换律,我们可以重新排列:

result = 4 ^ (1 ^ 1) ^ (2 ^ 2)

根据性质2,1 ^ 1 = 0,2 ^ 2 = 0:

result = 4 ^ 0 ^ 0

根据性质1,4 ^ 0 = 4,4 ^ 0 = 4:

result = 4

我们不费吹灰之力就找到了答案!


四、C++ 最优代码与详解

有了位运算这个强大的武器,代码实现变得异常简洁。

cpp 复制代码
#include <vector>
#include <numeric> // 在某些写法中可能用到,但这里不需要

class Solution {
public:
    int singleNumber(std::vector<int>& nums) {
        // 1.
        int accumulator = 0;

        // 2.
        for (int num : nums) {
            // 3.
            accumulator ^= num;
        }

        // 4.
        return accumulator;
    }
};
代码逐点解释
  1. int accumulator = 0;

    我们初始化一个"累加器"变量,值为 0。选择 0 是因为它是异或运算的单位元,不会影响第一次异或的结果(0 ^ 第一个元素 = 第一个元素)。

  2. for (int num : nums)

    我们使用范围 for 循环遍历 nums 数组中的每一个数字。

  3. accumulator ^= num;

    这是算法的核心。在每一次循环中,我们将累加器的当前值与数组中的当前元素 num 进行异或运算,并将结果存回累加器。a ^= b 是 a = a ^ b 的简写形式。

  4. return accumulator;

    当循环结束时,所有成对出现的数字都已相互抵消(变成了 0),累加器中剩下的就是那个唯一的、只出现了一次的数字。我们将其返回。


五、深度思考与答疑

问:如何理解"线性时间"和"常量空间"?

:这是你笔记中提出的一个非常深刻的问题!

  • 线性时间复杂度 O(n)

    • "线性"可以类比一次函数 y = kx。算法的执行时间(或步骤数)与输入数据的规模 n 成一个正比关系

    • 我们的代码只有一个 for 循环,它会遍历 n 个元素。如果数组长度翻倍,循环次数也大致翻倍。这就是典型的 O(n)。

    • 你对 n-a 的思考非常有趣 :在大 O 表示法中,我们只关心增长的趋势 。O(n),O(n-1),O(2n+5) 都被简化为 O(n),因为当 n 趋向于无穷大时,常数项和系数的影响都可以忽略不计。

  • 常量额外空间 O(1)

    • "常量"意味着算法使用的额外 内存空间不随输入规模 n 的变化而变化

    • 在位运算解法中,我们只使用了一个额外的 int 变量 accumulator。无论输入数组 nums 有 10 个元素还是 1000 万个元素,我们都只需要这一个额外的变量。它的内存占用是恒定的。

    • 而哈希表解法中,如果数组有 1000 万个元素,哈希表也需要存储约 500 万个元素,内存占用随 n 线性增长,所以是 O(n) 空间。


六、总结与收获

  • 复杂度分析 :位运算解法完美地满足了题目的苛刻要求。时间复杂度为 O(n) ,额外空间复杂度为 O(1)

  • 核心思想

    • 哈希表是解决频率统计问题的通用武器,但以空间换时间。

    • 位运算(异或) 在处理"成对出现"的问题时,提供了一种极致空间优化的"捷径"。

  • 关键技巧:熟练掌握异或运算的三个核心性质,是解锁许多位运算难题的关键。

相关推荐
励志不掉头发的内向程序员3 小时前
【STL库】哈希表的原理 | 哈希表模拟实现
开发语言·c++·学习·散列表
tan77º4 小时前
【项目】基于多设计模式下的同步&异步日志系统 - 项目介绍与前置知识
linux·c++·设计模式
RTC老炮4 小时前
webrtc弱网-AcknowledgedBitrateEstimatorInterface类源码分析与算法原理
网络·算法·webrtc
给大佬递杯卡布奇诺4 小时前
FFmpeg 基本API avcodec_open2函数内部调用流程分析
c++·ffmpeg·音视频
绝无仅有4 小时前
面试真实经历某商银行大厂计算机网络问题和答案总结
后端·面试·github
绝无仅有4 小时前
面试真实经历某商银行大厂系统,微服务,分布式问题和答案总结
后端·面试·github
进击的圆儿5 小时前
高并发内存池项目开发记录 - 02
开发语言·c++·实战·项目·内存池
YXXY3135 小时前
二叉树进阶
c++
Antonio9155 小时前
【图像处理】常见图像插值算法与应用
图像处理·算法·计算机视觉