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

来源:互联网 发布:张鲁农村淘宝网 编辑:程序博客网 时间:2024/06/11 21:55

昨天,去面试,被问到一题,给你内存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_queueclass 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







0 0
原创粉丝点击