本文的很多内容来自网络。如有错误。欢迎指出。
问题描写叙述
首先这里对单词的界定是:以空白切割的字符序列。单词统计的问题能够描写叙述为:在一篇正常格式的英文文档中(作为面试,这里并没有提及中文分词和单词统计的问题),统计每一个单词出现的次数。要求统计出现次数最多的N个单词和对应的出现次数。问题简单明了,无需对字面做很多其它的解释。
为什么面试官都喜欢考诸如此类的问题?
这类问题,大都有一个共同点:不仅要考虑数据的存储。并且要考虑排序的问题。更重要的是,普通方法能够解,偏偏面试官要找性能最好的、形式最优雅的。
怎么解?
细致研读一下题目,发现事实上仅仅有三件事情要做:1.单词次数统计 2. 排序。3.输出前N个,当中问题3仅仅是问题2的衍生。因此能够忽略。进一步分析发现。事实上这是典型的TOP K问题。
抛开TOP K问题的经典解决方式不提,我们先看看一步步来看问题的解决思路。
1. Map
C++ Standard Template Library中提供了一种高效的容器Map, 它是一种键值对容器(关联容器),因此能够轻松存储<单词, 次数>这种键值序列,因而,最简洁的解决方式能够是这种:
int main(void){ Map <String , int> wc; Map <String , int>::iterator j; String w; while( cin >> w ){ wc[ w ] ++; } for( j = wc.begin(); j != wc.end(); j++ ){ count << j->first << " : " j->second << "\n"; } return 0; }
因为Map的特性(数据插入时保证有序),因此输出的结果事实上是依照key(也就是单词)排好序的。
所以,还须要对输出的结果进行排序。
至于怎么排序,sort,qsort,还是自建heap sort,不再赘述。
这样已经非常好了,Map容器本身内建红黑树,使得插入的效率非常高。
可是,这样真的好吗?别忘了。面试官的要求更高。
2. Hashtable 神器,一键直达,效率再快一点
对于单词和单词次数的映射,除了使用Map外,还能够使用自己定义的hash表,hash表的节点应该包含三个主要的域:指向单词的指针或String,单词的出现次数int,指向下一个节点的指针node *。如果我们处理hash冲突的方案是链地址法。
则:
Hash表node的定义为:
typedef struct node{ char * word; int count; node * next; } node;
再次如果。处理的独立的单词个数不超过5w, 因此能够选择50021 这个质数作为hash表的大小。
而hash算法,我们能够选择经常使用的BKDR算法(以31为累乘因子),这样,能够将单词映射为一个unsigned int的整数:
#define HASH_SIZE 50021; #define MUL_FACTOR 31; node* mmap[HASH_SIZE]; unsigned int hashCode( char *p){ unsigned int result = 0; while(*p != '\0' ){ result = result * MUL_FACTOR + *p++; } return result % HASH_SIZE; }
函数wordCount( char *p )用于将单词和单词的出现次数插入hash表中,假设hash表中有对应的节点。则添加单词的次数并返回。否则须要加入新的节点到hash表的表头并初始化次数为1.
1 void wordCount( char *w ){ 2 unsigned int hashCode = hash( p ); 3 node * p; 4 5 while( p = mmap[hashCode];p!=NULL;p = p->next ){ 6 if(strcmp(w, p->word) == 0){ 7 (p->count)++; 8 return ; 9 } 10 } 11 12 node*q = (node*) malloc(sizeof(node)); 13 q->count = 1; 14 q->word = (char *) malloc(sizeof(w) + 1); 15 strcpy(q->word, w); 16 q->next = mmap[hashCode]; 17 mmap[hashCode] = q; 18 }
相同,hash表仅仅是提供了数据存储。并没有排序,排序的问题。还须要你来完毕。
这样似乎就完美了。
可素。伦家既不懂算法。又不会C++的容器。肿么办 ?
3. Shell版本号的单词统计
Linux提供了非常多文本操作的工具。比方uniq, tr ,sort。而管道的存在,恰到优点的攻克了文本和单词及中间处理结果须要存储的问题。最重要的一点,这些工具是一系列的黑盒,你仅仅须要知道怎样使用,而大不必去在乎内部实现的细节。
对于本题。用Linux 工具去处理,命令能够是:
cat word | tr –cs a-zA-Z\’ ‘\n’ | tr A-Z a-z | sort | uniq –c | sort –k1,1nr | head –n 10
对该程序每一行的解释:
- cat word 将word文件的内容读入缓存区,并作为下一个命令的输入
- tr –cs a-zA-Z\’ ‘\n’ 将非字母字符转换为换行符,保证一行一个单词
- tr A-Z a-z 将全部单词转为小写形式,保证and和And是同一个单词
- sort 按单词排序
- uniq –c 统计每一个单词的反复次数,也就相当于单词的出现次数
- sort –k1,1nr 依照出现次数排序逆序排序,-n指定数字比較。-r指定逆序排序
- head –n 10 输出出现次数最多的10个单词和它们的出现次数
写到这里。突然想起若干年前去百度面试的时候。一个技术T5的大拿问的问题,也就是这道单词统计的问题:一个文本中包括了非常多单词,每行一个单词,怎样统计每一个单词的次数?极其小白的我毫不含糊的回答:PHP脚本中每次读一行,用关联数组存单词和单词次数…….结果必定是慘烈的。
4. 文本处理的利器-AWK
既然提到了linux工具,那就不得不提一下awk(ɔk), awk是一个强大的文本分析处理工具。Wiki上如是说:
AWK是一种优良的文本处理工具,Linux及Unix环境中现有的功能最强大的数据处理引擎之中的一个。AWK提供了极其强大的功能:能够进行正則表達式的匹配,样式装入、流控制、数学运算符、进程控制语句甚至于内置的变量和函数。它具备了一个完整的语言所应具有的差点儿全部精美特性。
尽管仅是溢美之词。可是不得不承认awk确实非常强大。
awk -F ' |,' '{ for(i=1; i<=NF;i++){ a[tolower($i)]++; } } END{ for(i in a) print i, a[i] |"sort -k2,2nr"; }' word
gawk 3.1+中,能够使用内置函数asort和asorti对数组进行排序。只是排序的功能较弱。比如asort(a) ,若a是关联数组,asort的行为是仅仅对值排序,而键将被丢弃。取而代之的是新的1-n的数字索引。
这能够通过自己定义排序函数的方式或者结果通过管道传递给系统sort排序解决。
5. 数据库版本号的方案。
我们这里如果文本已经是一行一个单词。数据库表的基本结构为:
CREATE TABLE `test` ( `word` varchar(20) DEFAULT NULL ) ENGINE=MyISAM DEFAULT CHARSET=gbk;
单词入库(也能够load data in file):
awk ' BEGIN{ sql="insert into test(word) values "; mysql="mysql -hxxxxxx -uxxxxx -pxxx test -e "; } { for(i=1;i<=NF;i++){ sq="\"" sql "('\''" $i"'\'')" "\""; print mysql sq |"sh -x"; } }' word
那么简单的查询:
select word,count(word) as total from test group by word order by total desc;
就能够得到单词的次数。这样的方法非常easy,由于数据库替你完毕了全部统计和排序的工作,这也是为什么非常多人一旦有什么需求就求助于数据库。
基于Key-Value的缓存系统(Redis等)也能够完毕排序的功能,这里不再赘述。
思考
假设你是面试官。你看好哪一种解法?算法和数据结构的。还是shell的,awk的。
就我而言。我觉得面试是挑选人才的。而不是靠所谓的“奇技淫巧”去为难别人的。假设是我,看到有人用了shell的方式,或者awk的方式。我会给他高分。尽管算法是王道,但真的须要“一切从源代码做起”么?
參考文献:
1. http://www.cnblogs.com/ggjucheng/archive/2013/01/13/2858470.html
2.《shell脚本指南》
3.《编程珠玑》