作者:Grey

原文地址: 使用加强堆结构解决topK问题

题目描述

LintCode 550 · Top K Frequent Words II

思路

由于要统计每个字符串的次数,以及字典序,所以,我们需要把用户每次add的字符串封装成一个对象,这个对象中包括了这个字符串和这个字符串出现的次数。

假设我们封装的对象如下:

  1. public class Word {
  2. public String value; // 对应的字符串
  3. public int times; // 对应的字符串出现的次数
  4. public Word(String v, int t) {
  5. value = v;
  6. times = t;
  7. }
  8. }

topk的要求是: 出现次数多的排前面,如果次数一样,字典序小的排前面

很容易想到用有序表+比较器来做。

比较器的规则定义成和topk的要求一样,然后把元素元素加入使用比较器的有序表中,如果要返回topk,直接从这个有序表弹出返回给用户即可。比较器的定义如下:

  1. public class TopKComparator implements Comparator<Word> {
  2. @Override
  3. public int compare(Word o1, Word o2) {
  4. // 次数大的排前面,次数一样字典序在小的排前面
  5. return o1.times == o2.times ? o1.value.compareTo(o2.value) : (o2.times - o1.times);
  6. }
  7. }

有序表配置这个比较器即可

  1. TreeSet<Word> topK = new TreeSet<>(new TopKComparator());

所以topk()方法很简单,只需要从有序表里面把元素拿出来返回给用户即可

  1. public List<String> topk() {
  2. List<String> result = new ArrayList<>();
  3. for (Word word : topK) {
  4. result.add(word.value);
  5. }
  6. return result;
  7. }

时间复杂度 O(K)

以上步骤不复杂,接下来是add的逻辑,add的每次操作都有可能对前面我们设置的topK有序表造成影响,

所以在每次add操作的时候需要有一个机制可以告诉topK这个有序表,需要淘汰什么元素,需要新加哪个元素,让topK这个有序表时时刻刻只存topk个元素,

这样就可以确保topK()方法比较单纯,时间复杂度保持在O(K)

所以接下来的问题是:如何告诉topK这个有序表,需要淘汰什么元素,需要新加哪个元素?

我们可以通过堆来维持一个门槛,堆顶元素表示最先要淘汰的元素,所以堆中的比较策略定为:

次数从小到大,字典序从大到小,这样,堆顶元素永远是:次数相对更少或者字典序相对更大的那个元素。所以如果某个时刻要淘汰一个元素,从堆顶拿出来,然后再到topK这个有序表中查询是否有这个元素,有的话就从topK这个有序表中删除这个元素即可。

  1. private class ThresholdComparator implements Comparator<Word> {
  2. @Override
  3. public int compare(Word o1, Word o2) {
  4. // 设置堆门槛,堆顶元素最先被淘汰
  5. return o1.times == o2.times ? o2.value.compareTo(o2.value) : (o1.times - o2.times);
  6. }
  7. }

如果使用Java自带的PriorityQueue做这个堆,无法实现动态调整堆的功能,因为我们需要把次数增加的字符串(Word)在堆上动态调整,自带的PriorityQueue无法实现这个功能,PriorityQueue只能支持每次新增或者删除一个节点的时候,动态调整堆(
O(logN),但是如果堆中的节点变化了,PriorityQueue无法自动调整成堆结构,所以我们需要实现一个增强堆,用于节点变化的时候可以动态调整堆结构(保持O(logN)复杂度)。

加强堆的核心是增加了一个哈希表,

  1. private Map<Word, Integer> indexMap;

用于存放每个节点所在堆上的位置,在节点变化的时候,可以通过哈希表查出这个节点所在的位置,然后从所在位置进行heapify/heapInsert操作,且这两个操作只会走一个,
这样就动态调整好了这个堆结构,以下resign方法就是完成这个工作

  1. public void resign(Word word) {
  2. int i = indexMap.get(word);
  3. heapify(i);
  4. heapInsert(i);
  5. }

除了这个resign方法,自定义堆中的其他方法和常规的堆没有区别,在每次进行heapify和heapInsert操作的时候,如果涉及到交换两个元素,需要将indexMap中的两个元素的位置也互换

  1. private void swap(int i, int j) {
  2. if (i != j) {
  3. indexMap.put(words[i], j);
  4. indexMap.put(words[j], i);
  5. Word tmp = words[i];
  6. words[i] = words[j];
  7. words[j] = tmp;
  8. }
  9. }

由于自定义堆和有序表topk只存top k个数据,所以TopK结构中还需要一个哈希表来记录所有的字符串出现与否:

  1. private Map<String, Word> map;

自此,TopK结构中的add方法需要的前置条件已经具备,整个add方法的流程如下:

关于复杂度,add方法,时间复杂度O(log K), topk方法,时间复杂度O(K)

完整代码

  1. class TopK {
  2. private TreeSet<Word> topK;
  3. private Heap heap;
  4. private Map<String, Word> map;
  5. private int k;
  6. public TopK(int k) {
  7. this.k = k;
  8. topK = new TreeSet<>(new TopKComparator());
  9. heap = new Heap(k, new ThresholdComparator());
  10. map = new HashMap<>();
  11. }
  12. public void add(String str) {
  13. if (k == 0) {
  14. return;
  15. }
  16. Word word = map.get(str);
  17. if (word == null) {
  18. // 新增元素
  19. word = new Word(str, 1);
  20. // 是否到达门槛可以替换堆中元素
  21. if (heap.isReachThreshold(word)) {
  22. if (heap.isFull()) {
  23. Word toBeRemoved = heap.poll();
  24. topK.remove(toBeRemoved);
  25. }
  26. heap.add(word);
  27. topK.add(word);
  28. }
  29. } else {
  30. if (heap.contains(word)) {
  31. topK.remove(word);
  32. word.times++;
  33. topK.add(word);
  34. heap.resign(word);
  35. } else {
  36. word.times++;
  37. if (heap.isReachThreshold(word)) {
  38. if (heap.isFull()) {
  39. Word toBeRemoved = heap.poll();
  40. topK.remove(toBeRemoved);
  41. }
  42. heap.add(word);
  43. topK.add(word);
  44. }
  45. }
  46. }
  47. map.put(str, word);
  48. }
  49. public List<String> topk() {
  50. if (k == 0) {
  51. return new ArrayList<>();
  52. }
  53. List<String> result = new ArrayList<>();
  54. for (Word word : topK) {
  55. result.add(word.value);
  56. }
  57. return result;
  58. }
  59. private class Word {
  60. public String value;
  61. public int times;
  62. public Word(String v, int t) {
  63. value = v;
  64. times = t;
  65. }
  66. }
  67. private class TopKComparator implements Comparator<Word> {
  68. @Override
  69. public int compare(Word o1, Word o2) {
  70. // 次数大的排前面,次数一样字典序在小的排前面
  71. return o1.times == o2.times ? o1.value.compareTo(o2.value) : (o2.times - o1.times);
  72. }
  73. }
  74. private class ThresholdComparator implements Comparator<Word> {
  75. @Override
  76. public int compare(Word o1, Word o2) {
  77. // 设置堆门槛,堆顶元素最先被淘汰
  78. return o1.times == o2.times ? o2.value.compareTo(o1.value) : (o1.times - o2.times);
  79. }
  80. }
  81. private class Heap {
  82. private Word[] words;
  83. private Comparator<Word> comparator;
  84. private Map<Word, Integer> indexMap;
  85. public Heap(int k, Comparator<Word> comparator) {
  86. words = new Word[k];
  87. indexMap = new HashMap<>();
  88. this.comparator = comparator;
  89. }
  90. public boolean isEmpty() {
  91. return indexMap.isEmpty();
  92. }
  93. public boolean isFull() {
  94. return indexMap.size() == words.length;
  95. }
  96. public boolean isReachThreshold(Word word) {
  97. if (isEmpty() || indexMap.size() < words.length) {
  98. return true;
  99. } else {
  100. if (comparator.compare(words[0], word) < 0) {
  101. return true;
  102. }
  103. return false;
  104. }
  105. }
  106. public void add(Word word) {
  107. int size = indexMap.size();
  108. words[size] = word;
  109. indexMap.put(word, size);
  110. heapInsert(size);
  111. }
  112. private void heapify(int i) {
  113. int size = indexMap.size();
  114. int leftChildIndex = 2 * i + 1;
  115. while (leftChildIndex < size) {
  116. Word weakest = leftChildIndex + 1 < size
  117. ? (comparator.compare(words[leftChildIndex], words[leftChildIndex + 1]) < 0
  118. ? words[leftChildIndex]
  119. : words[leftChildIndex + 1])
  120. : words[leftChildIndex];
  121. if (comparator.compare(words[i], weakest) < 0) {
  122. break;
  123. }
  124. int weakestIndex = weakest == words[leftChildIndex] ? leftChildIndex : leftChildIndex + 1;
  125. swap(weakestIndex, i);
  126. i = weakestIndex;
  127. leftChildIndex = 2 * i + 1;
  128. }
  129. }
  130. public void resign(Word word) {
  131. int i = indexMap.get(word);
  132. heapify(i);
  133. heapInsert(i);
  134. }
  135. private void heapInsert(int i) {
  136. while (comparator.compare(words[i], words[(i - 1) / 2]) < 0) {
  137. swap(i, (i - 1) / 2);
  138. i = (i - 1) / 2;
  139. }
  140. }
  141. public boolean contains(Word word) {
  142. return indexMap.containsKey(word);
  143. }
  144. public Word poll() {
  145. Word result = words[0];
  146. swap(0, indexMap.size() - 1);
  147. indexMap.remove(result);
  148. heapify(0);
  149. return result;
  150. }
  151. private void swap(int i, int j) {
  152. if (i != j) {
  153. indexMap.put(words[i], j);
  154. indexMap.put(words[j], i);
  155. Word tmp = words[i];
  156. words[i] = words[j];
  157. words[j] = tmp;
  158. }
  159. }
  160. }
  161. }

更多

算法和数据结构笔记

参考资料

算法和数据结构体系班-左程云

使用加强堆结构解决topK问题的更多相关文章

  1. 基于PriorityQueue(优先队列)解决TOP-K问题

    TOP-K问题是面试高频题目,即在海量数据中找出最大(或最小的前k个数据),隐含条件就是内存不够容纳所有数据,所以把数据一次性读入内存,排序,再取前k条结果是不现实的. 下面我们用简单的Java8代码 ...

  2. Java最小堆解决TopK问题

    TopK问题是指从大量数据(源数据)中获取最大(或最小)的K个数据. TopK问题是个很常见的问题:例如学校要从全校学生中找到成绩最高的500名学生,再例如某搜索引擎要统计每天的100条搜索次数最多的 ...

  3. Java解决TopK问题(使用集合和直接实现)

    在处理大量数据的时候,有时候往往需要找出Top前几的数据,这时候如果直接对数据进行排序,在处理海量数据的时候往往就是不可行的了,而且在排序最好的时间复杂度为nlogn,当n远大于需要获取到的数据的时候 ...

  4. 堆结构的优秀实现类----PriorityQueue优先队列

    之前的文章中,我们有介绍过动态数组ArrayList,双向队列LinkedList,键值对集合HashMap,树集TreeMap.他们都各自有各自的优点,ArrayList动态扩容,数组实现查询非常快 ...

  5. 如何解决TOP-K问题

    前言:最近在开发一个功能:动态展示的订单数量排名前10的城市,这是一个典型的Top-k问题,其中k=10,也就是说找到一个集合中的前10名.实际生活中Top-K的问题非常广泛,比如:微博热搜的前100 ...

  6. java实现堆结构

    一.前言 之前用java实现堆结构,一直用的优先队列,但是在实际的面试中,可能会要求用数组实现,所以还是用java老老实实的实现一遍堆结构吧. 二.概念 堆,有两种形式,一种是大根堆,另一种是小根堆. ...

  7. Libheap:一款用于分析Glibc堆结构的GDB调试工具

    Libheap是一个用于在Linux平台上分析glibc堆结构的GDB调试脚本,使用Python语言编写.         安装 Glibc安装 尽管Libheap不要求glibc使用GDB调试支持和 ...

  8. 分治思想--快速排序解决TopK问题

    ----前言 ​ 最近一直研究算法,上个星期刷leetcode遇到从两个数组中找TopK问题,因此写下此篇,在一个数组中如何利用快速排序解决TopK问题. 先理清一个逻辑解决TopK问题→快速排序→递 ...

  9. 【pwn】学pwn日记(堆结构学习)

    [pwn]学pwn日记(堆结构学习) 1.什么是堆? 堆是下图中绿色的部分,而它上面的橙色部分则是堆管理器 我们都知道栈的从高内存向低内存扩展的,而堆是相反的,它是由低内存向高内存扩展的 堆管理器的作 ...

随机推荐

  1. [SWPU2019] NETWORK

    [SWPU2019]Network(TTL隐写) 1.题目概述 2.解题过程 文档中的数字代表什么呢?会不会是RGB? 看了一下以前做过的题目,好像并不是 那是什么呢?百度告诉我这是TTL隐写,哇,长 ...

  2. 官宣 .NET 7 Preview 2

    今天,我们很高兴发布 .NET 7 预览版 2..NET 7 的第二个预览版包括对 RegEx 源生成器的增强.将 NativeAOT 从实验状态转移到运行时的进展,以及对"dotnet n ...

  3. kubernetes允许master调度

    1,让 Master 也当作 Node 使用 (1)如果想让 Pod 也能调度到在 Master(本样例即 localhost.localdomain)上,可以执行如下命令使其作为一个工作节点: 注意 ...

  4. Arcgis Server发布的带有透明度的地图服务,调用时不显示透明度问题

    问题: 在发布道路地图时候设置地图透明度为50% 使用arcgis API for js 中 ArcGISDynamicMapServiceLayer 调用该地图时,发现透明效果不实现 如下图: 解决 ...

  5. 内网渗透----Token 窃取与利用

    0x00 前言 在之前的文章<渗透技巧--程序的降权启动>介绍了使用 SelectMyParent 降权的方法,本质上是通过 token 窃取实现的.这一次将要对 token 窃取和利用做 ...

  6. count(*)这么慢,我该怎么办?

    1)计算一个表有多少行数用什么命令? select count(*) from t 2)count(*)底层是怎样实现的? 在MYISAM中,是把这个总行数存到磁盘中去的,要的时候直接去读就行,特别快 ...

  7. 变量 Java day 5

    Java 第五天的学习 变量 变量注意事项 变量的底层 ASCII编码表 1.什么是变量? 概念:变量及代数. 在Java中,变量分为两种:基本类型的变量和引用类型的变量 1>基本类型的变量:必 ...

  8. 有关电控制图软件EPLAN的安装,下面有破解版本2.7

    前段时间刚刚接触这一块,就安装个软件老是出问题,所以我通过自己的努力学会啦,来给正要学习EPLAN的同学发福利啦 15:07:48 安装包发放在百度网盘来自取呀  建议安装我勾选的这个哦 链接:htt ...

  9. 半吊子菜鸟学Web开发 -- PHP学习3-文件

    目录 1 PHP文件系统 1.1 PHP文件的读取 1.4 获得文件的大小 1.5 PHP写入文件 1.6 删除文件 1 PHP文件系统 1.1 PHP文件的读取 文件读取的函数是file_get_c ...

  10. Kafka学习(二)

    作者:程序员cxuan链接:https://www.zhihu.com/question/53331259/answer/1262483551来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非 ...