zoukankan      html  css  js  c++  java
  • 排序算法,堆算法实现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

  • 相关阅读:
    Filter&Listener
    jsp&mvc开发模式&jstl标签&三层架构
    Tomcat&Servlet
    xml
    SpringMVC
    io流-缓冲流
    io流-文件流节点流
    Io流
    JAVA中反射机制
    IDEA 将web项目部署到tomcat中运行
  • 原文地址:https://www.cnblogs.com/bianqi/p/12183918.html
Copyright © 2011-2022 走看看