"top k"问题的深入探讨
常常遇到这样的一个问题:在海量数据中找出出现频率最高的前K个数,或者从海量数据中找出最大的前K个数,这类问题通常称为“top K”问题,如:在搜索引擎中,统计搜索最热门的10个查询词;在歌曲库中统计下载率最高的前10首歌等等。
①: 本人初次学习软件工程,近来便遇到一个类似的问题,问题是关于统计一片文章中统计出现频率最高的前十个单词。其实刚拿到这个程序时,也觉得很容易:无非是文件的导入、单词的分类、以及排序算法。所以便开始编程(因为大学没有养成需求设计的习惯,更加注重编写代码),所以,编着编着便越遍越多,仔细看来不仅代码冗余,而且结构混乱。正如师所说,自己的代码不仅别人看不懂,自己几天后都不知道自己的某个类时期什么作用。于是我便开始着手分析程序(因为自己熟悉java,觉得java的封装做的好,其实论速度还是c语言比较快)。
First:理解这道题做什么的?
这道小程序做的是统计一篇文章出现频率最高的十个词,及相同的次的次数排序。
Second:理解这道题须解决哪些问题?
(1)文件的导入(其实很简单,一个BufferedReader便能搞定)。
(2)单词的读取,因为是一整篇文章,需要分割单词依据“,。?!”等这些进行分割。
(3)相同字数的统计。
(4)排序算法。
Third:功能的实现?
(1)文件的导入,依据java提供的基于字符型的文件的输入输出与缓冲流的应用(相信学过I/O的输入输出都应该会)
关键代码:
File file=new File(path); FileReader fileReader=new FileReader(file); //建立文件输入流 BufferedReader bufferedReader=new BufferedReader(fileReader); //建立缓冲输入流
(2)单词的提取,其实刚开始本来想利用数组分割,但是觉得麻烦,后来查阅资料,以前学过的java正则表达式,起到了关键作用。
public String[] split(String regex,int limit)
- 根据匹配给定的正则表达式来拆分此字符串。
此方法返回的数组包含此字符串的子字符串,每个子字符串都由另一个匹配给定表达式的子字符串终止,或者由此字符串末尾终止。数组中的子字符串按它们在此字符串中出现的顺序排列。如果表达式不匹配输入的任何部分,那么所得数组只具有一个元素,即此字符串。
关键代码:
String []splittStr=str.split("[\\s,.;!?]"); //利用java正则表达式实现单词的分离
(3)相同字数的统计:因为学过Hashmap集合类,想到了Hashmap中有个方法:
containsKey(Object key)
如果此映射包含对于指定键的映射关系,则返回 true。故可以利用此方法进行相同归类:关键代码:
while((str=bufferedReader.readLine())!=null) { String []splittStr=str.split("[\\s,.;!?]"); //利用java正则表达式实现单词的分离 for(int i=0;i<splittStr.length;i++) { if(!splittStr[i].equals("")) { splittStr[i]=splittStr[i].toLowerCase(); //利用hashmap的containsKey判断是否包含键的映射关系 if(!hashMap.containsKey(splittStr[i])) { hashMap.put(splittStr[i], 1); } else { hashMap.put(splittStr[i], Integer.parseInt(""+hashMap.get(splittStr[i]))+1); } } } }
(4)排序算法:将Map转化位List,利用sort(List<T> list, Comparator<? super T> c)
根据指定比较器产生的顺序对指定列表进行排序。
关键代码:
List<Map.Entry<String,Integer>> list_Data = new ArrayList<Map.Entry<String, Integer>>(hashMap.entrySet()); Collections.sort(list_Data, new Comparator<Map.Entry<String, Integer>>() { public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) { if(o2.getValue()!=null&&o1.getValue()!=null&&o2.getValue().compareTo(o1.getValue())>0){ return 1; }else{ return -1; } } });
Forth:结果测试:
②:公司解决如此问题:
针对top k类问题,通常比较好的方案是【分治 + trie树/hash + 小顶堆】,即先将数据集按照hash方法分解成多个小数据集,然后使用trie树或者 hash统计每个小数据集中的query词频,之后用小顶堆求出每个数据集中出频率最高的前K个数,最后在所有top K中求出最终的top K。
实际上,最优的解决方案应该是最符合实际设计需求的方案,在实际应用中,可能有足够大的内存,那么直接将数据扔到内存中一次性处理即可,也可能机器有多个核,这样可以采用多线程处理整个数据集。
哈哈,到此本人第一次软件工程初步认识,希望大家多提意见多多进步。
附源代码:
package com.su.test; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileReader; import java.io.FileWriter; 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.Scanner; public class Test { //没有处理异常,将异常抛出 public static void main(String[] args) throws Exception{ // TODO Auto-generated method stub System.out.print("请输入所在文件的位置:"); Scanner scanner=new Scanner(System.in); //文本输入 String path=scanner.next(); File file=new File(path); FileReader fileReader=new FileReader(file); //建立文件输入流 BufferedReader bufferedReader=new BufferedReader(fileReader); //建立缓冲输入流 String str=null; HashMap<String,Integer> hashMap=new HashMap<String, Integer>(); //hashmap集合,利用键值:key-单词、value-出现次数 while((str=bufferedReader.readLine())!=null) { String []splittStr=str.split("[\\s,.;!?]"); //利用java正则表达式实现单词的分离 for(int i=0;i<splittStr.length;i++) { if(!splittStr[i].equals("")) { splittStr[i]=splittStr[i].toLowerCase(); //利用hashmap的containsKey判断是否包含键的映射关系 if(!hashMap.containsKey(splittStr[i])) { hashMap.put(splittStr[i], 1); } else { hashMap.put(splittStr[i], Integer.parseInt(""+hashMap.get(splittStr[i]))+1); } } } } /* * *对Hashmap进行排序(按value值进行排序) */ List<Map.Entry<String,Integer>> list_Data = new ArrayList<Map.Entry<String, Integer>>(hashMap.entrySet()); Collections.sort(list_Data, new Comparator<Map.Entry<String, Integer>>() { public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) { if(o2.getValue()!=null&&o1.getValue()!=null&&o2.getValue().compareTo(o1.getValue())>0){ return 1; }else{ return -1; } } }); int len=list_Data.size(); //获取list_Data的大小 for(int i=0;i<10;i++) { System.out.print(list_Data.get(i)+" "); } bufferedReader.close(); } }