【编程珠玑】【第二章】问题A

A题

给定一个最多包含40亿个随机排列的32位整数的顺序文件,找出一个不在文件中一32位整数。有三个问题:(1在文件中至少存在这样一个数?2如果有足够的内存,如何处理?3如果内存不足,仅可以用文件来进行处理,如何处理?

答案:

(1)32位整数,包括-2146473648~~2146473647,约42亿个整数,32bit可用表示的最大无符号整数约为43亿。可见,一定存在至少一个这样的整数不被包含在40亿个整数中。

(2)如果采用位向量思想,通过建立一个大小为 2^32 的bool数组,用来表示相应的整数是否出现,进而,这种数组其实可以用第一章中学过的位图bitmap来实现,先对各位全部初始化为0。遍历输入文件,若某数出现,将该位置为1。最后,遍历一遍这个位图,所有为0的位就表示该位对应的整数没有出现过。时间复杂度为 O(n)+ 2*L (L为定值,表示 2^32,乘以2表示一次初始化和一次遍历),空间上32位整数最多需要占用43亿(2的32次方)个位,大约(2^32)/(8*10^6) = 512 MB的内存空间。

使用位向量方法,通常是逐位进行判断,但是这个例子中绝大多数的位都被置为1,只有极少的位为0,所以为了加速比较可以直接粗粒度的对int进行比较而不是对bit进行test(i)比较。为-1的二进制表示是全1的,判断每个int是否等于-1,若等于意味着该Int的32位都为1,否则说明其中某一位为0,该位对应的整数缺失。

这个方法的最大缺点,就是耗费内存。这时采用多趟算法,假如我们只有 500 B内存,就是说我们一次只能申请到 0~3999 个可用位(实际上加上其他开销,可能申请不到这么多),可以先遍历一趟文件,只测试在 0~3999 之间的整数,第二趟,测试 4000~7999 ,依次类推……。(如果题目要求只找到一个数就停止,那这种方法应该很不错。。)

(3.1)内存不足(假如只有1M内存),可以采用散列分桶的思想:输入的40亿个数

2的32次方个数(近43亿)用位向量表示需要512MB的内存,可是实际上只有1MB内存,这样不能够直接使用位向量记录输入文件中的40亿个整数(相应位置为0)并进而判断没有被记录的那些整数(相应位为0)。我们可以使用分桶的策略,把2的32次方分成一个个能够被装入内存中的小桶,每个小桶进行独立的处理,这样处理完所有的桶之后便能够输出所有的不在文件中的整数。此策略的缺点是需要重复的遍历输入文件和重复的进行每个桶的处理,优点是以时间代价来满足空间限制、且思路简单易懂。1MB内存空间有2^23个bits,可以表示2^23个整数,而整个整数范围为2^32,因此需要划分为2^(32-23)=2^9个桶,即512个桶。每个桶内仍使用位向量方法进行判断。

(3.2)题目中并没有说输入的40亿个数字中有没有重复,只说明了是32位的整数,这意味着40亿的输入数据一定不能够覆盖全部的2^32的范围,无论是否有重复,如果有重复意味着40亿的输入数据所缺失的数字更多,所以也没有影响。任意将40亿个数字分为两个集合,能确定至少有一个集合(数据量少的那个)会有缺失的项。

因此这里考虑二分法(这里要摒弃固定思维,二分法并不是什么情况下都需要排序后才能使用)。二分法思想是这样的:我们读取输入文件并从中取出整数,根据高位为1及高位为0将这些整数分为两类放到不同文件里,这个过程不需要多少工作内存,几十个byte足够,设高位为1的放入文件A中,为0的放入B中。

(3.2.1)当A中数据量和B一样多时,说明两组中都有遗漏的数据(遗漏一个或者多个,遗漏的数目相同),因为数据范围为43亿,实际数据只有40亿(没有重复的情况下,有重复的话则更少),最多每组只有20亿,不可能都包含43亿数据中的一半数据。此时任取其中一组进行接下来的二分即可。

(3.2.2)如果A数据量和B数据量不同时,可以肯定的是数据量少的那组一定缺少某一个或者多个数,而数据量多的那组遗漏与否是不一定的。在没有重复的情况下可以通过比较数据量多的那组数据量和2^32/2值的大小来判断它是否包含完整的一半数据,若有重复的情况下难以直接判断数据量多的那组是否缺项。此时我们取数据量小的(可以使得接下来的二分需要处理的数据更少,数据量多的那个则有可能没有不存在的数)那组进行二分——然后递归前面的步骤,最终会找到某个不在40亿数中的数。

但是较本题目更为复杂的一个问题是,如果输入数据量大于整数范围的最大值,且输入的数据中有重复数据,此时问题比较难以处理,若仍要使用二分法可能要进行精密的改动。下面将要讲的是来自网上的一个例子,例子能够很好地解释前述的二分法过程,但是并不能正确的应用于该例子假设的所有情况,具体请详细参阅:

假设一个文件里头有20个4bit的整数,需要找出其中遗漏的数字。我们一次从中取出一个数字,如果是最高位为1,那么放到一个文件A中,否则放到另外一个文件B中。理论上最高位为1的4bit数字不重复的共有2^3=8个,最高位为0的4bit数字同样为8个。若统计的个数少于8个肯定是这堆数中有遗漏的数。

设输入数据文件中包含的20个整数中有大量重复数字,而且导致最高位为1的数字个数可能大于8,但这不表示它其中没有缺少的数字。那到底如何去分辨哪个堆里头有缺少数字呢?在分拣到文件的过程中,程序有两个计数器,分别记录放入哪个文件的数字的个数,缺少的数字肯定在较小个数的那个文件里头。

  对{1,3,4,5,3,6,3,7,9,4,9,1,2,2,11,12,15,11,14,15}共20个整数进行二分法,最高位为1的数字共有{9,11,12,15,11,14,15}七个,而最高位为0的数字共有{1,3,4,5,3,6,3,7,4,1,2,2}十二个,我们应该选择高位为1的那组数据继续二分法。同时我们还要设一个“标兵”,当前1 000和0 000是二分法的两个边界,我们取1000为此轮二分法的分界点,值为8。

  对{9,11,12,15,11,14,15}继续二分法,第二高位为1的数字共有{12,15,15},第二高位为0的数字共有{9,11,11,14}。取较小数量的{12,15,15},又因边界为1100和1000,故“标兵”为8+4=12。

  对{12,15,15}继续二分法,第三高位为1的数字共有{15,15}两个,第三高位为0的共有{12}一个。选择较小数量的{12},又因边界为1110和1100,这时“标兵”为12。

  对{12}继续二分法,第四高位为1的数字个数零个,第四高位为0的数字为{12}一个。选择第四高位为1的空集合{},边界为1101和1100,因此这时候“标兵”为12+1=13。

  四个位判断完毕后,我们求出上例中没有的数字为13。

#include <stdlib.h>
int getLost(unsigned char *a, unsigned char *b, unsigned char *c, int alen, int bit) {
    unsigned char *t;
    int re = 0, v = 0, biter = 0, citer, i = 0;
    if (!a || !b || alen >=(unsigned long)( (1<< bit)))
        return -1;                       //这规定了输入数据量不能大过bits所能表示的最大数值。
    while (bit--) {                      //从最高位开始逐位进行二分,直到所有的bits位处理完为止。
        v = (1 << bit);                              //定位到当前最高位
        for (biter = citer = i = 0; i < alen; ++i) {  //遍历a[len]数组
            if (a[i] & (1 << bit))                   //将当前最高位为1的元素存储到b数组中。
                 b[biter++] = a[i];
            else                                 //将当前最高位为0的元素存储到c数组中。
                 c[citer++] = a[i];
        }
        if (biter <= citer) {                  //b,c数组中选择数据量较小的数组
            re += v;                           //如果b组数据量小,意味着当前高位为1的数组被选中,
            t = a; a = b; b = t;          //需要更新“标兵”re,否则不需要更新re。
            alen = biter;               //把a指向b地址,并更新alen的长度值,以便进行下次二分
        }
        else {
            t = a; a = c; c = t;
            alen = citer;
        }
    }
    return re;              //所有位二分结束后,返回“标兵”的值,即为缺失的值。
}  

int main()
{
    unsigned char b[20] ={0};         //b数组相当于文件a
    unsigned char c[20] ={0};          //c数组相当于文件b
    unsigned char a[] = {1,3,4,5,3,6,3,7,9,4,9,1,2,2,11,12,15,11,14,15};    //相当于输入数据
    printf("%d\n",getLost(a,b,c,20,4));
    system("pause");
    return 1;
}        

已知4bits二进制的整数值最大为15,按理说20个输入数据中有可能没有遗漏的数,也就是说包含所有的数,所以上述的思路实际上是错误的,它只能够处理输入数据量小于整数最大值范围的情况,不能够处理超过最大范围的情况,因此代码中用红色注释标红了注意事项,这个条件限制了输入的情况,保证了算法的适用性,足以满足编程珠玑中的题目要求。

注意输入的数据默认是无序的,这样我们在将所有输入数据根据最高位划分成两组时,需要依次遍历所有的数据并将其添加到对应的数组b或者c中存储起来。下一次二分时根据b,c的长度选择较小的数组进行类似的操作。但是若输入的数据是有序的,我们在使用二分法查找遗漏数据时,并不需要开辟新的数组来存储两组数据,只需要存储几个输入数组的下标便可,这是因为通过下标变换就能够在有序数组上随意的定位某一段范围内的元素。代码如下:

#include <stdio.h>
#include <stdlib.h>
int getLost(unsigned char *a, int length, int bitlen){
    unsigned char result = 0;
    int start = 0;
    int i,j;
    for( i = 1; i <= bitlen; i++){
        int bit0 = 0;
        int bit1 = 0;
        int mod = 1 << (bitlen - i);
        int len = length/(1<<(i-1));
        for( j = start; j < start + len; j++){
            if((a[j] & mod) == 0)
                bit0++;
            else
                bit1++;
        }
        if(bit0 < bit1)
            result |= 0<<(bitlen - i);
        else{
            result |= 1<<(bitlen - i);
            start+=len/2;
        }
    }
    return result;
}
int main(){
    unsigned char b[] = {1,3,4,5,5,6,7,8,9,10,11,12,13,14,15};
    printf("%d\n",getLost(b,15,4));
    system("pause");
    return 1;
}

(4)算法分析:书中明确指出了可以利用外部临时文件,但是内存较小,这意味着我们可以使用较小的内存和充足的外存,这给了我们什么提示呢?

这通常意味着我们不能够把大量数据读入内存中处理,只能够通过遍历输入文件,读取少量数据到内存中,进行处理后存储到临时文件里去,而后可能通过对临时文件进行处理得到最终结果。

正如本例中,输入数据量是巨大的,只能通过一条一条记录读取,根据当前数据记录分到两个临时组文件A和B中存储,而后转而处理其中一个临时组文件,其他的文件被交替使用。如此重复,最终求得结果。

复杂度分析:每次需处理的数据量都是原来的一半:n+n/2+n/4+n/8+n/2^log(2)n=2n-1;的确是O(n)的。所以不要误以为时间复杂度正比于log2(n),但要注意一共划分的次数是正比于 log2(n)的。为了便于理解,举个常见的二分查找的例子,二叉树查找目标元素,需要log2(n)次比较,每次比较的复杂度是O(1),因此二分查找的复杂度即为O(log2(n))。但是在这里,不仅需要执行log2(n)次划分,每次划分的复杂度不是1而是n/2^log2(n),所以本例中的复杂度是正比于O(n)的。

(5)上面介绍了乱序输入和排序后输入两种情况下的getlost算法,不过这两种算法只是模拟算法,仅仅用于介绍二分思想,因为他们的输入数据都是由数组保存的,在内存有限的情况下是不可能开辟出这么大的内存数组来保存输入数据的。因此,这里介绍真正的利用文件实现的算法,整体思路上与之前的算法是一致的:

/*
 * 1.定义相关变量
 * 2.如果当前探测位为最高位,意味着是第一次二分,需要读原始数据文件。
 *   如果当前探测位不是最高位,意味着需要读上次二分后所含数据元素少的临时文件。
 * 3.如果当前探测位是最低位,意味着这次二分要写的文件即为最终输出文件。
 *   如果当前探测位不是最低位,意味着本次二分要写的文件为临时输出文件。
 * 4.其他思路同上
 * 5.值得注意的是curbit和totalbit之间的关系以及mask = 1<<(curbit-1)中的curbit-1的意义。
 *6.参数totalbits是指需要进行二分的总位数,原则上要求输入数据不能超过这些位所能表示的最大值,但是代码中并没有进行错误检查等,因此该代码并不具备较高的容错性;而且每次二分都是新建临时文件比较浪费磁盘空间,可以每次读取完毕后及时删除或者重复利用固定数目的几个文件,等等的问题值得后续进行改进。
 */
#include <stdio.h>
#include <assert.h>
void GetLost(int totalbits) {
    FILE *input,*output0,*output1;
    char filename[30] = "";
    int mask,value,num0 = 0,num1 = 0,missing =0,curbit=totalbits; 

    while(curbit>0) {
        if(curbit==totalbits){
            input = fopen("source_input.txt","r");
        }else if(num0<=num1){
            sprintf(filename,"tmp_bit%d_0.txt",curbit+1);
            input = fopen(filename,"r");
        }else {
            sprintf(filename,"tmp_bit%d_1.txt",curbit+1);
            input = fopen(filename,"r");
        }
        if(curbit==1) {
            sprintf(filename,"final_output_0.txt");
            output0 = fopen(filename,"w");
            sprintf(filename,"final_output_1.txt");
            output1 = fopen(filename,"w");
        }else {
            sprintf(filename,"tmp_bit%d_0.txt",curbit);
            output0 = fopen(filename,"w");
            sprintf(filename,"tmp_bit%d_1.txt",curbit);
            output1 = fopen(filename,"w");
        }
        assert(input!=NULL && output0!=NULL&&output1!=NULL);
        num1=num0=0;
        mask = 1<<(curbit-1);    

        while(!feof(input)) {
            fscanf(input,"%d\n",&value);
            if(value&mask) {
                fprintf(output1,"%d\n",value);
                num1++;
            }else {
                fprintf(output0,"%d\n",value);
                num0++;
            }
        }
        if(num1<=num0){
            missing |= (1<<(curbit-1));
        }
        fflush(output0);
        fflush(output1);
        fclose(output0);
        fclose(output1);
        fclose(input);

        curbit--;
    }
    printf("missing number:%d\n",missing);
}

int main() {
    GetLost(4);
    return 0;
}

原文地址:https://www.cnblogs.com/blastbao/p/8306553.html

时间: 2024-10-13 00:51:56

【编程珠玑】【第二章】问题A的相关文章

一维向量旋转算法 编程珠玑 第二章

看了编程珠玑第二章,这里面讲了三道题目,这里说一下第二题,一维向量旋转算法. 题目:将一个n元一维向量(例数组)向左旋转i个位置. 解决方法:书上讲解了5种方法,自己只想起来2种最简单方法(下面讲的前两种). 1.原始方法. 从左向右依次移动一位,对所有数据平移:这样循环i次,算法最坏时间复杂度达n^2.耗时不推荐. 2.空间换时间. 顾名思义,申请一个i长度的空间,把前i半部分放到申请空间中,再把后面的所有数据向左移动i个位置,最后把申请的空间中的数据放到后半部分.浪费空间,不推荐. 3.杂技

编程珠玑第二章

编程珠玑第二章 A题 给定一个最多包含40亿个随机排列的32位整数的顺序文件,找出一个不在文件中一32位整数. 1.在文件中至少存在这样一个数? 2.如果有足够的内存,如何处理? 3.如果内存不足,仅可以用文件来进行处理,如何处理? 答案: 1.32位整数,包括-2146473648~~2146473647,约42亿个整数,而文件中只有40亿个,必然有整数少了. 2.如果采用位数思想来存放,则32位整数最多需要占用43亿个位.约512MB的内存空间. 可以采用前一章的位处理方法.然后判断每个in

集体智慧编程_第二章(提供推荐)_1

前言:最近正在拜读Toby Segaran先生写的集体智慧编程,首先感谢Toby Segaran先生将知识以书本的方式传播给大家,同时也感谢莫映和王开福先生对此书的翻译,谢谢各位的不辞辛苦.首先在写随笔之前,跟各位分享一下我的编程环境:win7系统,python版本是2.7.10,开发环境我选择的是pycharm程序.本书的第一章为集体智慧导言,主要介绍的何为集体智慧和机器学习的相关概念和其局限性,以及与机器学习相关的例子和应用场景.下面开始机器学习第二章--提供推荐的相关内容. 本章主要内容:

java编程思想 第二章

这篇时间较之前篇章时间靠后,是由于,某一天晚上看完Java编程思想文献之后来不及做笔记了. 以下笔记基本为转载,不是原创 第二章   一切都是对象 目录: 2.1 用引用操纵对象 2.2 必须由你创建所有对象 2.3 永远不需要销毁对象 2.4 创建新的数据类型:类 2.5 方法.参数和返回值 2.6 构建一个Java程序 2.7 你的第一个Java程序 2.8 注释和嵌入式文档 2.9 编码风格 2.1 用引用操纵对象 一切都看作对象,操纵的标识符实际上是对象的一个“引用”,遥控器(引用)操纵

[书籍翻译] 《JavaScript并发编程》 第二章 JavaScript运行模型

本文是我翻译<JavaScript Concurrency>书籍的第二章 JavaScript运行模型,该书主要以Promises.Generator.Web workers等技术来讲解JavaScript并发编程方面的实践. 完整书籍翻译地址:https://github.com/yzsunlei/javascript_concurrency_translation .由于能力有限,肯定存在翻译不清楚甚至翻译错误的地方,欢迎朋友们提issue指出,感谢. 本书第一章我们探讨了JavaScri

对一千万条数据进行排序---编程珠玑第二版 第一章

本书第一章提出了一个看似简单的问题,有最多1000万条不同的整型数据存在于硬盘的文件中,如何在1M内存的情况下对其进行尽可能快的排序. 每个数字用4byte,1M即可存储250 000个数据,显然,只要每次对250 000个数据排序,写入到文件中即可,重复40次. 那么如何选出每次遍历的二十五万条数据呢?有如下两个策略: 1.对一千万条数据遍历40次,第i次遍历时,判断数是否属于[i*250000,i*250000+249999),如果是,则读入内存,当第i次遍历完成时,内 存中有了二十五万条数

编程珠玑第一章习题答案

习题 1.1      如果不缺内存,如何使用一个具有库的语言来实现一种排序算法? 因为C++有sort,JAVA也有,这里以C++为例给出,记住如果用set集合来排序时,是不可以有元素重复的 代码: #include <iostream> #include <cstring> #include <cmath> #include <queue> #include <stack> #include <list> #include <

java编程思想--第二章 一切都是对象

如果不做引申的话,本章没有多少可总结的内容.有些以前没有注意的细节可以写下来. 1.数据存储的位置 (1).寄存器.程序控制不了. (2).堆栈.对象的引用.基本类型. (3).堆.各种对象. (4).常量存储.和代码在一起. (5).非RAM存储.主要指流对象和持久化对象,前者准备网络传输,后者被丢到了硬盘上. 2.java中的数组会被自动初始化: (1).基本类型数组元素会被初始化为0 (2).引用类型数组元素会被初始化为null 3.变量作用域 (1).基本类型的跟C一样 (2).引用类型

Python核心编程_第二章课后习题

以下是自己在学习Python核心编程时,做的课后练习题.现在把它们贴出来,以记录自己的学习过程.小弟是机械出身,很多练习题目写的很是机械.虽然写出来的脚本都能满足题目要求,但效率可能不是最好的,所以,小弟还是厚着脸皮把它们给贴出来,一来可以让高手指点,二来可以与我一样在学习Python的兄弟共同学习. 以下的程序均以题目标号命名,如2-3这个题目,程序名就为2_3.py. 习题2_3.py #!/usr/bin/env python A = 10 B = 4 print "A plus B is

python核心编程2第二章课后练习

2-1 print来显示变量的内容,仅用变量名时,输出的字符串使用单引号括起来的,这是为了让非字符串对象也能以字符串的方式显示在屏幕上,print语句使用str()函数显示对象,交互解释器调用repr()函数来显示对象 2-2 (a)运算1+2*4 (b)只会做运算不会输出 (c)运算未显示结果 (d)交互解释器输入一段语句后会返回语句结果 (e)print ‘1+2*4’   2-3   2-4 (a) #!/usr/etc/env python string =raw_input("plea