面试官最初的问题是“......并且允许使用多线程”。这个问题的措辞可能有点模棱两可,但问题的精神很明显:面试官要求候选人编写一个程序来解决问题,并分析/证明在建议的解决方案。这是一个简单的问题,用于测试候选人思考大规模问题并解释他们做出的算法选择的能力,确保候选人没有在不理解的情况下从互联网网站上反刍。
鉴于此,无论是否使用多线程,这个特定的面试问题都可以在 O( n log n )(渐近地说)内有效地解决,并且多线程还可以用于以对数方式加速实际执行时间。
解决方案概述
如果您被顶级公司问到 OP 的问题,以下方法将表明您确实了解问题和所涉及的问题。在这里,我们提出了一个两阶段的方法:
该文件首先被分区并读入内存。
分区上使用了特殊版本的合并排序,该分区在对文件进行排序时同时记录每个名称的频率。
例如,让我们考虑一个具有 32 个名称的文件,每个名称长一个字母,并且每个名称的初始频率计数为 1。上述策略可以形象化如下:
1. File: ARBIKJLOSNUITDBSCPBNJDTLGMGHQMRH 32 Names
2. A|R|B|I|K|J|L|O|S|N|U|I|T|D|B|S|C|P|B|N|J|D|T|L|G|M|G|H|Q|M|R|H 32 Partitions
1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1 with counts
3. AR BI JK LO NS IU DT BS CP BN DJ LT GM GH MQ HR Merge #1
11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 and tally
4. ABRI JKLO INSU BDST BCNP DJLT GHM HMQR Merge #2
1111 1111 1111 1111 1111 1111 211 1111 and tally
5. ABIJKLOR BDINSTU BCDJLNPT GHMQR Merge #3
11111111 1111211 11111111 22211 and tally
6. ABDIJKLNORSTU BCDGHJLMNPQRT Merge #4
1212111111211 1112211211111 and tally
7. ABCDGHIJKLMNOPQRSTU Merge #5
1322111312132113121 and tally
因此,如果我们从头到尾读取内存中的最终列表,它会产生排序列表:
A|B|C|D|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U
-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
1|3|2|2|1|1|1|3|1|2|1|3|2|1|1|3|1|2|1 = 32 Name instances (== original file).
为什么解决方案是有效的
无论是否使用哈希表(如原始海报所建议的那样),以及是否使用多线程,这个问题的任何解决方案都不能比 O( n log n ) 更有效地解决,因为必须执行排序。鉴于此限制,可以采用两种策略:
从磁盘读取数据,使用哈希表管理名称/频率总计,然后对哈希表内容进行排序(原发帖人建议的方法)
从磁盘读取数据,用文件中的频率总数初始化每个名称,然后合并排序名称,同时对每个名称的所有总数求和(此解决方案)。
方案(1)要求在读入所有数据后对哈希表进行排序。方案(2)在排序时进行频率统计,从而消除了哈希表的开销。完全不考虑多线程,我们已经可以看到,即使对于解决方案 (1) 使用最有效的哈希表实现,解决方案 (2) 已经更有效,因为它根本没有哈希表的开销。
多线程的约束
在解决方案 (1) 和解决方案 (2) 中,假设解决方案 (1) 使用了有史以来最有效的哈希表实现,两种算法都在 O( n log n ) 中渐近执行相同的操作;只是它们的操作顺序略有不同。然而,虽然多线程解决方案 (1) 实际上会减慢其执行速度,但多线程解决方案 (2) 将在速度上获得显着提高。这怎么可能?
如果我们使用多线程解决方案 (1),无论是在从磁盘读取还是在之后的排序中,都会遇到哈希表争用问题,因为所有线程都试图同时访问哈希表。特别是对于写入表,这种争用可能会大大削弱解决方案 (1) 的执行时间,以至于在没有多线程的情况下运行它实际上会提供更快的执行时间。
为了使多线程能够加快执行时间,有必要确保每个线程执行的每个工作块都独立于其他每个线程。这将允许所有线程以最大速度运行而不会争用共享资源并更快地完成工作。解决方案 (2) 正是这样做的,它完全删除了哈希表并使用了Merge Sort,这是一种分而治之的算法,允许将问题分解为彼此独立的子问题。
多线程和分区以进一步缩短执行时间
为了多线程合并排序,可以将文件划分为多个分区,并创建一个新线程来合并每个连续的分区对。由于文件中的名称是可变长度的,因此必须从头到尾连续扫描文件才能进行分区;不能对文件进行随机访问。但是,由于任何解决方案都必须至少扫描一次文件内容,因此仅允许对文件进行串行访问仍然会产生最佳解决方案。
多线程解决方案 (2) 可以预期什么样的执行时间加速?鉴于其简单性,该算法的分析非常棘手,并且是各种白皮书的主题。但是,将文件分成n 个分区将使程序的执行速度 ( n / log( n )) 比在没有文件分区的单个 CPU 上快几倍。简单来说,如果单个处理器需要 1 小时来处理一个 640GB 的文件,那么将文件分成 64 个 10GB 的块并在具有 32 个 CPU 的机器上执行将使程序在大约 6 分钟内完成,增加了 10 倍(忽略磁盘开销)。