排序算法,堆算法实现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

posted @ 2020-01-12 19:09  回眸,境界  阅读(171)  评论(0编辑  收藏  举报