排序算法,堆算法实现TopK
TopK问题
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筛选
统计计数的过程结束之后,就是真正的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