软件工程课程的一个题目:写一个程序,分析一个文本文件中各个词出现的频率,并且把频率最高的10个词打印出来。文本文件大约是30KB~300KB大小。
首先说一下这边的具体的实现都是在linux上实现的。没有大型IDE的性能检测。其实30KB还不是瞬间的事情,基于语言和一些简单的策略。所以在后面可能会尝试考虑增加文件大小到G级,然后发生的东西。我只能是从简单的原理研究。至于调试我只能写个简单的shell来自己检测一下。嗯,就这样吧。能力还是有点小白,特别是看了v_JULY_v 的海量数据处理http://blog.csdn.net/v_july_v/article/details/6279498之后。
说回题目。首先这个题目的两个重点分别是分词和处理词语。第一个分词的话。高级一点的语言就可以用正则表达式来处理,像这样"\w+(?:[-']\w+)*|'|[-.(]+|\S\w*" 我也是从网上学习的。然后我比较侧重的就是处理词语。首先是分离词语后存储的数据结构选取方面。一个词语对应出现的次数,第一个想到的当然是数组。不过对于像java和c语言来说没有关联数组这种数据类型,所以只能利用hash table来做这个事情,毕竟查找确实是O(1)。后面也会说用像PHP写的关联数组版,因为PHP底层实现就是用的Hash table,还是很久之前大神告诉过我的一句话:语言只是工具,这些东西用PHP,Python或者Go实现起来更少代码,更快,不一定非得C/C++才能写出来。
先上c++版(因为我用STL方便点)
#include <string> #include <fstream> #include <iostream> #include <map> #include <ext/hash_map> #include <algorithm> #include <vector> using namespace std; using namespace __gnu_cxx; struct str_hash { size_t operator()(const string &s)const { return __stl_hash_string(s.c_str()); } } struct str_compare { int operator()(const string &a,const string &b)const { return(a==b); } } typedef hash_map<string,string,str_hash,str_compare>::value_type valType; typedef pair<string,int> PAIR; int CmpByValue(PAIR const & a,PAIR const & b){ return a.second > b.second; } int main(int argc,char **argv) { ifstream fin("file.txt"); string s; int num; map<string,int> Imap; while(fin >> s) { map<string,int>::iterator it=Imap.find(s); if(it == Imap.end()){ //cout << s << endl; Imap.insert(valType(s,1)); }else{ num = Imap[s]; Imap[s] = num+1; } } vector<PAIR> SortList(Imap.begin(),Imap.end()); sort(SortList.begin(),SortList.end(),CmpByValue); //cout << SortList[0]; for(int i = 0;i != SortList.size();i++){ cout << SortList[i].second << endl; } return 0; }
运行结果:
通过G++编译,编译HASH_MAP的时候需要using namespace __gnu_cxx哦。g++还得加上一个参数。忘了,不然会报错,不过可以运行。
其实上面的程序重点便是hash map的key value存储方式。当然以这种方式排序其实也是一种消耗,这里使用的将数据放到一个vector里面,利用vector容器的排序进行按value的排序。
然后我又敲了一下java版。首先先上代码:
package topk; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; public class topk { public static void main(String[] args){ try{ BufferedReader reader = new BufferedReader(new FileReader("F:/java/topk/src/file.txt")); StringBuffer buffer = new StringBuffer(); String line = null; while((line = reader.readLine()) != null){ buffer.append(line); } reader.close(); Pattern expression = Pattern.compile("[a-zA-Z]+"); String string = buffer.toString(); Matcher matcher = expression.matcher(string); Map<String,Integer> map = new HashMap<String,Integer>(); String word = ""; int times = 0; while(matcher.find()){ word = matcher.group(); if(map.containsKey(word)){ times = map.get(word); map.put(word, times+1); }else{ map.put(word, 1); } } List<Map.Entry<String,Integer>> infoIds = new ArrayList<Map.Entry<String,Integer>>(map.entrySet()); Collections.sort(infoIds,new Comparator<Map.Entry<String,Integer>>(){ public int compare(Map.Entry<String, Integer> o1,Map.Entry<String,Integer> o2){ return (o1.getValue().compareTo(o2.getValue())); } }); int last = infoIds.size()-1; for(int i= last;i > last-10;i--){ String id = infoIds.get(i).toString(); System.out.println(id); } }catch(FileNotFoundException e){ System.out.println("文件未找到"); }catch(IOException e){ System.out.println("du"); } } }
运行结果:
在写java中知道了BufferedReader 这个比较强大的文件输入流函数。其实就是把文件提取到缓冲区里面,然后依次从缓冲区里面读取。缓冲区又称为缓存,它是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。这中间就有一个问题:当一个文件超出缓冲区大小的时候,是如何运作的。这里就是对缓冲区概念的熟悉。因为我可能还没学过c++中有输入缓冲区的库。所以得自己了解有机会去实现一个缓冲区去解决部分这样的问题。
缓冲区 分为三种类型:全缓冲、行缓冲和不带缓冲。
1、全缓冲
在这种情况下,当填满标准I/O缓存后才进行实际I/O操作。全缓冲的典型代表是对磁盘文件的读写。
2、行缓冲
在这种情况下,当在输入和输出中遇到换行符时,执行真正的I/O操作。这时,我们输入的字符先存放在缓冲区,等按下回车键换行时才进行实际的I/O操作。典型代表是键盘输入数据。
3、不带缓冲
也就是不进行缓冲,标准出错情况stderr是典型代表,这使得出错信息可以直接尽快地显示出来。
缓冲区的刷新
下列情况会引发缓冲区的刷新:
1、缓冲区满时;
2、执行flush语句;
3、执行endl语句;
4、关闭文件。
可见,缓冲区满或关闭文件时都会刷新缓冲区,进行真正的I/O操作。另外,在C++中,我们可以使用flush函数来刷新缓冲区(执行I/O操作并清空缓冲区)。
当然使用当缓冲区满时再向下读文件刷新缓冲区的时候可能这个方法会是比较好的。这里我还得研究,后面应该会发文说说这方面的学习心得。
但是我发现当一个实在是太大的文件的时候,单纯使用这种做法的意义其实不大。因为大可以切割文件然后再进行hash,还是v_JULY_v 大神的方法。而且我有一个想法。就是分割文件之后可以多线程处理啊。。起线程来处理对应的一部分文章再合并起来。(YY有待实现,看过这方面的面试题)
然后,我们可以使用再高级一点的语言来实现这个题目:(PHP版)
$content = file_get_contents("file.txt"); $content = explode(" ",$content); //preg_match_all($pattern,$content,$matches); $list = array(); foreach($content as $row){ if(array_key_exists($row,$list)){ $list[$row]++; }else{ $list[$row] = 1; } } arsort($list);
运行结果:
看似很简单的代码,其实内里隐藏了很多有趣的东西。像PHP中的关联数组这种数据结构确实在开发的过程中会省很多事情。不过我们还是来研究一下他实现的一些原理性的东西。
HashTable是zend的核心数据结构,在PHP里面几乎并用来实现所有常见功能,我们知道的PHP数组即是其典型应用,此外,在zend内部,如函数符号表、全局变量等也都是基于hash table来实现。
PHP的hash table具有如下特点:
●支持典型的key->value查询
●可以当做数组使用
●添加、删除节点是O(1)复杂度
●key支持混合类型:同时存在关联数组合索引数组
●Value支持混合类型:array (“string”,2332)
●支持线性遍历:如foreach
从上面的描述中可以看到其实也是hash table这个强大的数据结构。不过Zend hash table实现了典型的hash表散列结构,同时通过附加一个双向链表,提供了正向、反向遍历数组的功能。这里也不展开讲,因为涉及到PHP存储变量的数据结构。从这里我才深深的感受到了其实语言的魅力真的很大。虽然说简单几行可以实现的功能变成c存储起来确实如此的麻烦。当然对于上层开发来说,这个的确是可以加快开发的速度。(扯远了)
其实我还从这个题目中想着去研究分析文本比较高效的shell啦。 awk '{split("'${b}'", array, " ");print array[1]}' 测试首选。
还有js版:
var file = require("fs"); file.readFile('file.txt','utf-8',function(err,data){ if(err){ return console.log(err); }else{ var arr = data.split(" "||","||"?"||"."); var ArrLen=arr.length; var object={}; for(var i=0;i<ArrLen;i++){ var val=arr[i]; if(val in object) object[val]++; else object[val] = 1; } var Arrsort=[]; for(i in object){ Arrsort.push(object[i]); } Arrsort.sort(function(n1,n2){ return n2-n1; }) } })
运行结果:
对于性能调试,只是写个小shell测试了一下。没有直接在代码里面加时间。因为代码运行在用户态之外的时间呢。其实是我没有用IDE里比较方便的调试。
完。