MarsCode AI 第一题:找单独的数 你真的懂了吗?

286 阅读5分钟

第一道题是:在一个班级中,每位同学都拿到了一张卡片,上面有一个整数。有趣的是,除了一个数字之外,所有的数字都恰好出现了两次。现在需要你帮助班长小C快速找到那个拿了独特数字卡片的同学手上的数字是什么。

这是一个经典的"找出数组中只出现一次的数字"问题,这道题的难点是题目要求:

  1. 设计一个算法,使其时间复杂度为 O(n),其中 n 是班级的人数。
  2. 尽量减少额外空间的使用,以体现你的算法优化能力。

那这意味着现实生活中的方法是无法解决这道题的,比如我们通常会这么思考:

  1. 排队法
  • 让同学们按照卡片上的数字排成一列
  • 相同数字的同学会自然站在一起
  • 最后看谁旁边没有同伴,那个同学的数字就是独特的
  • 不满足❌:需要移动所有同学,比较耗时
  1. 登记表法
  • 班长准备一张表,让每位同学轮流来登记自己的数字
  • 在表上画正字符号,第一次出现画一笔,第二次出现画第二笔
  • 最后看哪个数字只有一笔,那就是独特的数字
  • 不满足❌:需要额外准备登记表

那以 Python 为例,我们很快就能敲出下面代码

def solution(nums):
    count_dict = {}
    for num in nums:
        count_dict[num] = count_dict.get(num, 0) + 1
    for num, count in count_dict.items():
        if count == 1:
            return num
            
def solution(nums):
    nums.sort()  # 只是演示目的
    i = 0
    while i < len(nums)-1:
        if nums[i] != nums[i+1]:
            return nums[i]
        i += 2
    return nums[-1]  # 处理最后一个数是单独的情况

可以看到上面的两种算法虽然可以跑过 Test case,但是没有办法满足题目的要求。 那我们就要学习新的知识:异或 XOR

什么是 XOR

异或是一个位运算的运算符,基本规则是:

  • 相同为0
  • 不同为1

用二进制举例:

  1. 比较 5 和 5: 5 的二进制是: 101 5 的二进制是: 101 异或结果是: 000 (因为每一位都相同,所以都是0)

  2. 比较 5 和 3: 5 的二进制是: 101 3 的二进制是: 011 异或结果是: 110 (不同的位得1,相同的位得0)

为什么异或能找到单独的数字?

  1. 当两个相同的数异或时,结果为0

    比如:5 xor 5 = 0
    
  2. 任何数和0异或,结果是它自己

    比如:5 xor 0 = 5
    
  3. 所以,如果有一组数:[5, 3, 5]

    第一步:5 xor 3 = 6
    第二步:6 xor 5 = 3
    
    • 两个5相互抵消(因为5 xor 5 = 0)
    • 最后剩下3(因为0 xor 3 = 3)

通过这几个例子我们发现了一个基本规律:

  • 相同的数字配对后会消除(变成0)
  • 任何数字和0配对还是它自己
  • 配对的顺序不重要

为什么配对的顺序不重要

让我用一个具体的例子来说明为什么顺序不影响。

假设我们有数组 [5, 3, 5],我们可以用不同的顺序来异或:

顺序1:从左到右

第一步:5 xor 3 = 6
第二步:6 xor 5 = 3
结果是3

顺序2:先处理两个5

第一步:5 xor 5 = 0  (相同的数异或得0)
第二步:0 xor 3 = 3  (0异或任何数得到这个数)
结果还是3

这是因为异或运算有三个重要特性:

  1. 交换律:a xor b = b xor a

    • 就像 5 xor 3 = 3 xor 5
  2. 结合律:(a xor b) xor c = a xor (b xor c)

    • 就像 (5 xor 3) xor 5 = 5 xor (3 xor 5)
  3. 自反性:a xor a = 0

    • 相同的数异或得0

所以无论我们用什么顺序:

  • [5, 3, 5] 可以看作 (5 xor 3) xor 5
  • 也可以看作 5 xor (3 xor 5)
  • 也可以看作 (5 xor 5) xor 3

利用这个特性,我们就可以直接遍历数组,那个最终留下的数就是没有配对的数

def solution(nums):
    result = 0
    for num in nums:
        result ^= num
    return result

反思

这道题特意设计 "除了一个数字外都出现两次"这个条件,来贴合异或运算的特性。但是题目里又没有明说,所以平日不怎么用的到的 xor 会让很多同学翻车。

但这道题真的只是为了考而考吗? 其实也不是, 异或运算在实际的使用场景中还是很普遍的因为其优势:

  • 空间效率高, 只需要用一个变量存储结果
  • 计算速度快, 比加减乘除移位步骤少很多

举一个内存检测的例子,如果我们想在内存出厂时检测内存是否损坏:

  • 初始状态:将一块内存区域写入一组配对的数据

  • 比如写入:[0x55, 0xAA, 0x55, 0xAA](0x55和0xAA是常用的测试模式)

  • 运行一段时间后检测:

    如果内存完好:异或结果 = 0
    如果某个位置损坏:异或结果 ≠ 0
    
  • 这样只需要一个变量就能检测出内存是否损坏,而不需要存储完整的参考数据

再比在数据传输时可以作为校验的手段,比如嵌入式设备在数据传输时

  1. 发送方

    • 数据包:[1, 2, 3, 4]
    • 校验值:将所有数据异或得到校验码 C
    C = 1 xor 2 xor 3 xor 4 = 4
    
    • 发送:[1, 2, 3, 4, 4](数据包 + 校验码)
  2. 接收方

    • 正常接收:[1, 2, 3, 4, 4]
    • 对所有收到的数据(包括校验码)进行异或
    • 如果传输正确,结果应该是0
  3. 错误情况

    • 如果收到:[1, 2, 3, 4, 4, 3](多了一个3)
    • 异或结果就不是0,说明数据有问题
    • 如果收到:[1, 2, 4, 4](丢了3)
    • 异或结果也不是0,说明数据有问题