当前位置:   article > 正文

用程序实现在大文件中出现次数为Top N的数字_一个大文件统计整数top

一个大文件统计整数top

有一个问题:有一个很大的文件(如20GB),内存装不下,其中存了很多个数字(也可能是URL之类的),找出出现次数最多的3个数字。

解题思路有这么3个点:

1. Top N的问题自然是用最小堆来解。不过如果只是找Top 3而已,也不用构造堆那么麻烦,直接几行比较代码应该就可以了。
2. 文件很大内存装不下,也就意味着不可能一次把文件整个读入内存再做处理。其实各个编程语言都有读大文件的方法,就是一行一行或一块一块地读,如C++有getline()函数,Python、Perl等都有类似的方法。

3. 形成一个Hash表,key是数字,value是该数字出现的次数。然后遍历整个Hash表,找到Top 3的数字。但是注意,这个Hash表的大小也可能会超出内存的限制。

假设数字的取值范围为0--2^31-1,那么可能的数字个数为21亿多(即2G),而每个数字占4个字节;假设所有的数字都出现了,并且每个数字只出现1次,然后再算上value的存储,那么占用的内存为: 2G * 4B * 2 =16GB,所以这个Hash表是有可能超出普通计算机的可用内存限制的。当然,如果数字个数不变而数字重复率高,自然Hash表就会小很多了。可是谁知道重复率怎样呢?如果是用生成随机数的库函数来生成这个大文件的,那么重复率还是比较低的。

在这种情况下,就要用到分而治之的思想了。如果把原来的大文件分成若干小文件,且每个小文件中出现的数字不会出现在任何其他的小文件中,那么只要统计每个小文件中出现次数的前3名,然后比较所有小文件的前3名,就能找到整个大文件中出现次数最高的前3名了。那么如何制作这样的小文件呢?对数字取模就可以了。

下面用程序来实现大文件的构造和问题的解答。

1. 我们需要创建一个很大的文件,它拥有1亿行,每行20个数字,即总共20亿的数字,即近2G个数字。

这里说一个与本题无关的数学问题。这个文件会有多大?

考虑到每个数字作为十进制保存在文件里的时候,占用的位数不一定是4位,即并不是4个字节,那么,平均每个数字在磁盘文件中占用几个字节呢?

首先,我们采用C语言的rand()来生成随机数,那么随机数的取值范围是0-RAND_MAX,这个RAND_MAX是在cstdlib中定义的一个值,就是2^31-1 = 2147483647, 然后采用加权平均的算法来计算:

  1. (1*10 + 2*90 + 3*900 + 4*9000 + 5*9e4 +
  2. 6*9e5 + 7*9e6 + 8*9e7 + 9*9e8 +
  3. 10*(2147483647-9e8))/2147483647
  4. = 9.95

这就是说,平均每个数字在磁盘文件中占据了差不多10个字节!那么近2G个数字,就会占据磁盘空间近20GB. 这个理论推测和实际观察也是一致的。下面就是生成这个大文件的C++程序:

  1. // gen_rand.cpp
  2. // g++ gen_rand.cpp -std=c++11 -o gen
  3. #include <iostream>
  4. #include <string>
  5. #include <sstream>
  6. #include <cstdlib>
  7. #include <ctime>
  8. #include <vector>
  9. #include <fstream>
  10. using namespace std;
  11. // For convenience, make all configurable variables Global
  12. const int total_rows = 2e8; // 1e8 is close to 100M
  13. const int max_rows_in_buf = 1e4; // 1e4 rows are about 1M bytes
  14. const int max_cols_in_buf = 20; // 20 * 1e8 = 2e9 numbers
  15. const string file = "./tmp.txt";
  16. void gen_rand_int(ofstream & fout)
  17. {
  18. srand((unsigned)time(NULL));
  19. int row_count = 0;
  20. string line;
  21. stringstream ss(line);
  22. while (row_count < total_rows) {
  23. for (int rows=0; rows < max_rows_in_buf; ++rows) {
  24. for(int cols = 0; cols < max_cols_in_buf; ++cols) {
  25. ss << rand() << " ";
  26. }
  27. ss << "\n";
  28. }
  29. // using stringstream as buffer is 14% faster than using fout directly
  30. fout << ss.str();
  31. ss.str(""); // clear string stream
  32. row_count += max_rows_in_buf;
  33. // cout << row_count << " rows has been generated" << endl;
  34. }
  35. }
  36. int main()
  37. {
  38. ofstream fout(file.c_str(), ios::out);
  39. if (fout.good()) {
  40. gen_rand_int(fout);
  41. }
  42. fout.close();
  43. return 0;
  44. }

总结一下:
a. 本人又写了一段Python代码,做同样的事情,只是为了对比一下效率。结果是,C++比Python快70倍左右。
b. 以上代码中使用了stringstream做了缓存,缓存为10000行,即每20万个数字写入文件一次,而不是每生成一行数字就写入文件。使用了缓存之后,速度快了约14%.
c. 代码运行总时间为 166秒左右。 
d. 如果要做并行化提高速度,可以多个线程写入多个文件,完了用 cat file1 file2 file3 > big_file 的方式合并。
        
2. 假设我们想把大文件分成40个小文件。这里需要保证的是,对于每个小文件所形成的Hash表,不会超过内存的限制。总共是2G个数字,如果分布比较平均的话,40个小文件,每个小文件包含50M个数字,那么占用内存大约为: 50M*4B*2 = 400MB ,但是假设分布不够平均的话,那么即使再double一下,也不过占用800MB,还是承受的住的。其实这里分成40个小文件还是有点武断了,也许应该做更多更深入的思考。只是鉴于我们在以上第一部分中,采用计算机伪随机算法来生成的随机数,还算是比较平均分布的,因此不会有什么问题。

因此,将大文件分成40个小文件的代码如下:

  1. // read_rand.cpp
  2. // g++ read_rand.cpp -std=c++11 -o rr
  3. #include <vector>
  4. #include <utility>
  5. #include <fstream>
  6. #include <sstream>
  7. #include <string>
  8. #include <iostream>
  9. using namespace std;
  10. // For convenience, make all configurable variables Global
  11. const int file_num = 10;
  12. const string infile_name = "./tmp.txt";
  13. const string out_file_prefix = "out_file_";
  14. // The buffer is critical to performance
  15. const int max_num_in_bufline = 1000;
  16. int main()
  17. {
  18. // Preapre OUT files
  19. fstream outfiles[file_num];
  20. vector<string> vec_outfile_names;
  21. vec_outfile_names.reserve(file_num);
  22. for (int i=0; i<file_num; ++i) {
  23. string tmp_str;
  24. stringstream tmp_ss(tmp_str);
  25. tmp_ss << out_file_prefix << i << ".txt";
  26. string file_name = tmp_ss.str();
  27. vec_outfile_names.push_back(file_name);
  28. outfiles[i].open(file_name.c_str(), ios::app);
  29. if (!outfiles[i].good()) {
  30. cout << "Cannot open " << i << " file\n";
  31. exit(-1);
  32. }
  33. }
  34. stringstream ss_array[file_num];
  35. // each line takes how many numbers (less than line_num_in_buf), will be initialized as 0
  36. vector<int> buf_line_count(file_num);
  37. // Open IN file
  38. fstream infile(infile_name, ios::in);
  39. if (!infile.good()) {
  40. cout << "Failed to open " << infile_name << endl;
  41. exit(-1);
  42. }
  43. // Reading and then Writing
  44. string line;
  45. int count = 0;
  46. while (!infile.eof()) {
  47. count ++;
  48. getline(infile, line);
  49. stringstream ss(line);
  50. int tmp_int;
  51. while (ss >> tmp_int) {
  52. int id = tmp_int % file_num;
  53. buf_line_count[id] ++;
  54. ss_array[id] << tmp_int << " ";
  55. if (buf_line_count[id] == max_num_in_bufline) {
  56. outfiles[id] << ss_array[id].str() << endl;
  57. // clear
  58. buf_line_count[id] = 0;
  59. ss_array[id].str("");
  60. }
  61. }
  62. }
  63. infile.close();
  64. // Write the last line (the number of figures in this line could be less than max_num_in_bufline)
  65. for (int i=0; i<file_num; ++i) {
  66. if (buf_line_count[i] > 0) {
  67. outfiles[i] << ss_array[i].str() << endl;
  68. }
  69. }
  70. for (int i=0; i<file_num; ++i) {
  71. outfiles[i].close();
  72. }
  73. return 0;
  74. }


  本步骤运行时间为 520秒左右。因为涉及单个文件的读写,这一步不太好做并行化,但也不是完全不能做。这里省略了。

 

3. 最后一步,读取每个小文件里的数字,对每个小文件都分别形成Hash表,然后找出每个小文件中出现次数排前3的数字;最后找出整个大文件的出现次数排名前3的数字。代码如下:

  1. // get_numbers.cpp
  2. // g++ get_numbers.cpp -std=c++11 -o gn
  3. #include <iostream>
  4. #include <fstream>
  5. #include <sstream>
  6. #include <string>
  7. #include <unordered_map>
  8. #include <vector>
  9. using namespace std;
  10. template <typename T>
  11. vector<pair<int, int> > get_max_3_numbers(const T& pairs)
  12. {
  13. vector<pair<int, int> > array;
  14. for (int i=0; i<3; ++i) {
  15. array.push_back({0,0});
  16. }
  17. for (auto item : pairs) {
  18. int index = -1;
  19. if (array[0].second <= array[1].second) {
  20. if (array[0].second <= array[2].second) {
  21. index = 0;
  22. } else {
  23. index = 2;
  24. }
  25. } else {
  26. if (array[1].second <= array[2].second) {
  27. index = 1;
  28. } else {
  29. index = 2;
  30. }
  31. }
  32. if (array[index].second < item.second) {
  33. array[index] = item;
  34. }
  35. }
  36. return array;
  37. }
  38. vector<pair<int, int> > get_numbers_from_one_file(const string& infile_name) {
  39. unordered_map<int, int> db;
  40. fstream infile(infile_name.c_str(), ios::in);
  41. if (!infile.good()) {
  42. cout << "Failed to read " << infile_name << endl;
  43. exit(-1);
  44. }
  45. string line;
  46. while(!infile.eof()) {
  47. getline(infile, line);
  48. stringstream ssline(line);
  49. int tmp_int;
  50. while (ssline >> tmp_int) {
  51. if (db.find(tmp_int) == db.end()) {
  52. db[tmp_int] = 1;
  53. }
  54. else {
  55. db[tmp_int] += 1;
  56. }
  57. }
  58. }
  59. infile.close();
  60. return get_max_3_numbers<unordered_map<int, int> >(db);
  61. }
  62. int main()
  63. {
  64. const int file_number = 40;
  65. vector<string> infilenames;
  66. for (int i=0; i<file_number; ++i) {
  67. stringstream ss(string(""));
  68. ss << "./out_file_" << i << ".txt";
  69. infilenames.push_back(ss.str());
  70. }
  71. vector<pair<int, int> > multi_file_result;
  72. for (auto infile_name : infilenames) {
  73. vector<pair<int, int> > result = get_numbers_from_one_file(infile_name);
  74. for (auto item : result) {
  75. multi_file_result.push_back(item);
  76. }
  77. cout << infile_name << " is done..." << endl;
  78. }
  79. vector<pair<int, int> > final_result = get_max_3_numbers<vector<pair<int, int> >>(multi_file_result);
  80. for (auto item : final_result) {
  81. cout << item.first << ": " << item.second << endl;
  82. }
  83. return 0;
  84. }

最终的结果如下:

  1. 1201971198: 12
  2. 542861170: 11
  3. 49242340: 11
  4. real 61m1.689s
  5. user 60m11.015s
  6. sys 0m48.395s

总结一下:
a. 使用了模板,确实需要,一次填的值是vector<pair<int, int> >类型,另一次填的是 unordered_map<int, int> 类型;
b. 这一步的执行时间很久,约1个小时,所以应该使用并行处理去做,后续会去实现。

用真正的程序来实现了一个以前只会纸上写写画画的面试题,大约算是做到了“Show me the code”了吧。不过,并没有结束,除了对优化的思考之外,至少还应该去实现并行化的处理。这留作不久将来的实现吧。

(未完待续)
 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/码创造者/article/detail/864113
推荐阅读
相关标签
  

闽ICP备14008679号