### 问题描述
(来源于**《编程珠玑》第2版**的第2章第11页问题A)
Given a sequential file that contains at most four billion 32-bit integers in random order, find an integer that isn't in the file (and there must be at least one missing -- why?). How would you solve this problem with ample quantities of main memory ? How would you solve it if you could use several external "scratch" files but only a few hundreds bytes of main memories ? [译]给定一个包含40亿个随机排列的顺序文件,找到一个不在文件中的32位整数,在有足够内存的情况下应该如何解决该问题?如果有几个外部的临时文件可用,但是仅有几百字节的内存,又该如何解决?如有足够内存,完全可用第一章介绍的位图方法解决。这里关注内存不足时的解决方案。
### 基本思想
二分搜索(binary search)又称折半查找,它是一种效率较高的查找方法。二分查找要求:1)必须采用顺序存储结构; 2)必须按关键字大小有序排列。优缺点:折半查找法的优点是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入删除困难。 因此,折半查找方法适用于不经常变动而查找频繁的有序列表。充分利用了元素间的次序关系,采用分治策略,可在最坏的情况下用O(log n)完成搜索任务。
首先,将表中间位置记录的关键字与查找关键字比较, 如果两者相等,则查找成功; 否则利用中间位置记录将表分成前、后两个子表, 如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。将n个元素分成个数大致相同的两半,取a[n/2]与欲查找的x作比较,如果x=a[n/2]则找到x,算法终止。如果x<a[n/2],则我们只要在数组a的左半部继续搜索x(这里假设数组元素呈升序排列)。如果x>a[n/2],则我们只要在数组a的右半部继续搜索x。
二分搜索法的应用极其广泛,而且它的思想易于理解。第一个二分搜索算法早在1946 年就出现了,但是第一个完全正确的二分搜索算法直到1962年才出现。Bentley在他的著作《Writing Correct Programs》中写道,90%的计算机专家不能在2小时内写出完全正确的二分搜索算法。问题的关键在于准确地制定各次查找范围的边界以及终止条件的确定,正确地归纳奇偶数的各种情况,其实整理后可以发现它的具体算法是很直观的。
**分析**
对于32位的整数,可以表示的整数个数为2^32 = 4 294 967 296 > 4e9(40亿),因此有些数据不在该文件中。部分呼号约定:设存有40亿数据的文件为F,其中每个数据均为32bit,不在该文件中的数据集合为M(just think about the word missing for a while)。F 中的数据和M中的数据一起构成32比特能够表示的所有数据,即|F| + |M| = 2^32。从F中各数据的最高位比特开始,按其为0或者1分为两部分,假设分别为A、B,则A、B的大小有两种情况:
1) A和B中的数据个数相同。[由于|F| < 2^32,则|A| = |B| = |F|/2 < 2^31,]由于最初的数据文件F不包含数据M,则A、B均不包含M中的数据,即A、B中均缺失32比特能够表示的”部分数据“(即M中的数据),此时随便找一个文件,比如A,设定其为新的F,同时假设A中缺失的数据为新的M(也就是先前M中以0开头的数据,由于A 是以0开头的数据,事实上M当然是未知的),然后按照次高位比特进行划分。
2) A和B中的数据个数不同。假设A的个数多余B中的个数,即|A| > |B|,不能确定A中是否缺失数据,因为A完全可能包含以0开头的所有数据,这样A就不缺失数据。但是可以确定的是B一定缺失数据,否则|B| = 2^31,总数为40亿(小于2^32),导致A中的数据小于2^31,进而少于于B中的数据个数,与开始处的假设矛盾。令B为新的F,B中缺失的数据位新的M(即M中以1打头的数据,这也是由于B是以1开始的)。
通过上述最高位比特的分析后,可以得到一个文件F,其数据规模不超过N/2,其中N位F中的所有数据。同时可知有些数据不在文件F中,仅在M中。接着按第二位比特的信心对F进行上述的A、B分类,A是F中第二位比特为0的数据,B是F中第二位比特为1的数据。也能得出A、B中数据个数的信息。
1、若|A| = |B|,即A、B缺失数据,令F为A即可,继续。
2、若|A| != |B|,假设|A| > |B| ,则B缺失数据,令F为B,继续。
可以得到新的F其规模不超过N/4,继续A、B分类
### 示例模拟:
一大堆的文字描述不好理解,下面以一组4bit数字模拟一下上面的过程:4bit共可表示2^4 = 16个整数,假设初始文件F如下:
~~~
0 0000
1 0001
3 0011
4 0100
5 0101
6 0110
7 0111
8 1000
9 1001
11 1011
12 1100
13 1101
14 1110
15 1111
~~~
前面一列为10进制数,可以看到文件中缺失了0010(2),1010(10)。
### 第一次分解文件
A文件只包含0xxx的数:(x为未探索的位)
~~~
0 0000
1 0001
3 0011
4 0100
5 0101
6 0110
7 0111
~~~
B文件只包含1xxx的数:
~~~
8 1000
9 1001
11 1011
12 1100
13 1101
14 1110
15 1111
~~~
按照上面的分析,|A|=|B|=7 < 2^3 ,都有数据缺失,随机选取A文件为新的文件F。
### 第二次分解文件
A文件只包含00xx的数:
~~~
0 0000
1 0001
3 0011
~~~
B文件只包含01xx的数:
~~~
4 0100
5 0101
6 0110
7 0111
~~~
由于|A| < |B|,选取A为新的F文件。
### 第三次分解文件
A文件只包含000x的数据:
~~~
0 0000
1 0001
~~~
B文件只包含001x的数据:
~~~
3 0011
~~~
由于|B| < |A|,选取B为新的F文件。
### 第四次分解文件
A文件只包含0010的数据:没有数据。
B文件只包含0011的数据:0011
此时|A| < |B| ,所以缺失的数据必在A中,那么缺失的数据应该为0010,即十进制2。
### 代码
~~~
#include <stdio.h>
#include <stdlib.h>
#define MAX_BIT 32
void file_split(FILE *srcfd, FILE *bit0fd, FILE *bit1fd, unsigned int *bit0_count,
unsigned int *bit1_count, int cur_bit);
/* binary search(0/1 search). For simplicity, ignore error checking */
int main(int argc, char**argv){
unsigned int mdata = 0;
unsigned int bit0_count = 0;
unsigned int bit1_count = 0;
unsigned int missing_num = 0xFFFFFFFF;
char mdatafn[] = "mdata.txt";
char bit0fn[] = "bit0.txt";
char bit1fn[] = "bit1.txt";
FILE *mdatafn_ptr = NULL;
FILE *bit0_ptr = NULL;
FILE *bit1_ptr = NULL;
int cur_bit = MAX_BIT;
mdatafn_ptr = fopen(mdatafn, "w+");
printf("Please input one number or input EOF to stop input:\n");
while(scanf("%u", &mdata) != EOF){
fprintf(mdatafn_ptr, "%u\n", mdata);
printf("Please input one number or input EOF to stop input:\n");
}
fflush(mdatafn_ptr);
rewind(mdatafn_ptr);
bit0_ptr = fopen(bit0fn, "w+");
bit1_ptr = fopen(bit1fn, "w+");
while(cur_bit-- > 0){
bit0_count = 0;
bit1_count = 0;
file_split(mdatafn_ptr, bit0_ptr, bit1_ptr, &bit0_count, &bit1_count, cur_bit);
if(bit0_count <= bit1_count){ /* if |A| <= |B|, select A as new metadata */
printf("the missiong data's %d bit is 0.\n", cur_bit+1);
missing_num &= (~(1<<cur_bit));
mdatafn_ptr = fopen(bit0fn, "r+");
bit0_ptr = fopen(mdatafn, "w+");
bit1_ptr = fopen(bit1fn, "w+");
}
else{ /* if |B| < |A|, select B as new metadata */
printf("the missiong data's %d bit is 1.\n", cur_bit+1);
missing_num |= (1<<cur_bit);
mdatafn_ptr = fopen(bit1fn, "r+");
bit0_ptr = fopen(bit0fn, "w+");
bit1_ptr = fopen(mdatafn, "w+");
}
if(0 == bit0_count || 0 == bit1_count){
break;
}
}
fclose(mdatafn_ptr);
fclose(bit0_ptr);
fclose(bit1_ptr);
unlink(mdatafn);
unlink(bit0fn);
unlink(bit1fn);
printf("missing num is: %u\n", missing_num);
return 0;
}
/* get every unsigned int number from file pointed by srcfd,
* if it's single bit according cur_bit is 0, then write to file pointed
* by bit0fd or else bit1fd return every sub-file's number of unsigned int
*/
void file_split(FILE *srcfd, FILE *bit0fd, FILE *bit1fd, unsigned int *bit0_count,
unsigned int *bit1_count, int cur_bit){
char mdatastr[32] = {0,};
unsigned int mdata = 0;
if(NULL == srcfd || NULL == bit0fd || NULL == bit1fd
|| NULL == bit0_count || NULL == bit1_count){
printf("invalid input parameter\n");
exit(-1);
}
while(fgets(mdatastr, 32, srcfd)){
mdata = strtoul(mdatastr, NULL, 10);
if(mdata & 1<<cur_bit){
printf("put %08X to bit_1.txt.\n", mdata);
fprintf(bit1fd, "%u\n", mdata);
(*bit1_count)++;
}
else{
printf("put %08X to bit_0.txt.\n", mdata);
fprintf(bit0fd, "%u\n", mdata);
(*bit0_count)++;
}
memset(mdatastr, 0, sizeof(mdatastr));
}
fclose(srcfd);
fclose(bit0fd);
fclose(bit1fd);
}
~~~
敬请关注本博客和新浪微博[songzi_tea](http://weibo.com/songzitea).
- 前言
- 螺旋矩阵、螺旋队列算法
- 程序算法艺术与实践:稀尔排序、冒泡排序和快速排序
- Josephu 问题:数组实现和链表实现
- 杨辉三角形算法
- 位图排序
- 堆排序的实现
- Juggling算法
- 【编程珠玑】排序与位向量
- 取样问题
- 变位词实现
- 随机顺序的随机整数
- 插入排序
- 二分搜索
- 产生不重复的随机数
- 约瑟夫环解法
- 快速排序
- 旋转交换或向量旋转
- 块变换(字符反转)
- 如何优化程序打印出小于100000的素数
- 基本的排序算法原理与实现
- 利用马尔可夫链生成随机文本
- 字典树,后缀树
- B-和B+树
- 程序算法艺术与实践引导
- 程序算法艺术与实践:基础知识之有关算法的基本概念
- 程序算法艺术与实践:经典排序算法之桶排序
- 程序算法艺术与实践:基础知识之函数的渐近的界
- 程序算法艺术与实践:递归策略之矩阵乘法问题
- 程序算法艺术与实践:递归策略之Fibonacci数列
- 程序算法艺术与实践:递归策略基本的思想
- 程序算法艺术与实践:经典排序算法之插入排序
- 程序算法艺术与实践:递归策略之递归,循环与迭代