面试题之10亿正整数问题

10亿个正整数,只有其中1个数重复出现过,要在O(n)的时间里面找出这个数,内存要尽可能少(小于100M)。

谢谢absolute同学提出的问题。

部分解答(还有没有完成的部分): 

首先看一下10亿个正整数,正整数可以表示的范围为1到2的31次方-1。
10亿也就是1*10^9,2^31次方=2*1024*1024*1024>20亿
再想起int为32位。
再想起位图法。
位图法也就是对于出现的数,其中每1bit代表这个数,如果该位为1,则说明该数出现;如果该位为0,则说明该数没有出现。
那多大的内存能够表示10亿的数呢?
1 byte = 8 bit
1024 byte = 8*1024 bit = 1k
1024 k = 8*1024*1024 bit = 1M = 8388608 bit
将10,0000,0000处以8388608得到119.20928955078125
也就是差不多120M的内存,可以表示全10亿的数。

所以可以建立120M的一个位图,将其所有位设置为0,然后开始遍历这10亿个整数,每遍历一个,则对应到位图中相应的位置1,如果对应到位图中相应的位已经置1了,则说明这个数是要找的那个重复的数。用这种方法,最多就是遍历一遍,将这个10亿个正整数遍历完。而使用的内存为120M左右。

当然,题目中要求是小于100M。其实写到这里,似乎感觉这个题目是在哪里看到过。似乎是《编程珠玑》或者类似的书中,当然,最初的来源肯定是编程珠玑,关于电话号码的部分。

于是下一步我就是将这本书翻出来,结果就是在开篇就是关于这个问题。
不过我们遇到的问题是10亿个数,100M内存。而书中的问题是10^7个正整数,1M的可用主存。书中的问题乘以100,就正好是我们遇到的问题了。不过书中的问题是去掉所有重复的数,并将结果是一个有序的排列。

如果严格的使用100M以下内存的话,我们只能利用磁盘作为虚拟存储空间。
如果使用磁盘的话,应该就会涉及到外排序之类的。
或者是虚拟内存的管理,页面的换入换出?

其实我们这里的问题并不需要完全排序,而只是需要找出重复的数就可以。是否可以不用排序就得到?

关于两通道算法,我得复习一下相关书籍了。。。

再想想,其实题目出的有问题,应该是最大不会超过10亿,不然位图法也不行。或者就需要做hash来得到对应关系了。


增加内容:

在写了这篇博客之后,又将《编程珠玑》拿出来将里面的内容看了一些,不过倒是有些地方没看明白了,这里写出来看看有没有同学能看出来我哪里理解错了(应该不会是写错了)

在《编程珠玑》中,这个问题是被作为一个故事来引入的。为了让没有该本书的同学更加清楚题意,我来简单描述一下。(补充:这本书很薄,一共连答案217页,售价RMB 28.00,大家一般买还能打折,可以买本来看看。我在CSDN上找到了相关的下载,为中英文都有的,而且是PDF格式。不过不知道英文是怎么排版的,还有就是有源码比较好,链接如下:http://d.download.csdn.net/down/323362/WuanerOK,题外话结束。)

一开始程序员提出的问题很简单,“我该如何对磁盘文件进行排序”。

【外排序,解决方案一 归并排序】

OK,首先就找本“常见的数据结构书”来看看外排序吧。我这里原本有两本,不过有本书不知道扔在哪里,就取了殷人昆的那本黄书~9.7节介绍了外排序。

其中介绍了,外排序多使用归并排序的方法。

书中将排序过程分为两个阶段,第一个阶段建立为外排序所用的内存缓冲区,根据它们的大小将输入文件划分为若干段,然后用某种有效的内排序方法对各段进行排序,这些经过排序的段叫做初始归并段或初始顺串,当它们生成后就被写到外存中去;

第二个阶段仿照内排序中所介绍过的归并树模式,把第一个阶段生成的初始归并段加以归并,一趟趟地扩大归并段和减少归并段个数,直到最后归并成一个大归并段(有序文件)为止。

这里第一个阶段我们应该比较熟悉,其实和内排序差不多,区别就是分段输入,以及输出初始归并段到硬盘文件中。

外排序和内排序主要的区别就是在将硬盘中的多个有序文件归并到一个最终的有序文件。

过程:

简单的2路归并,对两个归并段进行归并时。仅需把这两个归并段中的对象逐块读入内存,进行比较后,写入大归并段,然后再进行读出,所以这种方法能够对很大的归并段进行归并。而其他的内排序方法很难用于外排序。包括插入排序、希尔排序、冒泡排序、快速排序、选择排序、堆排序、基数排序。

这种方法,需要1次读入输入文件,多次读入/读出中间文件,1次读出输出文件。

之后回到书上的内容来,此时作者回答了程序员的问题,在流行编程书籍中的磁盘排序程序大概有10多个函数,200行程序代码(有这么多吗?)对于这些代码,实现和测试大概最多需要花费程序员1周的时间。

这当然不是最好的答案,所以才有了之后的交谈。还是用谈话的内容来介绍这一段比较好。斜体为作者的问题,然后是程序员的回答。

需要排序的内容究竟是什么?文件中有多少记录?每个记录的格式是什么?

该文件包含至多10000000个记录,每条记录都是一个7位整数。

等一下。假如文件那么小的话,为什么还要费力地使用磁盘排序呢?为什么不在主存储器中对它进行排序呢?

尽管机器有很多MB的主存储器,但是该排序功能属于某个大型系统中的一部分。我想实际上我可能只有1MB的空闲主存。(一个可以看出以前就算是大型系统,内存还是很紧俏啊,还有就是如何能够估算出自己能够使用的内存呢?因为现在我们的程序都是架在虚存上面的,每个进程都有属于自己的4G空间。)

你能将有关记录方面的内容说得更详细一点吗?

每个记录都是一个7位正整数,并且没有其他的关联数据,每个整数至多只能出现一次。

 

作者主要通过上面的对话,将问题了解得更清楚了。同时,根据其他的对话,最终了解到这个文本是美国的电话号码的一个存储文件。在美国,电话号码由3位区号与7位其他号码组成。拨打包含免费区号800的电话是不收费的。实际的免费电话号码数据库包含有大量的信息,包括免费电话号码,拨打的实际号码,用户名称和地址等等。

程序员所要处理的就是这样的一个文本数据库,将要进行排序的整数就是那些免费电话号码。输入文件是一个号码列表(其他信息都被删除了。由此可见,即使是这样的一个系统内的一个小模块,也在前面还做了其他的工作,这里的输入文件已经是经过处理的了。),并且同一号码出现两次以上将是一个错误(那这个错误是否需要处理,由谁来保证?)。预期的输出是一个包含大量号码,并且以升序的方式进行排序的文件。

同时关于性能,实际环境同时也定义了性能需求。在与系统进行长时间的会话期间,用户请求排序文件的频率大约是每小时一次。在完成排序之前,用户不能做任何事情。因此排序时间不能太长,最合适的运行时间是10秒钟。

 

【整理之后,精确的问题陈述】

输入:

所输入的是一个文件,至多包含n个正整数,每个正整数都要小于n,这里的n10^7。如果输入时某一个整数出现了两次,就会产生一个致命的错误。这些整数与其他任何数据都不关联。

输出:

以增序形式输出经过排序的整数列表。(这里应该补充一下是文件形式吗?)

约束:

至多(大概)只有1MB的可用主存,但是可用磁盘空间非常充足。运行时间至多只允许几分钟,最适宜的时间大概为10秒钟。

【解决方案二 多通道排序】

将每个号码存储在7个字节(byte)里,1M空间共有=1024*1024=1048576 bytes

所以1M中共能存储149796个号码。

如果将每个号码表示成32位的整数(也就是4bytes),那1M中共能存储262144个号码。

(书中的数字分别为143000250000。)

 

下面就对这种情况下,使用多通道程序进行外排序。

因为一共有10000000个整数,而1M中我们使用250000个号码存储,所以需要使用40个通道。

第一个通道中将0249999之间的任意整数读到内存中,并对这250000个整数进行排序,然后将它们写入输出文件中。

第二个通道对250000499999之间的整数进行排序,然后也是同样写入输出文件中。

依次类推,直到第40个通道。

40个通道将排序97500009999999之间的整数,然后写入输出文件中。

注意,这里意思是输出文件是同一个文件。

快速排序在主存中相当有效,它只需要20行代码。因此整个程序只需1到2页的代码即可实现,并且该程序还有一个令人满意的特性,即我们不必担心使用中间磁盘文件的问题。

40个通道的算法不使用中间文件,需要多次读取输入文件,但只进行一次输出文件的写入操作。 

有些疑问,怎么做到是同一个输出文件的。难道输出之后,整个文件就是有序的了?没有想明白。大家看看呢?

posted on 2009-11-06 23:08  cnyao  阅读(4546)  评论(18编辑  收藏  举报