A题是这样子的:
给定一个最多包含40亿个随机排列的32位整数的顺序文件,找出一个不在文件中的32位整数(在文件中至少缺失一个这样的数据——为什么?)。在具有足够内存的情况下,如何解决该问题?如果有几个外部的“临时”文件可用,但是仅有几个字节的内存,又该如何解决该问题?
因为2^32>40亿,所以,在文件中至少会缺少一个这样的数据。在有足够内存的情况下,我们可以使用上一章提到的位图数据结构来解决该问题。但利用位图数据结构来解决该问题需要利用到的内存为:40亿/8/1024/1024=476MB,所以当内存不足时是不能够用这种方法的。
前面一篇博文提到了可以用数列求和方法来求得缺失的数。但就是这种方法有一个要求就是,任一个数与其前一个数的差值是一个定值,即要求该数列是一个等差数列。所以,如果我们能够确定,这个需要找出缺失的数的顺序文件里边的数是一个等差数列,那我们就可以利用该方法简单求出缺失的数了,而且占用内存极少,效率当然也就更高。
作者对该问题的解决方法是利用“二分搜索”的方法。具体如下:
算法的第一趟(最多)读取40亿个输入整数,并把起始位为0的整数写入一个顺序文件,把起始位为1的整数写入另一个顺序文件。如下图所示。
这两个文件中,有一个文件最多包含20亿个整数,我们接下来将该文件用作当前输入并重复探测过程,但这次探测的是第2个位。如果原始的输入文件中包含n个元素,那么第1趟将读取n个整数,第2趟最多读取n/2个整数,第3趟最多读取n/4个整数,依此类推,所以总的运行时间正比于n。通过排序文件并扫描,我们也能够找到缺失整数,但是这样做会导致运行时间正比于nlogn。
下边我们就来看看,用“二分搜索”怎么解决这个问题:
1. 文件读写
首先我们来看怎么进行文件的读写。有一篇博文总结的很不错,值得参考。贴出具体代码如下:
1 #include <iostream> 2 #include <fstream> 3 #include <string> 4 #include <sstream> 5 #include <math.h> 6 using namespace std; 7 8 int main() 9 { 10 // *************************** Write File *************************** // 11 // Open file 12 ofstream wfile("file.txt", ios::out); 13 if (!wfile) 14 { 15 cout << "The file can not be opened!" << endl; 16 exit(1); 17 } 18 19 for (int i = 0; i < pow(2, 16); i++) 20 { 21 stringstream ss; 22 ss << i; 23 string str = ss.str(); 24 wfile << str; 25 wfile << endl; 26 } 27 28 // Close the file 29 wfile.close(); 30 31 // *************************** Read File *************************** // 32 // Open file 33 ifstream rfile("file.txt", ios::in); 34 if (!rfile) 35 { 36 cout << "The file can not be opened!" << endl; 37 exit(1); 38 } 39 40 string line; 41 int num; 42 while (getline(rfile, line)) 43 { 44 // cout << line << endl; 45 stringstream ss; 46 ss << line; 47 ss >> num; 48 cout << num << endl; 49 } 50 51 // Close the file 52 rfile.close(); 53 54 return 0; 55 }
2. 实际操作
生成40亿个数据实在是太庞大了,因此在这里我选择了生成0~65535个顺序数列,然后随意去掉一个数,检验程序能否找出缺失的数。
1 #include <iostream> 2 #include <fstream> 3 #include <string> 4 #include <sstream> 5 #include <vector> 6 #include <math.h> 7 using namespace std; 8 9 typedef unsigned int uint; 10 11 int main() 12 { 13 //// *************************** Write File *************************** // 14 //// Open file 15 //ofstream wfile("file.txt", ios::out); 16 //if (!wfile) 17 //{ 18 // cout << "The file can not be opened!" << endl; 19 // exit(1); 20 //} 21 22 //for (int i = 0; i < pow(2, 16); i++) 23 //{ 24 // stringstream ss; 25 // ss << i; 26 // string str = ss.str(); 27 // wfile << str; 28 // wfile << endl; 29 //} 30 31 //// Close the file 32 //wfile.close(); 33 34 // *************************** Read File *************************** // 35 // Open file 36 ifstream rfile("file.txt", ios::in); 37 if (!rfile) 38 { 39 cout << "The file can not be opened!" << endl; 40 exit(1); 41 } 42 43 vector<uint> right, left; 44 string line; 45 uint num; 46 while (getline(rfile, line)) 47 { 48 // cout << line << endl; 49 stringstream ss; 50 ss << line; 51 if (line != "") 52 { 53 ss >> num; 54 if (num & (1 << 15)) 55 right.push_back(num); 56 else 57 left.push_back(num); 58 } 59 60 } 61 // Close the file 62 rfile.close(); 63 64 uint count = 1; 65 uint miss = 0; 66 uint szLeft = 0; 67 uint szRight = 0; 68 while (count != 16) 69 { 70 vector<uint> temp; 71 szLeft = left.size(); 72 szRight = right.size(); 73 74 if (szLeft < szRight) 75 { 76 right.clear(); 77 for (uint i = 0; i < szLeft; i++) 78 { 79 if (left[i] & (1 << 15 - count)) 80 right.push_back(left[i]); 81 else 82 temp.push_back(left[i]); 83 } 84 left.clear(); 85 left = temp; 86 } 87 else 88 { 89 left.clear(); 90 for (uint i = 0; i < szRight; i++) 91 { 92 if (right[i] & (1 << 15 - count)) 93 temp.push_back(right[i]); 94 else 95 left.push_back(right[i]); 96 } 97 right.clear(); 98 right = temp; 99 } 100 101 count++; 102 } 103 104 szLeft = left.size(); 105 szRight = right.size(); 106 if (szLeft > szRight) 107 { 108 miss = left[0] + 1; 109 } 110 else if (szLeft < szRight) 111 { 112 miss = right[0] - 1; 113 } 114 else 115 { 116 // no elements is missed 117 miss = 65536; 118 } 119 120 cout << "The missed one is: " << miss << endl; 121 122 return 0; 123 }
注:该程序实际所占内存还是挺大的(针对存有40亿个数据的文件),主要是在前期刚从文件读入数据的时候。作者提到的是把这些中间数据写到文件中去的方法,但我在这里为了便于处理,就没有将中间数据写出然后再读进来。当然,我这样做只是权宜之计,要实际解决内存不足的话还是得将中间数据写出文件然后再读取。
通过不断地二分,因为缺少一个整数,所以程序中left和right的大小至少一个为0,又因为,最后的left和right(都已经只有一个数)如果在没有缺失数的情况下,有left[0] = right[0] - 1,这样就很简单就能够判断出缺少的是哪一个数了。
当然程序是可以找出缺失的数的。因为只是验证一下算法,程序的健壮性是没有太多考虑的。
我们可以看到,“二分法”能让程序对时间和空间的需求呈指数级下降,效果是特别明显的。
注:后来又看到其他人的想法,此程序的效率还是可以继续提升的!
我们每次读入总数1/10的数据量的时候,就比较一下最后一个读入的数据D_last跟读入的数据量大小L_read:
- 如果D_last+1=L_read,则表明目前读入的数据中并没有缺失的数,也因此可以在后边将二分搜索起点设定为L_read;
- 如果D_last=L_read,则表明目前已经读入的数据就已经存在缺失的数了,也就是说不用再继续读入数据了。
3. 课后习题2
给定包含4 300 000 000个32位整数的顺序文件,如何找出一个出现至少两次的整数?
二分搜索通过递归搜索包含半数以上整数的子区间来查找至少出现两次的单词。因为4300000000 > 2^32,所以必定存在重复的整数,搜索范围从[0, 2^32)开始,中间值mid为2^31,若区间[0, 2^31)内的整数个数大于2^31个,则调整搜索区间为[0, 2^31),反之则调整搜索区间为[2^31, 2^32),然后再对整个文件再遍历一遍,直到得到最后的结果。
在这里我选择了生成0~65535个顺序数列,然后随意增加N个 重复的数,检验程序能否找出重复的数。程序如下:
1 #include <iostream> 2 #include <fstream> 3 #include <string> 4 #include <sstream> 5 #include <vector> 6 #include <math.h> 7 using namespace std; 8 9 typedef unsigned int uint; 10 11 int main() 12 { 13 //// *************************** Write File *************************** // 14 //// Open file 15 //ofstream wfile("file.txt", ios::out); 16 //if (!wfile) 17 //{ 18 // cout << "The file can not be opened!" << endl; 19 // exit(1); 20 //} 21 22 //for (int i = 0; i < pow(2, 16); i++) 23 //{ 24 // stringstream ss; 25 // ss << i; 26 // string str = ss.str(); 27 // wfile << str; 28 // wfile << endl; 29 //} 30 31 //// Close the file 32 //wfile.close(); 33 34 // *************************** Read File *************************** // 35 // Open file 36 ifstream rfile("file.txt", ios::in); 37 if (!rfile) 38 { 39 cout << "The file can not be opened!" << endl; 40 exit(1); 41 } 42 43 vector<uint> right, left; 44 string line; 45 uint num; 46 while (getline(rfile, line)) 47 { 48 // cout << line << endl; 49 stringstream ss; 50 ss << line; 51 if (line != "") 52 { 53 ss >> num; 54 if (num & (1 << 15)) 55 right.push_back(num); 56 else 57 left.push_back(num); 58 } 59 60 } 61 // Close the file 62 rfile.close(); 63 64 uint count = 1; 65 uint repeat = 0; 66 uint szLeft = 0; 67 uint szRight = 0; 68 while (count != 16) 69 { 70 vector<uint> temp; 71 szLeft = left.size(); 72 szRight = right.size(); 73 74 if (szLeft > szRight) 75 { 76 right.clear(); 77 for (uint i = 0; i < szLeft; i++) 78 { 79 if (left[i] & (1 << 15 - count)) 80 right.push_back(left[i]); 81 else 82 temp.push_back(left[i]); 83 } 84 left.clear(); 85 left = temp; 86 } 87 else 88 { 89 left.clear(); 90 for (uint i = 0; i < szRight; i++) 91 { 92 if (right[i] & (1 << 15 - count)) 93 temp.push_back(right[i]); 94 else 95 left.push_back(right[i]); 96 } 97 right.clear(); 98 right = temp; 99 } 100 101 count++; 102 } 103 104 szLeft = left.size(); 105 szRight = right.size(); 106 if (szLeft > szRight) 107 { 108 repeat = left[0]; 109 } 110 else if (szLeft < szRight) 111 { 112 repeat = right[0]; 113 } 114 else 115 { 116 // no elements is repeated 117 repeat = 65536; 118 } 119 120 cout << "The repeated one is: " << repeat << endl; 121 122 return 0; 123 }
通过不断地二分,程序中left和right的大小至少一个为1,另一个,因为有重复的数,所以大小必然大于1。又因为,最后的left和right在没有重复整数的情况下理应只有一个数,这样就很简单根据left和right的大小就能够判断出重复的是哪一个数了。
程序可以正常工作。
注:后来又看到其他人的想法,此程序的效率还是可以继续提升的!
我们每次读入总数1/10的数据量的时候,就比较一下最后一个读入的数据D_last跟读入的数据量大小L_read:
- 如果D_last+1=L_read,则表明目前读入的数据中并没有重复的数,也因此可以在后边将二分搜索起点设定为L_read;
- 如果D_last+1<L_read,则表明目前已经读入的数据就已经存在重复的数了,也就是说不用再继续读入数据了。