Topk问题是Java中经典的一类问题,在浅谈TopK部分已经介绍了一道经典的面试题:最小的K个数,主要是通过优先级队列来实现的。前面介绍过了元素的比较和Map和Set的基础用法,接下来看一下两道稍微复杂的TopK问题相关的题目。
前k个高频元素
1、描述: 给你一个整数数组 nums 和一个整数 k ,请返回其中出现频率前 k 高的元素。你可以按任意顺序返回答案。
2、思路:
取前K个频率最高的元素,立马想到用小堆(取大用小),但是这时候需要通过元素出现的频率来判断大小关系,所以需要使用Map集合保存一对元素,以及告诉JDK元素之间的大小关系。首先扫描整个nums数组,将每个元素和它出现的次数保存在Map接口中,然后扫描Map接口,将出现频次最高的前k个对象保存到优先级队列中,最后从对列中取出这几个对象的key值就是最终的结果。
3、代码
public class Num347_TopKFrequent {
public int[] topKFrequent(int[] nums, int k) {
//自定义局部内部类
class Freq{
int key;
int times;//出现频次
public Freq(int key, int times) {
this.key = key;
this.times = times;
}
}
//1、扫描这个数组,将数组元素和出现的频次保存在map接口中
Map<Integer,Integer> numTimes = new HashMap<>();
for(int i : nums){
// numTimes.put(i,numTimes.getOrDefault(i,0) + 1);
if(numTimes.containsKey(i)){
//如果此时key值在numtimes中已经存在
//新的频次就是原始的频次value + 1
//先将原始频次取出
int value = numTimes.get(i);
//再将value值加1塞回map中
numTimes.put(i,value + 1);
}else{
//此时key值在map中第一次出现
//将当前key对应的value置为1
numTimes.put(i,1);
}
}
//2、扫描Map接口。将出现频次最高的前k个Freq对象保存到优先级队列中
Queue<Freq> queue = new PriorityQueue<>(new Comparator<Freq>() {
@Override
public int compare(Freq o1, Freq o2) {
//定义比较的方式
return o1.times - o2.times;
}
});
//需要将Map转为set才能遍历
for(Map.Entry<Integer,Integer> entry:numTimes.entrySet()){
queue.offer(new Freq(entry.getKey(),entry.getValue()));
if(queue.size() > k){
queue.poll();
}
}
//3、从队列中取出这k个freq的Key值就是所求结果
int[] result = new int[k];
for (int i = 0; i < k; i++) {
result[i] = queue.poll().key;
}
return result;
}
}
前k个高频单词
1、描述:给定一个单词列表 words
和一个整数 k ,返回前 k 个出现次数最多的单词。返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率, 按字典顺序排序。
2、思路:
这个题的整体思路和上面的题目差不多,但是有多次需要反转的地方。取大用小堆,这是首先能想到的。返回的答案应该按单词出现频率由高到低排序?这个怎么实现?小堆的最后结果保存了出现频次最高的前3个字母,但小堆元素默认是升序出队的,所以我们需要一个方法让他倒着打印。使用链表的头插来保存元素。如果不同的单词有相同出现频率, 按字典顺序 排序?也就是说字典序小的需要排在字典序大的前面(比如
“i”
在“l”
之前),但是最终选择用头插来保存元素,这样的话字典序大的就保存到了字典序小的前面。所以此时需要让JDK觉得字典序大的在字典序小的前面,头插之后,字典序小的就保存在了字典序大的前面。
3、代码实现
public class Num692_TopKFrequent {
public List<String> topKFrequent(String[] words, int k) {
//1、首先用Map来保存单词及其出现的频次
Map<String,Integer> wordsTable = new HashMap<>();
for(String str : words){
if(wordsTable.containsKey(str)){
//将频次取出
int times = wordsTable.get(str);
//再塞入Map中
wordsTable.put(str,times + 1);
}else{
//此时Map中不包含str,第一次出现
wordsTable.put(str,1);
}
}
//2、此时wordsTable保存了每个不重复单词及其出现的频次
//先定义优先级对列判定优先级,取大用小
Queue<String> queue = new PriorityQueue<>(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
//先按照出现频次来判定
int o1Times = wordsTable.get(o1);
int o2Times = wordsTable.get(o2);
//频次相同按照字典序判断
//由于最终要使用头插法保存元素,所以字典序大的要排在前面
return o1Times == o2Times? o2.compareTo(o1) : o1Times - o2Times;
}
});
//3、扫描Map集合,保存出现频次最高的前K个单词
for(Map.Entry<String,Integer> entry : wordsTable.entrySet()){
queue.offer(entry.getKey());
if(queue.size() > k){
queue.poll();
}
}
//4、此时队列中就保存了出现频次最高的前k个单词,输出保存
LinkedList<String> result = new LinkedList<>();
while (!queue.isEmpty()){
//现在是小堆,出队时按照出现频次右低到高出队的
//所以此时要将每个出队元素头插到链表中来保存最后结果
result.addFirst(queue.poll());
}
return result;
}
}
上面两道题目涵盖了优先级队列、Map的使用以及元素的比较相关的知识点,综合性较强,属于TopK问题中相对复杂的问题。遇到这类问题,可以先将每一步的解决思路写下来,然后对每一步做代码的实现,这样问题就可以被拆分,然后逐步攻克即可。
继续加油努力!!!