【进阶题目】
设计并实现TopKRecord结构,可以不断地向其中加入字符串,并且可以根据字符串出现的情况随时打印加入次数最多前k个字符串,具体为:
1. k在TopKRecord实例生成时指定,并且不再变换(k是构造函数的参数);
2.含有add(String str)方法,即向TopKRecord中加入字符串。
3.含有printTopK()方法,即打印加入次数最多的前k个字符串,打印有哪些字符串和对应的次数即可,不要求严格按排名顺序打印。
【举例】
TopKRecord record = new TopKRecord(2);//打印Top2的结构
record.add("A");
record.printTopK();
此时打印:
TOP:
Str:A Times:1
record.add("B");
record.add("B");
record.printTopK();
此时打印:
TOP:
Str:A Times:1
Str :B Times:2
或者打印:
TOP:
Str :B Times:2
Str:A Times:1
record.add("C");
record.add("C");
record.printTopK();
此时打印:
TOP:
Str :B Times:2
Str :C Times:2
或者打印:
TOP:
Str :C Times:2
Str :B Times:2
【要求】
1.在任何时刻,add方法的时间复杂度不超过O(logk)。
2.在任何时刻,printTopK方法的时间复杂度不超过O(k)。
【难度】
☆☆☆
【解答】
原问题是已经存在不再变化的字符串数组,所以可以一次性统计词频哈希表,然后建小根堆。可是进阶问题中每个字符串词频可能会随时增加,这个过程一直是动态的。当然也可以在加入一个字符串时,在词频哈希表中增加这个字符串的词频,这样,add方法的时间复杂度就是O(1)。可是当有printTopK操作时,只能像原问题一样,根据所有字符串的词频来简历小根堆,假设此时哈希表的记录数为N,那么printTopK方法的时间复杂度就成了O(Nlogk),明显不达标。此处提供的解法依然是利用小根堆这个数据结构,但是在设计上更复杂。下面介绍TopKRecord的结构设计。
TopKRecord结构重要的4个部分如下:
- 依然有一个小根堆heap。小根堆里装的依然是原问题中Node类的实例,每个实例表示一个字符串及其词频统计的信息。小根堆里装的都是加入过的所有字符串中词频最高的Top K。heap的大小在初始化时就确定,是Node类型的数组结构,数组的总大小为k。
- 整型变量index。表示如果新的Node类的实例想加入到heap,该方在heap的哪个位置。
- 哈希表strNodeMap。key为字符串类型,表示加入的某种字符串。value为Node类型。strNodeMap上的每条信息表示一种字符串及其所对应的Node实例。
- 哈希表nodeIndexMap,key为Node类型,表示一种字符串及其词频信息。value为整型,表示key这个Node类的实例对应到heap上的位置,如果不在heap上,为-1。
关于strNodeMap和nodeIndexMap的说明如下:
比如,“A”这个字符串加入了10次,那么在strNodeMap表中就会有类似这样的记录(key=“A”,value=(“A”, 10)), value是一个Node类的实例。如果"A"加入的次数很多,使“A”成为加入的所有字符串中词频最高的Top K之一,那么“A”应该在堆上。假设“A”在堆上的位置为5,那么在nodeIndexMap表中就会有类似这样的记录(key = ("A", 10), value = 5)。如果“A”不在堆上,则在nodeIndexMap表中就会有这样的记录(key = ("A", 10), value = -1)。strNodeMap是字符串及其所对应的Node实例信息的哈希表,nodeIndexMap是字符串的Node实例信息对应在堆中(heap)位置的哈希表。
以下为加入一个字符串时,TopKRecord类中add方法所做的事情:
- 当加入字符串时,假设为str。首先在strNodeMap中查询str之前出现的词频,如果查不到,说明str为第一次出现,在strNodeMap中加入一条记录(key = str, value=(str,1))。如果可以查到,说明str之前出现过,此时需要把str的词频增加,假设之前出现过10次,那么查到的记录为(key = str, value=(str,10)),变更为(key = str, value=(str,11))。
- 建立或调整完str的Node实例信息后,需要考虑这个Node的实例信息是否已经在堆上,通过查询nodeIndexMap表可以得到Node实例对应的堆上的位置,如果没有或者查询结果为-1,表示不在堆上,否则表示在堆上,位置记为pos。
- )如果在堆上,说明str词频没增加之前就是Top K之一,现在词频既然增加了,就需要考虑调整str对应的的Node实例信息在堆中的位置,从pos位置开始向下调整小根堆即可(heapify)。特别注意:为了保证nodeIndexMap表中位置信息的始终准确,调整堆时,每一次两个堆元素(Node实例)之间的位置交换都要更新在nodeIndexMap表中的位置。比如,在堆上的一个Node实例("A", 10)原来在2位置,在nodeIndexMap表中的信息为(key = ("A", 10), value = 2)。现在又加入了一个“A”,词频增加,信息当然要变成(key = ("A", 11), value = 2).然后从位置2调整堆时,发现这个实例需要和自己的一个孩子实例 (“B”, 10)交换,假设这个Node实例的位置是6,即在nodeIndexMap表中记录为(key = ("B", 10), value = 6)。那么在彼此交换位置后,在heap数组中的两个实例当然很容易互换位置,但同时在nodeIndexMap上各自的信息也要变更,分别变更为(key = ("A", 11), value = 6),(key = ("B", 10), value = 2)。也就是说,任何Node实例在堆中的位置调整都要改相应的nodeIndexMap表信息,这也是整个TopKRecord结构设计中最关键的逻辑。
- )如果不在堆中,则看当前的小根堆是否已满(index ? = k)。如果没满(index < k),那么把str的Node实例放入堆底(heap的index位置),自然也要在nodeIndexMap表中加上位置信息。然后做堆在插入时的调整(heapInsert),同样,任何交换都要改nodeIndexMap表。如果已满(index==k), 则看str的词频是否大于小根堆堆顶的词频(heap[0]),如果不大于,则什么都不做。如果大于堆顶的词频,把str的Node实例设为新的堆顶,然后从位置0开始向下调整堆(heapify),同样,任何堆中的位置的变更都要改nodeIndexMap表。
- 过程结束。
在加入新的字符串时,都可能会调整堆,而堆最大也仅是k的大小,所以add方法时间复杂度为O(logK)。随时更新的小根堆就是每时每刻的Top K,打印时又没有排序的要求,所以printTopK方法直接依次实现打印小根堆数组即可,时间复杂度为O(K)。
TopKRecord类的全部实现请看如下代码:
public class Node {
public String str;
public int times;
public Node(String s, int t) {
str = s;
times = t;
}
}
public class TopKRecord {
private Node[] heap;
private int index;
private HashMap<String, Node> strNodeMap;
private HashMap<Node, Integer> nodeIndexMap;
public TopKRecord(int size) {
heap = new Node[size];
index = 0;
strNodeMap = new HashMap<String, Node>();
nodeIndexMap = new HashMap<Node, Integer>();
}
public void add(String str) {
Node curNode = null;
int preIndex = -1;
if (!strNodeMap.containsKey(str)) {
curNode = new Node(str, 1);
strNodeMap.put(str,Node);
nodeIndexMap.put(Node, -1);
} else {
curNode = strNodeMap.get(str);
curNode.times++;
preIndex = nodeIndexMap.get(curNode);
}
if (preIndex == -1) {
if (index == heap.length) {
if (heap[0].times < curNode.times) {
nodeIndexMap.put(heap[0], -1);
nodeIndexMap.put(curNode,0);
heap[0] = curNode;
heapify(0, index);
}
} else {
nodeIndexMap.put(curNode, index);
heap[index] = curNode;
heapInsert(index++);
}
} else {
heapify(preIndex, index);
}
}
public void printTopK() {
System.out.println("TOP: ");
for (int i = 0; i != heap.length; i++) {
if (heap[i] == null) {
break;
}
System.out.print("Str: " +heap[i].str);
System.out.print(" Times:" +heap[i].times);
}
}
private void heapInsert(int index) {
while(index != 0) {
int parent = (index - 1)/2;
if (heap[index].times < heap[parent].times) {
swap(parent, index);
index = parent;
} else {
break;
}
}
}
private void heapify(int index, int heapSize) {
int l = index * 2 + 1;
int r = index * 2 + 2;
int smallest = index;
while (l < heapSize) {
if (heap[l].times < heap[index].times) {
smallest = l;
}
if (r < heapSize && heap[r].times < heap[smallest].times) {
smallest = r;
}
if (smallest != index) {
swap(smallest, index);
} else {
break;
}
index = smallest;
l = index * 2 + 1;
r = index * 2 + 1;
}
}
private void swap(int index1, int index2) {
nodeIndexMap.put(heap[index1], index2);
nodeIndexMap.put(heap[index2], index1);
Node tmp = heap[index1];
heap[index1] = heap[index2];
heap[index2] = tmp;
}
}