《编程珠玑 》第二章 问题A 总结

时间:2023-01-11 23:11:44

     《编程珠玑》第二章问题A 是这样描述的:给定一个最多包含40亿个随机排列的32位整数的顺序文件,找出一个不在文件中的32位整数。

1.在具有足够内存的情况下,如何解决问题?

2.如何有几个外部“临时”文件可用,但是仅有几百字节的内存,又该如何解决该问题?

      对于第一个问题,在有足够内存情况下,我们可以采用位图法求解,32位的每个bite都对应一个整数需要使用4*10(9阶)/(8*10(6阶))~~500MB内存的BITMAP。便于讲解,将问题简单化给出数组{1,3,4,5,3,6,3,7,9,4,9,1,2,2,11,12,15,11,14,15}为4位整数使用位图法找出一个不在数组中的32位整数。具体代码如下:

#include <stdio.h>
#include <stdlib.h>
#define BITSPERWORD 4
#define SHIFT 2
#define MASK 0x3
#define length 20

int b[1 + length/BITSPERWORD];

void set(int i) {        b[i>>SHIFT] |=  (1<<(i & MASK)); }
int  find(int i){ return b[i>>SHIFT] &   (1<<(i & MASK)); }

void getLost(unsigned char *a)
{
     for(int j = 0; j <length; j++)
         set(a[j]);
     for(int i = 0; i < (1<<BITSPERWORD); i++)
         if (find(i)==0)
             printf("%d\n", i);
    
}
int main()
{	
    unsigned char a[] = {1,3,4,5,3,6,3,7,9,4,9,1,2,2,11,12,15,11,14,15};
    getLost(a);
    system("pause");
    return 1;
}

    

         对于第二个问题,把内存做了限制,这就不能再用位图方法了。我们可以考虑二分法(这里要摒弃固定思维,二分法并不是什么情况下都需要排序后才能使用)

       二分法思想是这样的:我们读取记录并从中取出一个数字,将高位为1,以及高位为0两类放到不同文件里,这个过程不需要多少工作内存,几十个byte足够。
我们假设高位为1的放入文件1中;为0的放入0中;
(1)那么当1中数据量和0一样多时,说明两者中都有未包含的数据,任取一个即可;
(2)如果1数据量和0数据量不同时,我们取数据量小的(可以使得接下来的二分需要处理的数据更少,数据量多的那个则有可能没有不存在的数);然后递归前面的步骤,最终会找到一个不存在的数字;
       那让我们把情况说得简单一些,假设一个文件里头有20个4bit的整数{1,3,4,5,3,6,3,7,9,4,9,1,2,2,11,12,15,11,14,15}那么找出遗漏的数字。我们一次取出一个数字,如果是最高位为1,那么放到一个文件1中,否则放到另外一个文件0中。理论上最高位为1的4bit数字不重复的共有2^3=8个,最高位为0的4bit数字同样为8个。若统计的个数少于8个肯定是这堆数中有不存在的数。而我们举的20个整数中有大量重复数字,而且导致最高位为1的数字个数可能大于8,但这不表示它其中没有缺少的数字。那到底如何去分辨哪个堆里头有缺少数字呢?在分拣到文件的过程中,程序有两个计数器,分别记录放入连个文件的数字的个数,缺少的数字肯定在较小个数的那个文件里头。如本例这20个整数中,最高位为1的数字共有{9,11,12,15,11,14,15}七个,而最高位为0的数字共有{1,3,4,5,3,6,3,7,4,1,2,2}十二个,我们就应该选择高位为1的那组数据继续二分法。同时我们还要设一个“标兵”,最为二分法的分界点,这时的分界点为8。

        对{9,11,12,15,11,14,15}继续二分法,第二高位为1的数字共有{12,15,15},第二高位为0的数字共有{9,11,11,14}。取较小数量的{12,15,15},这时“标兵”为8+4=12。

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

       对{12}继续二分法,第四高位为1的数字个数零个,第四高位为0的数字为{12}一个。“标兵”这时候为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;  
  
    while (bit--)  
    {  
        v = (1 << bit);  
        for (biter = citer = i = 0; i < alen; ++i)  
        {  
            if (a[i] & (1 << bit)) b[biter++] = a[i];  
            else c[citer++] = a[i];  
        }  
        if (biter <= citer)  
        {  
            re += v;  
            t = a; a = b; b = t;  
            alen = biter;  
        }  
        else  
        {  
            t = a; a = c; c = t;  
            alen = citer;  
        }  
    }  
    return re;  
}  
int main()
{
	unsigned char b[20] ={0};
	unsigned char c[20] ={0};
	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;
}
      

       但是,我想到一个问题,若我们仅是选择两个数堆中数字个数少的就认为是有缺项的,这是不正确的。可以构造出这样的例子{1,3,4,5,3,6,3,7,9,4,9,9,9,9,9,9,9,9,9,9,9,1,2,2,11,12,15,11,14,15},运行如上代码后获得的结果为“7”。显然是不正确的。当然我们可以对0,1获得的数字组去除重复进行比较,但这一就会损失很多内存。这里原题给出的是最多包含40亿个随机排列的32位整数的顺序文件,这里显然40亿小于2的32次方。而上头举例的20个4bit数字显然不符合原题,只是个例子而已就不纠结那么多了,等想出来方法在添加上。

       既然提到了重复数字我们再探讨下如何找到至少重复出现两次的数字。

       如题:给定一个4300000000个32位整数的顺序文件,请问你如何可以找到一个至少出现两次的整数。

       这里若假设所有整数都是无序排列,使用前头讲述的二分法就可以解决,只是这时候我们要去数字个数大的堆进行继续二分法,这里就不详细阐述啦。

       若假设这里的所有整数都是有序排列的,我们就可以省去那两个存储0,1bit数字堆的外部文件。具体代码如下:

#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
int getRepeat(unsigned char *a, int length, int bitlen)
{
	unsigned char result = 0;
	int start = 0;
	for(int i = 1; i <= bitlen; i++)
	{
		int bit0 = 0;
		int bit1 = 0;
		int mod = 1 << (bitlen - i);
		int len = length/(1<<(i-1));
		for(int 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 getLost(unsigned char *a, int length, int bitlen)
{
	unsigned char result = 0;
	int start = 0;
	for(int i = 1; i <= bitlen; i++)
	{
		int bit0 = 0;
		int bit1 = 0;
		int mod = 1 << (bitlen - i);
		int len = length/(1<<(i-1));
		for(int 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 a[] = {1,1,3,4,4,5,5,6,7,8,9,10,11,12,13,14,15};
	printf("%d\n",getRepeat(a,17,4));
	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;
}

            这里getRepeat()函数是对整数找出重复数来。getLost()函数式对整数找出缺失的数来。