假设我们希望系统能够在最后一小时内保留最多k个频繁的单词出现在推文中。如何设计呢?
我可以提出hashmap,heap,log或MapReduce,但我找不到一种非常有效的方法来做到这一点。
实际上这是一个采访中的问题
首先,我使用哈希图来计算每个单词的频率。我也保留了一个日志,所以随着时间的推移,我可以倒数最老的单词频率
然后我保留了一个长度为K(Top K数组)的入口数组和一个数字N,它是数组中最小的计数数。
每当有一个新单词出现时,我都会更新计数hashmap并获取这个新单词的计数。如果它大于N,我会发现这个单词是否在数组中。如果是,我更新数组中的条目。如果没有,我删除数组中的最小条目并将新单词插入其中。 (相应地更新N)
这是问题,我的方法无法处理删除。我可能需要迭代整个计数hashmap以找到新的顶部K. 此外,正如采访者所说,系统应该很快得到结果。我想几台机器一起工作,每台机器都需要一些文字。但是,如何组合结果也成为一个问题。
答案 0 :(得分:11)
如果单词没有加权(权重0和1除外),则可以使用O(N)辅助存储器导出一个简单的数据结构,该数据结构按顺序维护单词计数,其中N
是滑动窗口中遇到的唯一字的数量(在示例中为一小时)。所有操作(添加单词,过期单词,查找最常用的单词)都可以在O(1)
时间内执行。由于任何准确的解决方案都需要保留滑动窗口中的所有唯一单词,因此尽管每个单词的常数因子不小,但这种解决方案并非渐近变差。
解决方案的关键是任何给定单词的计数只能递增或递减1,并且所有计数都是整数。因此,可以维护计数的双向链表(按顺序),其中列表中的每个节点指向具有该计数的双链字列表。此外,单词列表中的每个节点都指向适当的计数节点。最后,我们维护一个hashmap,它允许我们找到与给定单词对应的节点。
最后,为了在生命结束时衰减单词,我们需要保留滑动窗口中的整个数据流,滑动窗口的大小为O(N')
,其中N'
是总数在滑动窗口期间遇到的单词。这可以存储为单链表,其中每个节点都有一个时间戳和一个指向单词列表中唯一单词的指针。
当遇到或过期某个单词时,需要调整其计数。由于计数只能递增或递减1,因此调整总是包括将单词移动到相邻的计数节点(可能存在也可能不存在);由于计数节点存储在已排序的链表中,因此可以在时间O(1)
中找到或创建相邻节点。此外,通过从最大值向后遍历计数列表,可以始终以恒定时间跟踪最流行的单词(和计数)。
如果不是很明显,这是一个在给定时间点的数据结构的粗糙的ascii艺术图:
Count list word lists (each node points back to the count node)
17 a <--> the <--> for
^
|
v
12 Wilbur <--> drawing
^
|
v
11 feature
现在,假设我们找到Wilbur
。那将把它的数量提高到13;我们可以从12
成功13
的事实看出,13
计数节点需要创建并插入到计数列表中。在我们这样做之后,我们从当前的单词列表中删除Wilbur
,将其放入与新计数节点关联的新创建的空单词列表中,并将Wilbur
中的计数指针更改为指向新的计数节点。
然后,假设drawing
的使用过期,那么它的新计数将为11.我们可以看到12
的前身是11
这一事实没有新计数需要创建节点;我们只需从其单词列表中删除drawing
并将其附加到与11
关联的单词列表,并在我们这样做时修复其计数指针。现在我们注意到与12
关联的单词列表为空,因此我们可以从计数列表中删除12
计数节点并将其删除。
当一个单词的计数达到0时,而不是将它附加到不存在的0
计数节点,我们只删除单词节点。如果遇到一个新单词,我们只需将该单词添加到1
计数节点,如果该节点不存在,则创建该计数节点。
在最坏的情况下,每个单词都有唯一的计数,因此计数列表的大小不能大于唯一单词的数量。此外,单词列表的总大小正是唯一单词的数量,因为每个单词都在一个单词列表中,完全过期的单词根本不会出现在单词列表中。
--- 编辑
这个算法有点RAM内存,但它确实不应该有任何麻烦持有一小时的推文。甚至一天的价值。几天之后,即使考虑缩写和拼写错误,唯一字的数量也不会有太大变化。即便如此,值得考虑减少内存占用和/或使算法并行的方法。
为了减少内存占用,最简单的方法就是删除几分钟后仍然唯一的单词。这将大大减少独特的字数,而不会改变流行词的数量。实际上,你可以在不改变最终结果的情况下进行更大的修剪。
要并行运行算法,可以使用哈希函数生成计算机编号,将单个单词分配给不同的计算机。 (不与用于构造哈希表的哈希函数相同。)然后,通过合并每台机器的顶部k
单词,可以找到顶部k
个单词;通过散列分配保证每台机器的单词集是不同的。
答案 1 :(得分:1)
这组问题称为数据流算法。在您的特定情况下,有两个适合 - “有损计数”和“粘性采样” 这是paper that explains them或this, with pictures。这是simplified introduction更多。
编辑(太长时间,以适应评论)
虽然这些流式算法不会对过期数据本身进行折扣,但可以运行60个滑动窗口,每小时一个,然后每分钟删除并创建一个新窗口。顶部的滑动窗口用于排队,其他仅用于更新。这给你1米的分辨率。
批评说,流式算法是概率性的,并不会给你准确的计数,而这是真的,请在这里与Rici的算法进行比较,一个控制错误频率,并且如果需要可以使其非常低。随着流的增长,您需要从流大小设置%,而不是绝对值。
流式算法非常节省内存,这是实时处理大型流时最重要的事情。与Rici的精确算法相比,后者需要单个主机将所有数据保存在当前滑动窗口的内存中。它可能无法很好地扩展 - 增加率100 / s - > 100k / s或时间窗口大小1h - > 7d,你将在一台主机上耗尽内存。作为Rici算法必不可少的部分的Hastables需要一个连续的记忆blob,随着它们的成长变得越来越成问题。
答案 2 :(得分:0)
这是一个非常有效的算法: -
- 首先使用字典而不是hashmap来存储字符串,因为它提供了更好的空间效率。
- 将字典中的索引映射到频率的hashmap。
- 然后维持一个最小堆来存储k个最常用单词的索引。
- 为每个单词添加指针,使其在堆中定位(如果不存在则为-1)。
- 如果更新了字频,那么检查它是否存在于堆中,然后使用指针在堆中使用其直接位置对其进行使用 与堆一起维护。
- 如果单词不存在且频率大于top,则删除top并插入单词并更新堆中单词的指针。
醇>
时间复杂度: -
更新top k: - O(logk)
以进行heapify,insert,delete
更新或搜索字词: O(|W|) where |W| is length of word
堆的空间复杂性: O(k)
字典空间,HashMap,堆指针: - O(N)
N
是总字数
答案 3 :(得分:-1)
您可以使用TreeMap,它基本上是一个排序的hashmap。在java中,您可以使TreeMap按降序列出它的条目(通过覆盖Comparable接口中的比较方法)。在这种情况下,指定时间段后的前k个条目将为您提供结果。
答案 4 :(得分:-1)
更新:
只需设计一个数据结构,高效支持add(String word)
、remove(String word)
、List<String> currentTopK(int k)
currentTopK
返回前k个常用词,如果有轮胎,按字母顺序
类似于设计LRU,这可以用双向链表+HashMap来完成
add()
和 remove()
时间:O(1);如果有一个层(多个单词与输入单词的计数相同)O(log m) 其中 m 是具有相同计数的单词数,因为我使用 TreeSet 来保持字母顺序。如果层不重要,我可以返回任何相同数量的订单,那么我不需要使用TreeSet,我们可以得到保证O(1)
currentTopK(int k)
时间 O(k)
public class TopK {
private class Node {
int count;
TreeSet<Item> itemSet = new TreeSet<>();
Node prev, next;
Node(int count){
this.count = count;
}
}
private class Item implements Comparable<Item> {
String word;
Node countNode;
Item(String w, Node node) {
word = w;
countNode = node;
}
@Override
public int compareTo(Item o) {
return this.word.compareTo(o.word);
}
}
private final Node head = new Node(0);
private Map<String, Item> countMap;
public TopK(){
head.next = head;
head.prev = head;
countMap = new HashMap<>();
}
public void add(String word) {
Item item = countMap.get(word);
Node countNode = item == null? null : item.countNode;
if (countNode == null) {
if (head.next.count == 1) {
countNode = head.next;
} else {
countNode = new Node(1);
insertNode(head, countNode);
}
item = new Item(word, countNode);
countMap.put(word, item);
} else {
Node oldCountNode = countNode;
if (oldCountNode.next.count == oldCountNode.count + 1) {
countNode = oldCountNode.next;
} else {
countNode = new Node(oldCountNode.count + 1);
insertNode(oldCountNode, countNode);
}
oldCountNode.itemSet.remove(item);
if (oldCountNode.itemSet.isEmpty()) removeNode(oldCountNode);
item.countNode = countNode;
}
countNode.itemSet.add(item);
}
public void remove(String word) {
Item item = countMap.get(word);
if (item == null) return;
Node countNode = item.countNode;
if (countNode.count == 1) {
countNode.itemSet.remove(item);
countMap.remove(word);
} else {
Node oldCountNode = countNode;
if (oldCountNode.prev.count == oldCountNode.count - 1) {
countNode = oldCountNode.prev;
} else {
countNode = new Node(oldCountNode.count - 1);
insertNode(oldCountNode.prev, countNode);
}
oldCountNode.itemSet.remove(item);
if (oldCountNode.itemSet.isEmpty()) removeNode(oldCountNode);
item.countNode = countNode;
countNode.itemSet.add(item);
}
}
public List<String> currentTopK(int k){
List<String> res = new ArrayList<>(k);
Node cur = head.prev;
while (cur != head) {
for (Item item : cur.itemSet){
res.add(item.word);
if (res.size() == k) return res;
}
cur = cur.prev;
}
return res;
}
private void insertNode(Node prev, Node cur) {
cur.next = prev.next;
prev.next.prev = cur;
prev.next = cur;
cur.prev = prev;
}
private void removeNode(Node cur) {
Node prev = cur.prev;
Node next = cur.next;
prev.next = next;
next.prev = prev;
cur.prev = null;
cur.next = null;
}
}
旧答案
使用HashMap + TreeSet 初始化 时间 O(1)
添加一个新词: 时间 O(logk)
optput 当前 Top K 时间 O(k)
处理长度为 N 的数据流: 时间复杂度:O(N log k) 空间复杂度:O(数据流中不同单词的数量)<= O(N)
import java.util.*;
public class TopK {
private final int k;
private Map<String, Integer> counts;
private TreeSet<String> topk;
private Comparator<String> comp;
public TopK(int k) {
this.k = k;
counts = new HashMap<>();
comp = (w1, w2) -> {
int c1 = counts.getOrDefault(w1, 0), c2 = counts.getOrDefault(w2, 0);
return c1 == c2 ? w2.compareTo(w1) : c1 < c2 ? -1 : 1;
};
topk = new TreeSet<>(comp);
}
public void add(String word) {
int newCount = counts.getOrDefault(word, 0) + 1;
if (topk.size() < k) {
topk.remove(word);
counts.put(word, newCount);
topk.add(word);
} else {
if (topk.remove(word)) {
counts.put(word, newCount);
topk.add(word);
} else {
counts.put(word, newCount);
if (comp.compare(word, topk.first()) > 0) {
topk.pollFirst();
topk.add(word);
}
}
}
}
public List<String> currentTopK() {
return new ArrayList<>(topk.descendingSet());
}
}