面试官最初的问题陈述为“...并且允许使用多线程”。然而,这个问题的措辞可能有点含糊,但问题的精神是显而易见的:面试官要求候选人编写一个程序来解决问题,并分析/证明在所提出的解决方案中使用(或不使用)多线程的用途。这是一个简单的问题,用于测试候选人思考大规模问题并解释他们所做的算法选择的能力,确保候选人不只是从互联网网站上复制一些东西而没有理解。
鉴于此,无论是否使用多线程,这个特定的面试问题都可以高效地在O(n log n)(渐近意义上)内解决,而多线程可以被用来对实际执行时间进行对数加速。
解决方案概述
如果您被一家顶级公司问到OP的问题,以下方法将显示您真正理解了问题和涉及的问题。我们提出了一个两阶段的方法:
1.首先将文件分区并读入内存。
2.在分区上使用特殊版本的归并排序,同时在文件排序时计算每个名称的频率。
例如,让我们考虑一个包含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)完全做到了这一点,删除了哈希表,并采用归并排序,这是一种分而治之算法,可以将问题分解为相互独立的子问题。
多线程和分区以进一步提高执行时间
为了实现合并排序的多线程,可以将文件分成若干个分区,并创建新线程来合并每对相邻的分区。由于文件中的名称长度不固定,因此必须从头到尾按顺序扫描文件才能进行分区;不能使用文件的随机访问。然而,由于任何解决方案都必须至少扫描一次文件内容,因此仅允许串行访问文件仍然可以产生最佳解决方案。
对于多线程解决方案(2),可以期望在执行时间上获得怎样的加速?这种算法的分析非常棘手,尽管很简单,已经成为各种白皮书的研究对象。然而,将文件分成n个分区将使程序比在单个CPU上不分区执行快(n / log(n))倍。简单地说,如果单个处理器需要1小时处理一个640GB的文件,则将文件分成64个10GB的块,并在具有32个CPU的机器上执行,程序将在约6分钟内完成,增加了10倍(忽略磁盘开销)。