今天是我注册字节跳动青训营的第一天,正式开始刷算法题。作为一名人工智能方向的研究生,虽然听起来专业对口,但其实我是一个“转码”的学生,本科并不是学计算机相关专业。对于编程,我只能说掌握了一些非常基础的内容,尤其在算法这块,心理上一直有些障碍。
心理障碍与 INFJ 的思维模式
在做算法题时,我总是无法直接进入问题的逻辑深处。这也许和我的性格类型 INFJ 有关吧。INFJ 的思维习惯让我需要从感性上理解一个问题,需要经过情绪的梳理、自我体验以及对逻辑背后的本质透彻感知,才能真正抓住问题的核心。换句话说,我必须先解决自己的心理困惑,才能全身心投入到逻辑分析中。这种思维模式让我在面对算法题时,总感觉无从下手,但又忍不住一步步地去深究每个细节。
我的第一道算法题:找出唯一的数字
这道题目非常简单,要求从一个列表中找出唯一的数字。其他数字都恰好出现两次,而唯一的数字只出现一次。例如,给定数组 [7, 3, 3, 7, 10],唯一的数字是 10。听起来,这只是一个普通的任务,但我发现自己思考的方向和题目设计的意图完全背道而驰。
我的初步解法:两个数组,空间和时间复杂度的困境
在我不理解按位异或特性的前提下,我自然是用最直观的方式来解决问题。我的思路是:
- 使用一个数组存储原始数据。
- 用一个临时数组来记录未配对的数字。
- 遍历每个数字,将它与临时数组中的数字逐个比较。如果找到配对,就从临时数组中移除;如果没有配对,就将它加入临时数组。
- 最终,临时数组中剩下的唯一数字就是结果。
虽然这个思路逻辑上是通的,但却存在巨大的性能问题:
- 时间复杂度:每次取出一个数字,都需要与临时数组中的所有数字进行比较。如果临时数组长度逐渐增大,时间复杂度达到 ( O(N^2) )。
- 空间复杂度:需要额外的一个数组存储未配对的数字,空间复杂度为 ( O(N) )。
让我用更加细致的数学推导去计算时间复杂度。并且一步步验证计算是否正确,同时解释为什么这个过程最终仍然属于 (O(N^2))。
1. 我的操作逻辑和总操作次数的计算:
-
每当取出第 个数字时,需要与前 个数字比较一次。
-
假设班级有 个数字,最坏情况下,前 个数字都没有找到配对,需要比较: 这是一个等差数列,求和公式是: 项数是 ,首项是 1,末项是 ,所以:
-
这是所有前 个数字没有找到配对时的比较次数。接下来,每找到一个配对的数字,临时数组中存储的数字会减少,所以操作次数逐渐减少。
2. 总操作次数如何表示:
这个操作次数的细致分析是对的——最坏情况下,比较次数由两部分组成:
- 临时数组增长的阶段(即存储没有找到配对的数字):
- 比较次数是 ,如上所述,总和约为 。
- 临时数组减少的阶段(即找到配对后减少存储的数字):
- 假设每找到一个配对的数字,临时数组中存储的数字减少,比较次数逐步下降,呈现类似对称的等差数列。
总体上,操作次数会在增长阶段和减少阶段的两部分叠加,依然是一个 的规模。
3. 为什么是 ,而不是具体的 :
大 O 表示法 是一种忽略常数和低阶项的数学表示法。它的目的是描述算法的增长趋势,而不是精确地计算操作次数。
对于我的算法:
- 最坏情况下,操作次数大约是 ,但在大 O 表示法中,常数系数 会被省略,因为在输入规模很大时,它对增长趋势的影响可以忽略。
- 因此,我们直接表示为 。
4. 举例计算对比
我们用具体数字举个例子来验证:
假设 :
- 在增长阶段:
- 比较次数是 。
- 在减少阶段:
- 假设剩下的数字逐步配对,比较次数会对称地下降 。
总操作次数是 。
对于 ,我们发现操作次数与 是同数量级(系数为 )。
5. 总结和核心理解
- 我的算法在最坏情况下,每次需要和临时数组中的所有数字比较一次,这导致操作次数呈现平方增长。
- 等差数列的累积求和表明计算过程是正确的,但在大 O 表示法中,所有系数和低阶项都会被忽略,因此仍然是 。
- 大 O 表示法的目的是描述算法的增长趋势,而不是精确计算操作次数。
按位异或的登场:逻辑的突破
在经过一番挣扎后,我向 ChatGPT 请教,它向我介绍了一种完全不同的解决方案——按位异或。这种方法不仅能将时间复杂度降为 ,还将空间复杂度优化为 。然而,初次听到这个方法,我是完全无法理解的。
比如,[2, 3, 2] 这个例子:
- 我理解为什么
2 ^ 2 = 0,因为两个相同的数字会被“抵消”。 - 但当
1 ^ 2 = 3时,我却陷入了困惑:“为什么两个不同的数字异或会得出另一个完全不相关的数字?”
按位异或的魅力:计算机的魔法,与逻辑学异或的不同
1. 按位异或(XOR)的核心原理
按位异或运算的定义:
- 在二进制下,按位比较两个数:
- 如果两个位相同,则结果为
0。 - 如果两个位不同,则结果为
1。
- 如果两个位相同,则结果为
简单记忆:
数学表示:
对于任意两位 和 ,异或的结果可以用公式表示:
- 和 的和除以 2 的余数,就是异或的结果。
2. 异或的三个关键性质
性质 1:相同的数异或结果为 0
为什么?
- 的每一位和它自己比较,每一位都相同,所以按位异或后结果全是 0。
性质 2:任何数和 0 异或,结果是它本身
为什么?
- 的二进制位全是 0,和任何数的二进制按位异或,结果就是原数的二进制。
性质 3:异或满足交换律和结合律
为什么?
- 异或运算的每一位运算是独立的,顺序不影响结果。
- 相同的数会成对抵消,剩下的就是唯一的数字。
3. 异或背后的数学原理
异或的“抵消”本质上是通过模 2 加法实现的:
- 模 2 加法的特性:
- 任何数加上自己两次,结果是 0(类似于循环加法)。
- 这是因为模 2 运算中 。
示例:
- 假设 ,,我们来计算 :
- 2 的二进制是
10,3 的二进制是11。 - 按位异或:
10 \oplus 11 = 01(即1)。
- 2 的二进制是
- 如果再异或一次原来的 a :
01 \oplus 10 = 11,得到的是 。
这表明,通过异或,可以在不使用额外存储的情况下“记住”和“抵消”数字。
4. 异或的神奇效果:抵消重复数字
来看具体的算法应用场景:找出数组中唯一的数字。
问题:数组 [2, 3, 2]
- 初始化:
result = 0 - 第一次异或:
result = 0 \oplus 2 = 2(0不影响结果) - 第二次异或:
result = 2 \oplus 3 = 1(两数按位异或) - 第三次异或:
result = 1 \oplus 2 = 3(2被抵消)
最终结果是唯一的数字 3。
为什么神奇?
- 相同数字消失:每个数字与自己异或后会变成
0,不影响结果。 - 唯一数字保留:唯一的数字没有配对,它的值最终会“留在”异或结果中。
5. 这种“神奇”的实际应用
异或不仅仅是用来找唯一数字,在很多算法中都有类似的应用场景:
- 找两个数组的差异:异或两个数组后,差异的数字会保留。
- 加密和解密:异或是对称加密的一种方式,加密和解密的操作是一样的。
- 位运算的优化:在涉及集合运算、数据配对等问题时,异或可以简化操作并提高效率。
6. 总结:神奇效果的本质
按位异或的“神奇效果”来源于它的数学性质:
- 模 2 运算:通过模 2 加法实现自动抵消和保留。
- 交换律和结合律:让异或的顺序不重要,方便遍历数组。
- 按位计算的特性:直接操作二进制位,不依赖复杂的数据结构。
它的神奇之处在于:
- 用极少的内存和极高的效率完成了“抵消”和“保留”的过程。
- 将复杂问题(比如配对和消除)转化为简单的数学运算。
更重要的是,我认识到按位异或和我们在逻辑学中学到的“逻辑异或”完全是两回事:
- 逻辑异或是一种高层逻辑运算,表示两个输入中恰好有一个为真时输出为真,其结果是布尔值。
- 按位异或则是一种底层数学运算,对二进制的每一位单独操作,输出的结果是一个数字。
按位异或是计算机世界中的一种“魔法”,它的魅力在于高效而简洁,这种特性完全基于二进制的本质。
这道题的设计目的:领略按位异或的魅力
随着理解的深入,我也意识到这道题的真正目的。它并不是单纯为了让我们找出唯一的数字,而是为了展示按位异或的魅力。按位异或是计算机世界的一种“魔法”,它既高效又神奇,展现了二进制运算的强大之处。
一个 INFJ 的感悟:解题的思维旅程
对我来说,完成这道题不仅仅是解开了一道题目,更像是一场心理旅程。作为 INFJ,我需要先一步步分析自己的思路,解决内心的困惑,然后通过不断地讨论、提问,最终才能直面问题的逻辑本质。这样的过程可能比其他人慢,但正是这种慢让我对算法背后的思想有了更深的体会。
结语
《冰雪奇缘》中,Elsa 天生具有掌控冰雪的魔法,这种特性是她的本质。同样,按位异或的“抵消能力”也是它的本质——源于二进制,天生具备。对于我们这些初学者来说,理解它的意义远比完成一个简单的任务更重要。它让我们窥见了计算机世界的独特之处,也让我对算法有了更多期待。