您的位置:首页 > 职场人生

一道面试题,内存受限的情况,如何在海量的数据中找到重复最多的

2014-04-25 13:58 681 查看
昨天,去面试,被问到一题,给你内存500K,一百万条查询数据,每条数据20B,如何寻找到最多的那一条,内存不受限制的话,可以使用hash,之后排序就搞定了,但是内存受限的话,还真没有想过这个问题,因此憋了十分钟左右,还是没有想到,最后放弃了。之后的面试。。。。。。哎,果断的跪了。
回到实验室后的第一件事情,叹气之余,最大的想法就是我要把这个给编写出来,不然还是提高不了自己。

首先说说思路,内存受到限制,一百万条记录不可能放到内存中的,因此必须要把这个大文件(后文称记录文件)想办法分割成多块,每块保持在内存上限之下,要充分的运用硬盘资源,不过这样速度肯定慢下来了。分割多块的方法也不少,从记录文件中先读到差不多的数量就新建一个文件写进去,之后再从记录文件读,之后再新建一个文件写进去,直到记录文件中的内容读完为止,这个方法固然可行,不过后面每条记录出现的次数就不好统计了。所以,我们可以想办法尽可能的把相同的记录写到一个文件中去,这样就便于后面统计。什么方法有这个功能呢,没错,你猜对了,就是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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐