INFJ 的第一个算法题 —— 按位异或的神奇效果:天然抵消

119 阅读10分钟

今天是我注册字节跳动青训营的第一天,正式开始刷算法题。作为一名人工智能方向的研究生,虽然听起来专业对口,但其实我是一个“转码”的学生,本科并不是学计算机相关专业。对于编程,我只能说掌握了一些非常基础的内容,尤其在算法这块,心理上一直有些障碍。


心理障碍与 INFJ 的思维模式

在做算法题时,我总是无法直接进入问题的逻辑深处。这也许和我的性格类型 INFJ 有关吧。INFJ 的思维习惯让我需要从感性上理解一个问题,需要经过情绪的梳理、自我体验以及对逻辑背后的本质透彻感知,才能真正抓住问题的核心。换句话说,我必须先解决自己的心理困惑,才能全身心投入到逻辑分析中。这种思维模式让我在面对算法题时,总感觉无从下手,但又忍不住一步步地去深究每个细节。


我的第一道算法题:找出唯一的数字

这道题目非常简单,要求从一个列表中找出唯一的数字。其他数字都恰好出现两次,而唯一的数字只出现一次。例如,给定数组 [7, 3, 3, 7, 10],唯一的数字是 10。听起来,这只是一个普通的任务,但我发现自己思考的方向和题目设计的意图完全背道而驰。


我的初步解法:两个数组,空间和时间复杂度的困境

在我不理解按位异或特性的前提下,我自然是用最直观的方式来解决问题。我的思路是:

  1. 使用一个数组存储原始数据。
  2. 用一个临时数组来记录未配对的数字。
  3. 遍历每个数字,将它与临时数组中的数字逐个比较。如果找到配对,就从临时数组中移除;如果没有配对,就将它加入临时数组。
  4. 最终,临时数组中剩下的唯一数字就是结果。

虽然这个思路逻辑上是通的,但却存在巨大的性能问题:

  • 时间复杂度:每次取出一个数字,都需要与临时数组中的所有数字进行比较。如果临时数组长度逐渐增大,时间复杂度达到 ( O(N^2) )。
  • 空间复杂度:需要额外的一个数组存储未配对的数字,空间复杂度为 ( O(N) )。

让我用更加细致的数学推导去计算时间复杂度。并且一步步验证计算是否正确,同时解释为什么这个过程最终仍然属于 (O(N^2))。


1. 我的操作逻辑和总操作次数的计算:

  1. 每当取出第 (k+1)(k+1) 个数字时,需要与前 (k)(k) 个数字比较一次。

  2. 假设班级有 (N)(N) 个数字,最坏情况下,前 (N/2)(N/2) 个数字都没有找到配对,需要比较: [1+2+3++N2][1 + 2 + 3 + \dots + \frac{N}{2}] 这是一个等差数列,求和公式是: [=项数×(首项+末项)2][ \text{和} = \frac{\text{项数} \times (\text{首项} + \text{末项})}{2} ] 项数是 (N/2)(N/2),首项是 1,末项是 (N/2)(N/2),所以: [=N2×(1+N2)2=N×(N/2+1)4][ \text{和} = \frac{\frac{N}{2} \times (1 + \frac{N}{2})}{2} = \frac{N \times (N/2 + 1)}{4} ]

  3. 这是所有前 (N/2)(N/2) 个数字没有找到配对时的比较次数。接下来,每找到一个配对的数字,临时数组中存储的数字会减少,所以操作次数逐渐减少。


2. 总操作次数如何表示:

这个操作次数的细致分析是对的——最坏情况下,比较次数由两部分组成:

  1. 临时数组增长的阶段(即存储没有找到配对的数字):
    • 比较次数是 (1+2+3++N/2)(1 + 2 + 3 + \dots + N/2),如上所述,总和约为 (N28)(\frac{N^2}{8})
  2. 临时数组减少的阶段(即找到配对后减少存储的数字):
    • 假设每找到一个配对的数字,临时数组中存储的数字减少,比较次数逐步下降,呈现类似对称的等差数列。

总体上,操作次数会在增长阶段和减少阶段的两部分叠加,依然是一个 (N28+N28=N24)(\frac{N^2}{8} + \frac{N^2}{8} = \frac{N^2}{4}) 的规模。


3. 为什么是 (O(N2))(O(N^2)),而不是具体的 (N24)(\frac{N^2}{4})

大 O 表示法 是一种忽略常数和低阶项的数学表示法。它的目的是描述算法的增长趋势,而不是精确地计算操作次数。

对于我的算法:

  1. 最坏情况下,操作次数大约是 (N24)(\frac{N^2}{4}),但在大 O 表示法中,常数系数 (1/4)(1/4) 会被省略,因为在输入规模很大时,它对增长趋势的影响可以忽略。
  2. 因此,我们直接表示为 (O(N2))(O(N^2))

4. 举例计算对比

我们用具体数字举个例子来验证:

假设 (N=10)(N = 10)

  1. 在增长阶段:
    • 比较次数是 (1+2+3+4+5=15)(1 + 2 + 3 + 4 + 5 = 15)
  2. 在减少阶段:
    • 假设剩下的数字逐步配对,比较次数会对称地下降 (5+4+3+2+1=15)(5 + 4 + 3 + 2 + 1 = 15)

总操作次数是 (15+15=30)(15 + 15 = 30)

对于 (N=10)(N = 10),我们发现操作次数与 (N2=100)(N^2 = 100) 是同数量级(系数为 (1/4)(1/4))。


5. 总结和核心理解

  1. 我的算法在最坏情况下,每次需要和临时数组中的所有数字比较一次,这导致操作次数呈现平方增长。
  2. 等差数列的累积求和表明计算过程是正确的,但在大 O 表示法中,所有系数和低阶项都会被忽略,因此仍然是 (O(N2))(O(N^2))
  3. 大 O 表示法的目的是描述算法的增长趋势,而不是精确计算操作次数。

按位异或的登场:逻辑的突破

在经过一番挣扎后,我向 ChatGPT 请教,它向我介绍了一种完全不同的解决方案——按位异或。这种方法不仅能将时间复杂度降为 (O(N))( O(N) ),还将空间复杂度优化为 (O(1))( O(1) )。然而,初次听到这个方法,我是完全无法理解的。

比如,[2, 3, 2] 这个例子:

  • 我理解为什么 2 ^ 2 = 0,因为两个相同的数字会被“抵消”。
  • 但当 1 ^ 2 = 3 时,我却陷入了困惑:“为什么两个不同的数字异或会得出另一个完全不相关的数字?”

按位异或的魅力:计算机的魔法,与逻辑学异或的不同


1. 按位异或(XOR)的核心原理

按位异或运算的定义:

  • 在二进制下,按位比较两个数:
    • 如果两个位相同,则结果为 0
    • 如果两个位不同,则结果为 1

简单记忆: [ab=相同为 0,不同为 1][ a \oplus b = \text{相同为 0,不同为 1} ]

数学表示:

对于任意两位 (a)( a )(b)( b ),异或的结果可以用公式表示: [ab=(a+b)mod2][ a \oplus b = (a + b) \mod 2 ]

  • (a)( a )(b)( b ) 的和除以 2 的余数,就是异或的结果。

2. 异或的三个关键性质

性质 1:相同的数异或结果为 0

[aa=0][ a \oplus a = 0 ] 为什么?

  • (a)( a ) 的每一位和它自己比较,每一位都相同,所以按位异或后结果全是 0。

性质 2:任何数和 0 异或,结果是它本身

[a0=a][ a \oplus 0 = a ] 为什么?

  • (0)( 0 ) 的二进制位全是 0,和任何数的二进制按位异或,结果就是原数的二进制。

性质 3:异或满足交换律和结合律

[aba=(aa)b=0b=b][ a \oplus b \oplus a = (a \oplus a) \oplus b = 0 \oplus b = b ] 为什么?

  • 异或运算的每一位运算是独立的,顺序不影响结果。
  • 相同的数会成对抵消,剩下的就是唯一的数字。

3. 异或背后的数学原理

异或的“抵消”本质上是通过模 2 加法实现的:

  1. 模 2 加法的特性
    • 任何数加上自己两次,结果是 0(类似于循环加法)。
    • 这是因为模 2 运算中 (1+1=0)( 1 + 1 = 0 )

示例:

  • 假设 (a=2)( a = 2 )(b=3)( b = 3 ),我们来计算 (ab)( a \oplus b )
    1. 2 的二进制是 10,3 的二进制是 11
    2. 按位异或:10 \oplus 11 = 01(即 1)。
  • 如果再异或一次原来的 a :
    • 01 \oplus 10 = 11,得到的是 (b=3)( b = 3 )

这表明,通过异或,可以在不使用额外存储的情况下“记住”和“抵消”数字。


4. 异或的神奇效果:抵消重复数字

来看具体的算法应用场景:找出数组中唯一的数字。

问题:数组 [2, 3, 2]

  1. 初始化:result = 0
  2. 第一次异或:result = 0 \oplus 2 = 20 不影响结果)
  3. 第二次异或:result = 2 \oplus 3 = 1(两数按位异或)
  4. 第三次异或:result = 1 \oplus 2 = 32 被抵消)

最终结果是唯一的数字 3

为什么神奇?

  • 相同数字消失:每个数字与自己异或后会变成 0,不影响结果。
  • 唯一数字保留:唯一的数字没有配对,它的值最终会“留在”异或结果中。

5. 这种“神奇”的实际应用

异或不仅仅是用来找唯一数字,在很多算法中都有类似的应用场景:

  1. 找两个数组的差异:异或两个数组后,差异的数字会保留。
  2. 加密和解密:异或是对称加密的一种方式,加密和解密的操作是一样的。
  3. 位运算的优化:在涉及集合运算、数据配对等问题时,异或可以简化操作并提高效率。

6. 总结:神奇效果的本质

按位异或的“神奇效果”来源于它的数学性质:

  1. 模 2 运算:通过模 2 加法实现自动抵消和保留。
  2. 交换律和结合律:让异或的顺序不重要,方便遍历数组。
  3. 按位计算的特性:直接操作二进制位,不依赖复杂的数据结构。

它的神奇之处在于:

  • 用极少的内存和极高的效率完成了“抵消”和“保留”的过程。
  • 将复杂问题(比如配对和消除)转化为简单的数学运算。

更重要的是,我认识到按位异或和我们在逻辑学中学到的“逻辑异或”完全是两回事:

  • 逻辑异或是一种高层逻辑运算,表示两个输入中恰好有一个为真时输出为真,其结果是布尔值。
  • 按位异或则是一种底层数学运算,对二进制的每一位单独操作,输出的结果是一个数字。

按位异或是计算机世界中的一种“魔法”,它的魅力在于高效而简洁,这种特性完全基于二进制的本质。


这道题的设计目的:领略按位异或的魅力

随着理解的深入,我也意识到这道题的真正目的。它并不是单纯为了让我们找出唯一的数字,而是为了展示按位异或的魅力。按位异或是计算机世界的一种“魔法”,它既高效又神奇,展现了二进制运算的强大之处。


一个 INFJ 的感悟:解题的思维旅程

对我来说,完成这道题不仅仅是解开了一道题目,更像是一场心理旅程。作为 INFJ,我需要先一步步分析自己的思路,解决内心的困惑,然后通过不断地讨论、提问,最终才能直面问题的逻辑本质。这样的过程可能比其他人慢,但正是这种慢让我对算法背后的思想有了更深的体会。


结语

《冰雪奇缘》中,Elsa 天生具有掌控冰雪的魔法,这种特性是她的本质。同样,按位异或的“抵消能力”也是它的本质——源于二进制,天生具备。对于我们这些初学者来说,理解它的意义远比完成一个简单的任务更重要。它让我们窥见了计算机世界的独特之处,也让我对算法有了更多期待。