面试题之10亿正整数问题
部分解答(还有没有完成的部分):
增加内容:
在写了这篇博客之后,又将《编程珠玑》拿出来将里面的内容看了一些,不过倒是有些地方没看明白了,这里写出来看看有没有同学能看出来我哪里理解错了(应该不会是写错了)
在《编程珠玑》中,这个问题是被作为一个故事来引入的。为了让没有该本书的同学更加清楚题意,我来简单描述一下。(补充:这本书很薄,一共连答案217页,售价RMB 28.00,大家一般买还能打折,可以买本来看看。我在CSDN上找到了相关的下载,为中英文都有的,而且是PDF格式。不过不知道英文是怎么排版的,还有就是有源码比较好,链接如下:http://d.download.csdn.net/down/323362/Wuaner,OK,题外话结束。)
一开始程序员提出的问题很简单,“我该如何对磁盘文件进行排序”。
【外排序,解决方案一 – 归并排序】
OK,首先就找本“常见的数据结构书”来看看外排序吧。我这里原本有两本,不过有本书不知道扔在哪里,就取了殷人昆的那本黄书~其9.7节介绍了外排序。
其中介绍了,外排序多使用归并排序的方法。
书中将排序过程分为两个阶段,第一个阶段建立为外排序所用的内存缓冲区,根据它们的大小将输入文件划分为若干段,然后用某种有效的内排序方法对各段进行排序,这些经过排序的段叫做初始归并段或初始顺串,当它们生成后就被写到外存中去;
第二个阶段仿照内排序中所介绍过的归并树模式,把第一个阶段生成的初始归并段加以归并,一趟趟地扩大归并段和减少归并段个数,直到最后归并成一个大归并段(有序文件)为止。
这里第一个阶段我们应该比较熟悉,其实和内排序差不多,区别就是分段输入,以及输出初始归并段到硬盘文件中。
外排序和内排序主要的区别就是在将硬盘中的多个有序文件归并到一个最终的有序文件。
过程:
简单的2路归并,对两个归并段进行归并时。仅需把这两个归并段中的对象逐块读入内存,进行比较后,写入大归并段,然后再进行读出,所以这种方法能够对很大的归并段进行归并。而其他的内排序方法很难用于外排序。包括插入排序、希尔排序、冒泡排序、快速排序、选择排序、堆排序、基数排序。
这种方法,需要1次读入输入文件,多次读入/读出中间文件,1次读出输出文件。
之后回到书上的内容来,此时作者回答了程序员的问题,在流行编程书籍中的磁盘排序程序大概有10多个函数,200行程序代码(有这么多吗?)对于这些代码,实现和测试大概最多需要花费程序员1周的时间。
这当然不是最好的答案,所以才有了之后的交谈。还是用谈话的内容来介绍这一段比较好。斜体为作者的问题,然后是程序员的回答。
需要排序的内容究竟是什么?文件中有多少记录?每个记录的格式是什么?
该文件包含至多10000000个记录,每条记录都是一个7位整数。
等一下。假如文件那么小的话,为什么还要费力地使用磁盘排序呢?为什么不在主存储器中对它进行排序呢?
尽管机器有很多MB的主存储器,但是该排序功能属于某个大型系统中的一部分。我想实际上我可能只有1MB的空闲主存。(一个可以看出以前就算是大型系统,内存还是很紧俏啊,还有就是如何能够估算出自己能够使用的内存呢?因为现在我们的程序都是架在虚存上面的,每个进程都有属于自己的4G空间。)
你能将有关记录方面的内容说得更详细一点吗?
每个记录都是一个7位正整数,并且没有其他的关联数据,每个整数至多只能出现一次。
作者主要通过上面的对话,将问题了解得更清楚了。同时,根据其他的对话,最终了解到这个文本是美国的电话号码的一个存储文件。在美国,电话号码由3位区号与7位其他号码组成。拨打包含免费区号800的电话是不收费的。实际的免费电话号码数据库包含有大量的信息,包括免费电话号码,拨打的实际号码,用户名称和地址等等。
程序员所要处理的就是这样的一个文本数据库,将要进行排序的整数就是那些免费电话号码。输入文件是一个号码列表(其他信息都被删除了。由此可见,即使是这样的一个系统内的一个小模块,也在前面还做了其他的工作,这里的输入文件已经是经过处理的了。),并且同一号码出现两次以上将是一个错误(那这个错误是否需要处理,由谁来保证?)。预期的输出是一个包含大量号码,并且以升序的方式进行排序的文件。
同时关于性能,实际环境同时也定义了性能需求。在与系统进行长时间的会话期间,用户请求排序文件的频率大约是每小时一次。在完成排序之前,用户不能做任何事情。因此排序时间不能太长,最合适的运行时间是10秒钟。
【整理之后,精确的问题陈述】
输入:
所输入的是一个文件,至多包含n个正整数,每个正整数都要小于n,这里的n为10^7。如果输入时某一个整数出现了两次,就会产生一个致命的错误。这些整数与其他任何数据都不关联。
输出:
以增序形式输出经过排序的整数列表。(这里应该补充一下是文件形式吗?)
约束:
至多(大概)只有1MB的可用主存,但是可用磁盘空间非常充足。运行时间至多只允许几分钟,最适宜的时间大概为10秒钟。
【解决方案二 – 多通道排序】
将每个号码存储在7个字节(byte)里,1M空间共有=1024*1024=1048576 bytes。
所以1M中共能存储149796个号码。
如果将每个号码表示成32位的整数(也就是4个bytes),那1M中共能存储262144个号码。
(书中的数字分别为143000和250000。)
下面就对这种情况下,使用多通道程序进行外排序。
因为一共有10000000个整数,而1M中我们使用250000个号码存储,所以需要使用40个通道。
第一个通道中将0到249999之间的任意整数读到内存中,并对这250000个整数进行排序,然后将它们写入输出文件中。
第二个通道对250000到499999之间的整数进行排序,然后也是同样写入输出文件中。
依次类推,直到第40个通道。
第40个通道将排序9750000到9999999之间的整数,然后写入输出文件中。
注意,这里意思是输出文件是同一个文件。
快速排序在主存中相当有效,它只需要20行代码。因此整个程序只需1到2页的代码即可实现,并且该程序还有一个令人满意的特性,即我们不必担心使用中间磁盘文件的问题。
40个通道的算法不使用中间文件,需要多次读取输入文件,但只进行一次输出文件的写入操作。
有些疑问,怎么做到是同一个输出文件的。难道输出之后,整个文件就是有序的了?没有想明白。大家看看呢?