腾讯笔试题:
有一千万条有重复的短信,这些短信以一行一条有重复的文本文件形式保存,请用5分钟找出重复出现最多的前10条。
思路有如下几种:
- shell解决(由于效率不清楚,不做考虑)
sort messages.txt |uniq -c |sort -k1 |tail -10- 用MD5处理一下,放入数据库中,然后
再利用select语句某些方法得出前10条短信。(但是,我们需要注意的是实际上数据库是满足不了5分钟解决这个条件的,因为即使1s录入1万条5min才300万条,而且录入之后还必须建立索引,所以也不可行)- 常规方法map先排序,在遍历一次,找出重复最多的前10条,排序的算法复杂度最低为nlgn.方法如下:建立一个红黑树a;遍历短信,对每条短信取MD5值,对每个MD5值在a中做操作:如果有值,这个key对应的值 就+1,否则就=1;遍历完后对红黑树取值最大的10个数,复杂度为10lg n;
hash_map
可以设计一个hash_table,hash_map<string,int>,依次读取一千万条短信,加载到hash_table表中,并且统计重复的次数,与此同时维护一张最多10条的短信表,这样遍历一次就能找出最多的前10条,算法复杂度为O(n)。(
hash_map的查找速度比map要快,因为hash_map的查找速度与数据量大小无关,属于常数级别。map的查找速度是log(n)级别。但是hash_map每次查找都需要执行hash函数,所以也比较耗时。而且,hash_map很多桶中可能都没有元素,所以内存利用率不高。
)
实现如下:
#include<iostream> #include<map> #include<iterator> #include<stdio.h> using namespace std;//宏定义 #include<ext/hash_map> #define HASH __gnu_cxx /*hash_map位于/usr/include/c++/4.0.0/ext 目录下在__gnu_cxx的名字空间中方便在mac或Linux下使用hash_map*/ #define uint32_t unsigned int #define uint64_t unsigned long int//数据类型重定义 struct StrHash { uint64_t operator()(const std::string& str) const { uint32_t b = 378551; uint32_t a = 63689; uint64_t hash = 0; for(size_t i = 0; i < str.size(); i++) { hash = hash * a + str[i]; a = a * b; } return hash; } uint64_t operator()(const std::string& str, uint32_t field) const { uint32_t b = 378551; uint32_t a = 63689; uint64_t hash = 0; for(size_t i = 0; i < str.size(); i++) { hash = hash * a + str[i]; a = a * b; } hash = (hash<<8)+field; return hash; } }; struct NameNum{ string name; int num; NameNum():num(0),name(""){} }; int main() { HASH::hash_map< string, int, StrHash > names; HASH::hash_map< string, int, StrHash >::iterator it; NameNum namenum[10]; string l = ""; while(getline(cin, l)) { it = names.find(l); if(it != names.end()) { names[l] ++; } else { names[l] = 1; names[l] = 1; } } int i = 0; int max = 1; int min = 1; int minpos = 0; for(it = names.begin(); it != names.end(); ++ it) { if(i < 10) { namenum[i].name = it->first; namenum[i].num = it->second; if(it->second > max) max = it->second; else if(it->second < min) { min = it->second; minpos = i; } } else { if(it->second > min) { namenum[minpos].name = it->first; namenum[minpos].num = it->second; int k = 1; min = namenum[0].num; minpos = 0; while(k < 10) { if(namenum[k].num < min) { min = namenum[k].num; minpos = k; } k ++; } } } i++; } i = 0; cout << "maxlength (string,num): " << endl; while( i < 10) { cout << "(" << namenum[i].name.c_str() << "," << namenum[i].num << ")" << endl; i++; } return 0; }
使用g++编译如下:
g++ main.cpp -o main
短信文本新建为:msg.txt
注意:msg.txt中应该写入内容例如张三 张三 张三 张三 李四 李四 李四 李四。。。。
运行:./main<msg.txt
可以采用内存映射的办法,首先1千万条短信按现在的短信长度将不会超过1G空间,使用内存映射文件比较合适。可以一次映射(当然如果更大的数据量的话,可以采用分段映射),由于不需要频繁使用文件I/O和频繁分配小内存,这将大大提高数据的加载速度。其次,对每条短信的第i(i从0到70)个字母按ASCII嘛进行分组,其实也就是创建树。i是树的深度,也是短信第i个字母。该问题主要是解决两方面的内容,一是内容加载,二是短信内容比较。采用文件内存映射技术可以解决内容加载的性能问题(不仅仅不需要调用文件I/O函数,而且也不需要每读出一条短信都分配一小块内存),而使用树技术可以有效减少比较的次数。 struct TNode { BYTE *pText; //直接指向文件映射的内存地址 DWORD dwCount; //计算器,记录此节点的相同短信数 TNode *ChildNodes[256]; //子节点数据,由于一个字母的ASCII值不可能超过256,所以子节点也不可能超过256 TNode() { //初始化成员 } ~TNode() { //释放资源 } }; //int nIndex是字母下标 void CreateChilsNode(TNode *pNode,const BYTE* pText,int nIndex) { if(pNode->ChildNodes[pText[nIndex]]==NULL) { //如果不存在此子节点,就创建.TNode构造函数应该有初始化代码 //为了处理方便,这里也可以在创建的同时把此节点加到一个数组中 pNode->ChildNodes[pText[nIndex]]=new TNode; } if(pText[nIndex+1]=='\0') { //此短信已完成,计数器加1,并保存此短信内容 pNode->ChildNodes[pText[nIndex]]->dwCount++; pNode->ChildNodes[pText[nIndex]]->pText=pText; } else //if(pText[nText]!='\0') { //如果还未结束,就创建下一级节点 CreateNode(pNode->ChildNodes[pText[nIndex]],pText,nText+1); } } //创建根节点,pTexts是短信数组,dwCount是短信数量(这里是1千万) void CreateRootNode(const BYTE **pTexts,DWOED dwCount) { TNode RootNode; for(DWORD dwIndex=0;dwIndex<dwCount;dwIndex++) { CreateNode(&RootN,pTexts[dwIndex],0); } //所有节点按dwCount的值进行排序 //取前10个节点,显示结果 }