这题做法其实不止一种。最直观的做法就是用heap,在java里就是PriorityQueue。用一个大小为k的最小堆来做,对每个数字出现的次数做统计,保留出现次数最大的k个即可。当然你放在PriorityQueue里面的不能是一个简单的Integer,因为你不止要知道次数的排序,你还得知道是什么数字对应的那个次数。所以你就需要override默认的comparator才行,你PriorityQueue里面可以放任意一个能表达键值对形式的数据结构,键是数组里面的数字,值是对应的出现次数,重写的comparator根据值来排序即可。这里我们试一下用Java 8新引进的Lambda driver(全金属狂潮的羊肉系统)。。。不对,Lambda表达式来写comparator。具体可以参见https://segmentfault.com/a/1190000009186509 之类的。。。代码如下:
public List<Integer> topKFrequent(int[] nums, int k) {
HashMap<Integer, Integer> countMap = new HashMap<>();
Queue<Integer[]> countQueue = new PriorityQueue<>((a, b) -> (a[1] - b[1]));
for (int i : nums) {
countMap.put(i, countMap.getOrDefault(i, 0) + 1);
}
for (Map.Entry<Integer, Integer> entry : countMap.entrySet()) {
Integer num = entry.getKey();
Integer count = entry.getValue();
if (countQueue.size() < k || countQueue.peek()[1] < count) {
Integer[] resultRow = {num, count};
countQueue.add(resultRow);
}
if (countQueue.size() > k) {
countQueue.poll();
}
}
LinkedList<Integer> result = new LinkedList<>();
while (!countQueue.isEmpty()) {
result.addFirst(countQueue.poll()[0]);
}
return result;
}
下面有一段类似的代码,但是我用另一个数据结构代替了PriorityQueue,我用的是TreeSet,但是整个流程,基本是类似的,除去一些细微的变化,我先给出代码:
public List<Integer> topKFrequent(int[] nums, int k) {
HashMap<Integer, Integer[]> countMap = new HashMap<>();
TreeSet<Integer[]> countSet = new TreeSet<>((a, b) -> (a[1] != b[1] ? a[1] - b[1] : a[0] - b[0]));
for (int num : nums) {
Integer[] numCnt = countMap.getOrDefault(num, new Integer[]{num, 0});
if (!countMap.containsKey(num)) {
countMap.put(num, numCnt);
}
if (countSet.contains(numCnt)) {
countSet.remove(numCnt);
}
numCnt[1]++;
countSet.add(numCnt);
if (countSet.size() > k) {
countSet.pollFirst();
}
}
LinkedList<Integer> result = new LinkedList<>();
for (Integer[] numCnt : countSet) {
result.addFirst(numCnt[0]);
}
return result;
}
可以看得出来,第一个区别就是这个做法,我只用了一个for loop来完成的。主要目的,是为了解决这一题的延伸问题(来自dropbox貌似),如果问题是,input是一个流和不是一个固定数组的话,我们能否得到答案。流和固定数组的区别在于就是,如果我时不时加一个新的数字,我能否很快的更新新的答案。能否处理流的关键就是是否能够即时更新用于排序的数据结构,这里用PriorityQueue的方式是没办法做到的,因为PriorityQueue没办法更新放进queue里面的数据。(其实heap是可以的,每次更新数据做一个heapify就好了。但是重新的完整的写一个heap又真的很麻烦的),这个时候TreeSet就可以达到这个效果了,你可以通过logN的操作来删除TreeSet中的一个节点已经插入TreeSet中的一个节点。所以其实对比priorityqueue的做法,我只需要多一个contains和remove的步骤就可以对流进行处理了。这里TreeSet就可以很好地满足我了。
这两题的做法的复杂度都能够满足leetcode的要求就是小于O(nlogn)。which is O(nlogk)。
其实还有更好的做法,参考了这个https://leetcode.com/problems/top-k-frequent-elements/discuss/81602/Java-O(n)-Solution-Bucket-Sort
看链接就知道了,算法核心在于桶排序。也不算是一个完整的桶排序,是一个不需要进行桶内排序的桶排序。桶的设计比较粗暴但有效,桶i就表示出现了i次的数字组合,所以最多n个桶。可以和heap做法一样,先用哈希表把每个数字进行出现次数的统计,然后根据这个统计,把数字放进对应的桶里。然后再从高顺位的桶往低顺位遍历结果,一旦解集达到了k,那么我们就返回结果。代码如下:
public List<Integer> topKFrequent(int[] nums, int k) {
LinkedList<Integer>[] numsList = new LinkedList[nums.length];
HashMap<Integer, Integer> countMap = new HashMap<>();
for (int num : nums) {
countMap.put(num, countMap.getOrDefault(num, 0) + 1);
}
for (Map.Entry<Integer, Integer> countPair : countMap.entrySet()) {
Integer num = countPair.getKey();
Integer count = countPair.getValue();
if (numsList[count - 1] == null) {
numsList[count - 1] = new LinkedList<>();
}
numsList[count - 1].add(num);
}
LinkedList<Integer> result = new LinkedList<>();
for (int i = numsList.length - 1; i >= 0; i--) {
List<Integer> counts = numsList[i];
if (counts != null) {
for (Integer num : counts) {
result.add(num);
if (result.size() == k) {
return result;
}
}
}
}
return result;
}
这个算法的复杂度显而易见就是O(n),虽然桶是n个,但元素总数也就n个,所以算上遍历空桶,复杂度也就只有O(n)。