一道面试题,内存受限的情况,如何在海量的数据中找到重复最多的
2014-04-25 13:58
681 查看
昨天,去面试,被问到一题,给你内存500K,一百万条查询数据,每条数据20B,如何寻找到最多的那一条,内存不受限制的话,可以使用hash,之后排序就搞定了,但是内存受限的话,还真没有想过这个问题,因此憋了十分钟左右,还是没有想到,最后放弃了。之后的面试。。。。。。哎,果断的跪了。
回到实验室后的第一件事情,叹气之余,最大的想法就是我要把这个给编写出来,不然还是提高不了自己。
首先说说思路,内存受到限制,一百万条记录不可能放到内存中的,因此必须要把这个大文件(后文称记录文件)想办法分割成多块,每块保持在内存上限之下,要充分的运用硬盘资源,不过这样速度肯定慢下来了。分割多块的方法也不少,从记录文件中先读到差不多的数量就新建一个文件写进去,之后再从记录文件读,之后再新建一个文件写进去,直到记录文件中的内容读完为止,这个方法固然可行,不过后面每条记录出现的次数就不好统计了。所以,我们可以想办法尽可能的把相同的记录写到一个文件中去,这样就便于后面统计。什么方法有这个功能呢,没错,你猜对了,就是hash。
下面使用的就是Daniel J.Bernstein发明的算法,也是目前很有效的哈希函数。
有了hash以后,那如何来分割文件呢,假设我们想把记录文件分割成num个,因此我们可以从记录文件中读取记录,之后对每一条记录使用哈希算法,会计算出一个整数,我们让这个整数对num求余,这样就可以保证同样哈希值存放在同一个文件中。具体的代码如下:
完成上面的步骤之后,我们就可以把保存的结果按照字符串出现的频数,由高到低排列出来,我们可以使用大顶堆来排序,马上就想到使用STL中的priority_queue了。在使用priority_queue之前,我们先定义几个东西,后文会使用到。
现在我们开始使用大顶堆来排序,代码如下:
使用上面的方法对每个文件排序后,终于到最后一步了,这一步我们就可以进行总的归并了,对多个文件进行归并,还是使用堆吧,这玩意真好用,靠谱!
忘了,还有最后一步的。。。。包装一下嘛,起个名字--TopK :
void topK(int k, int filenum)
{
std::vector<std::string> vec;
std::unordered_map<std::string, int> map;
std::fstream fs[filenum];
std::string str;
SegmentFile("data.txt", filenum);
CaculateFrequence(filenum);
EveryFileDesc(filenum);
Mearge(k, filenum);
}
最最后,就是生成一个简单的字符串集合,本文使用简单的数字代替了,rand函数伪随机生成1000000个0到10000之间的数,保存到名为data.txt的文件中
补上main函数,里面就没有详细的去判断参数的正确与否啦,自己写个analysizeCmd就好啦:
我的操作都是在linux操作系统下搞的,
编译的话使用: g++ -std=c++11 -g -o demon demon.cpp
我不知道在linux如何去监控一个程序的最大内存使用量,也希望大家能指导我一下,所以只能使用top命令代替了,截图如下:
main函数里面加了一个while(1)无限循环得到的结果,分割的文件数是20个。
参考文章:
常见的hash函数 http://blog.csdn.net/mycomputerxiaomei/article/details/7641221
这个大家都知道的 http://www.cplusplus.com/reference
回到实验室后的第一件事情,叹气之余,最大的想法就是我要把这个给编写出来,不然还是提高不了自己。
首先说说思路,内存受到限制,一百万条记录不可能放到内存中的,因此必须要把这个大文件(后文称记录文件)想办法分割成多块,每块保持在内存上限之下,要充分的运用硬盘资源,不过这样速度肯定慢下来了。分割多块的方法也不少,从记录文件中先读到差不多的数量就新建一个文件写进去,之后再从记录文件读,之后再新建一个文件写进去,直到记录文件中的内容读完为止,这个方法固然可行,不过后面每条记录出现的次数就不好统计了。所以,我们可以想办法尽可能的把相同的记录写到一个文件中去,这样就便于后面统计。什么方法有这个功能呢,没错,你猜对了,就是hash。
下面使用的就是Daniel J.Bernstein发明的算法,也是目前很有效的哈希函数。
inline long DJBHash(std::string& str) { long hash = 5381; for(int i=0; i<str.size(); ++i) { hash = ((hash << 5) + hash) + str[i]; } return hash; }
有了hash以后,那如何来分割文件呢,假设我们想把记录文件分割成num个,因此我们可以从记录文件中读取记录,之后对每一条记录使用哈希算法,会计算出一个整数,我们让这个整数对num求余,这样就可以保证同样哈希值存放在同一个文件中。具体的代码如下:
/* * @path: 记录文件的完整路径和名称 * @filenum: 生成的文件个数 */ void SegmentFile(const char *path, int filenum) { long hash; std::string str; std::fstream fs, filearr[filenum]; fs.open(path, std::fstream::in); while(fs>>str) { //对读取的记录使用哈希算法 hash = DJBHash(str); hash = hash % filenum; if(!filearr[hash].is_open()) { //为了简便,新产生的文件的文件名直接使用数字表示 filearr[hash].open(std::to_string(hash).c_str(), std::fstream::out | std::fstream::in | std::fstream::trunc); if(!filearr[hash].is_open()) { std::cout<<"Open file "<<hash<<" fail."<<std::endl; continue; } } //保存记录到每个相应的文件中 filearr[hash]<<str<<'\n'; } for(int i=0; i<filenum; ++i) { if(filearr[i].is_open()) { filearr[i].close(); } } fs.close(); }假设我们要分割成40个文件,那么经过上述函数后,一般会在程序的目录下生成40个文件,文件名为0到39。通过这种方式可以保证每个文件中的字串在其他文件中是不存在的。之后,我们需要做的就是统计每个文件中相同字符串的个数,由于每个文件的大小都在内存上限之内(如果不在上限中,可以使用SegmentFile再分),因此我们可以将它一条一条读到内存中,之后使用STL中的unordered_map来记录每个字符串的频数。代码如下:
/* * @filenum: 生成的文件个数 */ void CaculateFrequence(int filenum) { std::string str; std::fstream fs[filenum]; std::unordered_map<std::string, int> map; for(int i=0; i<filenum; ++i) { map.clear(); fs[i].open(std::to_string(i).c_str(), std::fstream::in); if(fs[i].is_open()) { while(fs[i]>>str) { std::unordered_map<std::string, int>::iterator it = map.find(str); if(it == map.end()) { map[str] = 1; } else { it->second += 1; } } fs[i].close(); fs[i].open(std::to_string(i).c_str(), std::fstream::out | std::fstream::in | std::fstream::trunc); if(fs[i].is_open()) { for(std::unordered_map<std::string, int>::iterator it = map.begin(); it != map.end(); ++it) { fs[i]<<it->first<<" "<<it->second<<'\n'; } } } } for(int i=0; i<filenum; ++i) { if(fs[i].is_open()) { fs[i].close(); } } }
完成上面的步骤之后,我们就可以把保存的结果按照字符串出现的频数,由高到低排列出来,我们可以使用大顶堆来排序,马上就想到使用STL中的priority_queue了。在使用priority_queue之前,我们先定义几个东西,后文会使用到。
struct node { int len; //该字符串出现的次数 char data[21]; //保存每个记录字符串 std::fstream *pf; //该字符串保存在哪个文件流中 node* next; //下一个节点 node():len(0), pf(NULL), next(NULL) {} };
//仿函数,用于priority_queue class mycomparison { public: mycomparison(bool param = false) { reverse=param; } bool operator()(const node* left, const node* right) { if(reverse) return left->len > right->len; else return left->len < right->len; } private: bool reverse; };
// 这个queue太长了,直接使用typedef把它简化 typedef std::priority_queue<node *, std::vector<node*>, mycomparison> pq_t;
现在我们开始使用大顶堆来排序,代码如下:
/* * @filenum: 生成的文件个数 */ void EveryFileDesc(int filenum) { pq_t bigheap(mycomparison(false)); std::fstream fs[filenum]; for(int i=0; i<filenum; ++i) { node *head = new node; node *ptr = head; fs[i].open(std::to_string(i).c_str(), std::fstream::in); if(fs[i].is_open()) { //读取每条记录到大顶堆中 while(fs[i]>>ptr->data>>ptr->len) { bigheap.push(ptr); ptr->next = new node; ptr = ptr->next; } fs[i].close(); fs[i].open(std::to_string(i).c_str(), std::fstream::out | std::fstream::trunc); if(fs[i].is_open()) { //从大顶堆中取出数据,保存到文件中 while(!bigheap.empty()){ node* tmp = bigheap.top(); fs[i]<<tmp->data<<" "<<tmp->len<<'\n'; bigheap.pop(); delete tmp; } } } } for(int i=0; i<filenum; ++i) { if(fs[i].is_open()) { fs[i].close(); } } }
使用上面的方法对每个文件排序后,终于到最后一步了,这一步我们就可以进行总的归并了,对多个文件进行归并,还是使用堆吧,这玩意真好用,靠谱!
/* * @k: 前k个重复次数最多的 * @filenum: 生成的文件个数 */ void Mearge(int k, int filenum) { pq_t bigheap(mycomparison(false)); std::fstream fs[filenum], fd; node *head = new node; node *ptr = head; for(int i=0; i<filenum-1; ++i) { node* tmp = new node; ptr->next = tmp; ptr = ptr->next; } ptr = head; //先将每个文件的第一条记录读入堆中,堆中始终维持filenum个节点,保证可以不出现内存溢出 for(int i=0; i<filenum; ++i) { fs[i].open(std::to_string(i).c_str(), std::fstream::in); if(fs[i].is_open()) { fs[i]>>ptr->data>>ptr->len; ptr->pf = &fs[i]; bigheap.push(ptr); ptr = ptr->next; } } fd.open("result", std::fstream::out | std::fstream::trunc); if(fd.is_open()) { //从堆中读出数据,可以保证读出的值是依次减小的,结果打印出来并保存在result文件中 while(!bigheap.empty()) { if(k) { ptr = bigheap.top(); fd<<ptr->data<<" "<<ptr->len<<'\n'; std::cout<<ptr->data<<" "<<ptr->len<<std::endl;; bigheap.pop(); if(!(*(ptr->pf)).eof()) { (*(ptr->pf))>>ptr->data>>ptr->len; bigheap.push(ptr); --k; } } else { break; } } //释放空间 for(int i=0; i<filenum; ++i) { ptr = head; head = head->next; delete ptr; } } for(int i=0; i<filenum; ++i) { if(fs[i].is_open()) { fs[i].close(); } } fd.close(); }
忘了,还有最后一步的。。。。包装一下嘛,起个名字--TopK :
void topK(int k, int filenum)
{
std::vector<std::string> vec;
std::unordered_map<std::string, int> map;
std::fstream fs[filenum];
std::string str;
SegmentFile("data.txt", filenum);
CaculateFrequence(filenum);
EveryFileDesc(filenum);
Mearge(k, filenum);
}
最最后,就是生成一个简单的字符串集合,本文使用简单的数字代替了,rand函数伪随机生成1000000个0到10000之间的数,保存到名为data.txt的文件中
补上main函数,里面就没有详细的去判断参数的正确与否啦,自己写个analysizeCmd就好啦:
int main(int argv, char **argc) { if(argv != 4) { std::cout<<"Usage: demon path k filenum."<<std::endl; return 0; } topK(argc[1], std::stoi(std::string(argc[2])), std::stoi(std::string(argc[3]))); return 0; }
我的操作都是在linux操作系统下搞的,
编译的话使用: g++ -std=c++11 -g -o demon demon.cpp
我不知道在linux如何去监控一个程序的最大内存使用量,也希望大家能指导我一下,所以只能使用top命令代替了,截图如下:
main函数里面加了一个while(1)无限循环得到的结果,分割的文件数是20个。
参考文章:
常见的hash函数 http://blog.csdn.net/mycomputerxiaomei/article/details/7641221
这个大家都知道的 http://www.cplusplus.com/reference
相关文章推荐
- 如何在有限的内存的情况下,找到大量数据中重复查询次数最多的语句
- 内存有限的情况下 Spark 如何处理 T 级别的数据?
- 解决java读取大文件内存溢出问题、如何在不重复读取与不耗尽内存的情况下处理大文件
- 教你如何迅速秒杀掉:99%的海量数据处理面试题
- 一起做面试题--如何删除数据库表中的重复数据
- 一道面试题:如何防止异步请求的重复提交
- 解决java读取大文件内存溢出问题、如何在不重复读取与不耗尽内存的情况下处理大文件
- cisco交换机如何查看CPU和内存使用情况,以及如何查看接口数据量
- 内存有限的情况下 Spark 如何处理 T 级别的数据
- 如何找到一个数组里面重复次数最多的数
- 解决java读取大文件内存溢出问题、如何在不重复读取与不耗尽内存的情况下处理大文件
- 海量日志数据__怎么在海量数据中找出重复次数最多的一个
- Android突发情况(来电话,按Home键,内存不足)如何保存数据
- Hbase探究——如何避免一行数据过大导致加载至内存出现out of memory的情况
- 表中已存重复数据的情况,如何增加唯一性约束?
- 算法面试题,1-99的无序数组存在1对重复数如何最快找到重复的数
- 解决java读取大文件内存溢出问题、如何在不重复读取与不耗尽内存的情况下处理大文件
- 运用bitmap解决一道海量数据处理面试题:在2.5亿个整数中找出不重复的整数,注,内存不足以容纳这2.5亿个整数。
- cisco交换机如何查看CPU和内存使用情况,以及如何查看接口数据量
- 上千万或亿条数据量,如何统计出重复记录最多的前N条