出现次数的TopK问题进阶

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011386173/article/details/82429320

【进阶题目】

设计并实现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方法所做的事情:

  1. 当加入字符串时,假设为str。首先在strNodeMap中查询str之前出现的词频,如果查不到,说明str为第一次出现,在strNodeMap中加入一条记录(key = str, value=(str,1))。如果可以查到,说明str之前出现过,此时需要把str的词频增加,假设之前出现过10次,那么查到的记录为(key = str, value=(str,10)),变更为(key = str, value=(str,11))。
  2. 建立或调整完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表。                                                                                                                                                                                 
  3. 过程结束。

     在加入新的字符串时,都可能会调整堆,而堆最大也仅是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;
	}
}

                                                                                                                                                                                   

猜你喜欢

转载自blog.csdn.net/u011386173/article/details/82429320
今日推荐