TopK问题是一个经典的算法问题,TopK可以拆分为2个词Top, K意思就是选出其中最Top的K个变量,Top的意思可以是值最大,也可以是其他的一些衡量条件。也许你会想,这不是很简单吗,比如选一组数字中最大的一组数字,做个冒泡排序,输出前K个就OK了啊,当然没有说错,但是前提条件错了,数据量是非常庞大的时候,也许就没有这么简单了,有的时候,对于单个变量的计数统计,就有可能遇到问题。比如说一个查询统计,最后我要1天之内查询频率最高的10个词,并输出他们。面对成千上万的查询记录,关关统计每个查询词的次数就需要想高效率的方法。OK,下面就从这个切人点开始TopK问题的研究。
比如一组查询记录a b c a,这里以空格隔开,代表4次查询,这里可以明显看出a 2次,b 1次,c 1次,你可以存到一个map中做统计。一个最笨的办法就是一个个暴力的去比较,如果已经存在进行计数加1,这也是我们直接会想到的解决办法。其实用暴力统计算法时,可以先排下序,可以提高效率的,原因自己分析下。下面是关键的部分了,这里推荐一种空间换时间的办法,用字符串哈希算法,做映射,你可以类比于BloomFilter算法的实现,然后最后再加入到map时,直接映射,取出值存入map即可。
统计计数的过程结束之后,就是真正的TopK问题了,首先要明确一点,数据是海量的情况,肯定是不能全部数据进行排序的,所以我们可以维护K个变量,先读入K分变量,并排好序,然后再次读入一个变量,调整一下这K个变量,直到读完最后最后一个变量,这是一种方法,还有一种相比于普通排序算法更高效的算法,就是用堆排序算法来解决这个问题。先对K个打乱的树进行初始化堆排序,后面读入每次查询数据进行一次堆调整。
完整代码,请点击此处,https://github.com/linyiqun/lyq-algorithms-lib/tree/master/TopK
一个是计数过程的实现代码StatisticTool.java:
package TopK; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; /** * 统计工具类 * * @author lyq * */ public class StatisticTool { // 哈希表存放查询词以及查询数 public static int[] countMap; // query查询文件地址 private String filePath; // 哈希表容量 private int mapCotainNum; // 查询词集 private ArrayList<String> queryWords; // 存放查询词计数键值对 private Map<String, Integer> query2Count; public StatisticTool(String filePath, int mapCotainNum) { this.filePath = filePath; this.mapCotainNum = mapCotainNum; //执行初始化操作 initOperation(); readDataFile(); } /** * 从文件中读取数据 */ private void readDataFile() { File file = new File(filePath); ArrayList<String> dataArray = new ArrayList<String>(); try { BufferedReader in = new BufferedReader(new FileReader(file)); String str; String[] array; while ((str = in.readLine()) != null) { array = str.split(" "); for(String s: array){ dataArray.add(s); } } in.close(); } catch (IOException e) { e.getStackTrace(); } queryWords = dataArray; } /** * 初始化操作,在每次进行统计操作前进行 */ public void initOperation() { this.countMap = new int[mapCotainNum]; this.query2Count = new HashMap<String, Integer>(); } /** * 对总查询词进行冒泡排序操作 */ public String[] sortQuerys() { int k; String str1; String str2; String temp; String[] tempWords; tempWords = new String[queryWords.size()]; queryWords.toArray(tempWords); // 通过冒泡排序对查询词进行排序 for (int i = 0; i < tempWords.length - 1; i++) { k = i; for (int j = i + 1; j < tempWords.length; j++) { str1 = tempWords[k]; str2 = tempWords[j]; if (str1.compareTo(str2) > 0) { k = j; } } if (k != i) { temp = tempWords[i]; tempWords[i] = tempWords[k]; tempWords[k] = temp; } } return tempWords; } /** * 通过外部排序的算法实现统计 */ public void statisticBySort() { int count; //最后的词是否相等 boolean isEndSame; //上一个词 String lastWord; String[] sortedWord; sortedWord = sortQuerys(); lastWord = sortedWord[0]; count = 0; isEndSame = false; this.query2Count.clear(); // 进行线性扫描统计 for (String w : sortedWord) { // 如果本次的词等于上次的词,则计数加1 if (w.equals(lastWord)) { count++; isEndSame = true; } else { // 将上次的词存入map query2Count.put(lastWord, count); //重置操作 lastWord = w; count = 1; isEndSame = false; } } //如果最后的词是相等的,则,统计解法存入 if(isEndSame){ query2Count.put(lastWord, count); } } /** * 用哈希表的方法进行查询词的统计计数 */ public void statisticByHash() { long pos; int count; count = 0; pos = -1; this.query2Count.clear(); for (String word : queryWords) { pos = HashTool.BKDRHash(word); pos %= mapCotainNum; if (countMap[(int) pos] != 0) { countMap[(int) pos]++; } else { //countMap中的数组默认值为0 countMap[(int) pos] = 1; } } // 将统计结果存入map中,供下个阶段使用 for (String word : queryWords) { pos = HashTool.BKDRHash(word); pos %= mapCotainNum; count = countMap[(int) pos]; // 直接存入map中 query2Count.put(word, count); } } /** * 获取计数图 * @return */ public Map<String, Integer> getQuery2Count() { return this.query2Count; } }
一个是TopK过程的实现代码SelectTool.java(堆排序算法可能比较难懂,最后拿纸笔演示):
package TopK; import java.util.ArrayList; import java.util.Collections; import java.util.Map; /** * 筛选出TopK的算法工具类 * * @author lyq * */ public class SelectTool { // 筛选的前K个值的K数值 private int k; // 计数统计图 private Map<String, Integer> countMap; // 筛选出的TopK的查询数据 private ArrayList<Query> queryList; public SelectTool(int k, Map<String, Integer> countMap) { this.k = k; this.countMap = countMap; } /** * 利用外部排序进行TopK的选举,维护K个变量 */ public void selectTopKBySort() { int index; int count; String queryWord; Query insertQuery; Query query; Query query2; index = 0; queryList = new ArrayList<>(); for (Map.Entry<String, Integer> entry : countMap.entrySet()) { index++; count = entry.getValue(); queryWord = entry.getKey(); insertQuery = new Query(count, queryWord); if (index < k) { queryList.add(insertQuery); } else if (index == k) { queryList.add(insertQuery); // 对查询结果进行初次排序 Collections.sort(queryList); } else if (index > k) { for (int i = 0; i < queryList.size() - 1; i++) { query = queryList.get(i); query2 = queryList.get(i + 1); // 寻找插入的位置,如果count值在前后query之间,则进行替换 if (query.count >= insertQuery.count && query2.count < insertQuery.count) { queryList.set(i + 1, insertQuery); break; } } } } outputTopKQuerys(); } /** * 通过堆排序算法进行TopK的筛选 */ public void selectTopKByMaxHeap() { int index; int count; String queryWord; Query insertQuery; index = 0; queryList = new ArrayList<>(); for (Map.Entry<String, Integer> entry : countMap.entrySet()) { index++; count = entry.getValue(); queryWord = entry.getKey(); insertQuery = new Query(count, queryWord); if (index < k) { queryList.add(insertQuery); } else if (index == k) { queryList.add(insertQuery); // 如果刚刚填满k个查询量,则进行初始堆排序 queryList = initMaxHeap(queryList); } else if (index > k) { // 插入一个新的查询值,并维护这个堆结构 adjustHeap(insertQuery, queryList); } } outputTopKQuerys(); } /** * 初始化个数为k的大顶堆 * * @param queryList * 返回排好序的新的堆 * @return */ private ArrayList<Query> initMaxHeap(ArrayList<Query> queryList) { // 第一个查询词 Query firstQuery; ArrayList<Query> newMaxHeap; newMaxHeap = new ArrayList<>(); for (int i = 0; i < k; i++) { adjustMinValueFromHeap(queryList); // 将第一个元素与最后一个元素互换 firstQuery = queryList.get(0); newMaxHeap.add(firstQuery); // 将第一个用无限小替代 queryList.set(0, new Query(-Integer.MAX_VALUE, null)); } return newMaxHeap; } /** * 选出当前堆中最小的元素,与最后一个位置的元素进行交换 * * @param queryList * 目前维护的大顶堆 */ private void adjustMinValueFromHeap(ArrayList<Query> queryList) { int currentIndex; int otherIndex; int leafIndex; Query temp; Query query; Query query2; Query parentQuery; // 计算叶子节点的最小下标号 leafIndex = k / 2; for (int i = leafIndex; i < k; i += 2) { currentIndex = i; // 如果当前判断还没有到根节点 while (currentIndex > 0) { query = queryList.get(currentIndex); // 判断节点是否为左子节点还是右子节点,再判断取哪侧的节点 if (currentIndex % 2 == 0) { otherIndex = currentIndex - 1; query2 = queryList.get(otherIndex); } else { otherIndex = currentIndex + 1; query2 = queryList.get(otherIndex); } // 赋值子节点下标 if (query.count < query2.count) { currentIndex = otherIndex; temp = query2; } else { temp = query; } parentQuery = queryList.get((currentIndex - 1) / 2); // 重新进行赋值操作 if (temp.count > parentQuery.count) { queryList.set((currentIndex - 1) / 2, temp); queryList.set(currentIndex, parentQuery); } // 比较操作向上回溯 currentIndex = (currentIndex - 1) / 2; } } } /** * 进行大顶堆的调整 * * @param insertQuery * 待插入的查询词 * @param queryList * 堆数据 */ public void adjustHeap(Query insertQuery, ArrayList<Query> queryList) { int currentIndex; int leftIndex; int rightIndex; Query query; Query leftQuery; Query rightQuery; currentIndex = 0; while (currentIndex < queryList.size()) { query = queryList.get(currentIndex); // 如果待插入的查询计数比当前大,则做替换 if (insertQuery.count > query.count) { queryList.set(currentIndex, insertQuery); break; } else { leftIndex = 2 * (currentIndex + 1) - 1; rightIndex = 2 * (currentIndex + 1); leftQuery = queryList.get(leftIndex); rightQuery = queryList.get(rightIndex); // 选择一个计数值较小的做递归比较 if (leftQuery.count < rightQuery.count) { // 下标做变换 currentIndex = leftIndex; query = leftQuery; } else { // 下标做变换 currentIndex = rightIndex; query = rightQuery; } } } } /** * 输出TopK的统计结果 */ private void outputTopKQuerys() { int i = 0; for (Query q : queryList) { System.out.println("Top " + (i+1) + ":" + q.word + ":计数" + q.count); i++; } } }
输入
my name is is is lin yi yi qun qun a a a a b
输出
普通排序算法实现TopK Top 1:a:计数4 Top 2:is:计数3 Top 3:qun:计数2 Top 4:yi:计数2 Top 5:lin:计数1 Top 6:name:计数1 Top 7:my:计数1 堆排序算法实现TopK Top 1:a:计数4 Top 2:is:计数3 Top 3:qun:计数2 Top 4:lin:计数1 Top 5:b:计数1 Top 6:name:计数1 Top 7:yi:计数2
参考链接 http://blog.csdn.net/liyongbao1988/article/details/7397117