题目
给一非空的单词列表,返回前 k 个出现次数最多的单词。
返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率,按字母顺序排序。
示例 1:
输入: [“i”, “love”, “leetcode”, “i”, “love”, “coding”], k = 2
输出: [“i”, “love”]
解析: “i” 和 “love” 为出现次数最多的两个单词,均为2次。
注意,按字母顺序 “i” 在 “love” 之前。
示例 2:
输入: [“the”, “day”, “is”, “sunny”, “the”, “the”, “the”, “sunny”, “is”, “is”], k = 4
输出: [“the”, “is”, “sunny”, “day”]
解析: “the”, “is”, “sunny” 和 “day” 是出现次数最多的四个单词,
出现次数依次为 4, 3, 2 和 1 次。
注意:
假定 k 总为有效值, 1 ≤ k ≤ 集合元素数。
输入的单词均由小写字母组成。
扩展练习:
尝试以 O(n log k) 时间复杂度和 O(n) 空间复杂度解决。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/top-k-frequent-words
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
题解
- 字符串频率不同的时候按照字符的出现次数从大到小排序。
- 字符串的出现频率相等的时候我们需要按照字符串的字典序从小到大排列。
- 字典序通俗点来说就是将两个不同字符串的同一位置的字符从第一位开始按照 26 个字母的顺序进行比对,a最小, z最大。如果两个字符串的第一位相等则开始进行比较下一位依次类推。举个例子 aa< ab,b> ac。
方法一:小根堆
- 哈希表统计单词的出现频率
- 构建一个 大小为 K 的小根堆按照上述规则自定义排序的比较器。
- 依次将单词加入堆中,当堆中的单词个数超过 K 个后,弹出顶部最小的元素使得堆中始终保留 K 个元素
- 遍历完成后剩余的 K个元素就是前 K大的。
- 依次弹出堆中的 K 个元素加入到所求的结果集合中。
注意:构建的是小根堆,所以从顶部弹出的元素顺序是从小到大的,最后需要反转集和。
// 小根堆,时间:O(N*logK),空间:O(N)
public static List<String> topKFrequent(String[] words, int k) {
// 统计词频
HashMap<String, Node> map = new HashMap<>();
for (String word : words) {
Node node = map.getOrDefault(word, new Node(word));
node.count++;
map.put(word, node);
}
// 小根堆维护最大TopK,堆顶维护"门槛"
PriorityQueue<Node> minHeap = new PriorityQueue<>(new NodeComparator());
for (Node node : map.values()) {
addToMinHeap(minHeap, k, node);
}
// 弹出TopK元素,加入答案集合
LinkedList<String> ans = new LinkedList<>();
while (!minHeap.isEmpty()) {
ans.addFirst(minHeap.poll().s);
}
return ans;
}
private static void addToMinHeap(PriorityQueue<Node> minHeap, int k, Node node) {
if (minHeap.size() < k) {
minHeap.add(node);
}
// 小根堆已满,且当前元素大于了"门槛",干掉"门槛"入堆:
else if (minHeap.comparator().compare(node, minHeap.peek()) > 0) {
minHeap.poll();
minHeap.add(node);
}
}
static class Node {
String s;
int count;
public Node(String s) {
this.s = s;
}
}
static class NodeComparator implements Comparator<Node> {
@Override
public int compare(Node o1, Node o2) { // 1)词频大的靠前 2)词频相同,字典序小的靠前
return o1.count != o2.count ? o1.count-o2.count : o2.s.compareTo(o1.s);
}
}
复杂度
- 时间:O(N*logK)
其中 N是 words 数组的长度。起初我们用O(N) 的时间计算每个单词的频率,然后将N 个单词添加到堆中(堆的大小最大为 K),添加每个单词的时间为 O(log(K))。总共消耗 O(Nlog(K))。弹出 K次我们忽略不计,所以总共是 O(Nlog(K))。
- 空间:O(N)
建堆和用哈希表计数
class Solution {
class Word implements Comparable<Word>{
String val;
int count;
public Word(String s,int c){
val=s;
count=c;
}
public String getVal(){
return this.val;
}
public int getCount() {
return this.count;
}
public int compareTo(Word w) {
int c=w.getCount();
if(c!=this.count)return c-this.count;
else return this.val.compareTo(w.getVal());
}
}
public List<String> topKFrequent(String[] words, int k) {
Map<String,Integer> wordmap=new HashMap<>();
for(String e:words){
wordmap.put(e,wordmap.getOrDefault(e,0)+1);
}
Set<Map.Entry<String,Integer>> wordset=wordmap.entrySet();
List<Word> wordlist=new ArrayList<>();
for(Map.Entry<String,Integer> e:wordset){
wordlist.add(new Word(e.getKey(),e.getValue()));
}
Collections.sort(wordlist);
List<String> ans=new ArrayList<>();
for(int i=0;i<k;i++){
ans.add(wordlist.get(i).val);
}
return ans;
}
}
方法二:【随机快速选择】,选出第K大元素做“门槛”
// 随机快速选择,时间复杂度:O(N + K * logK),空间:O(N)
public static List<String> topKFrequent2(String[] words, int k) {
// 统计词频
HashMap<String, Node> map = new HashMap<>();
for (String word : words) {
Node node = map.getOrDefault(word, new Node(word));
node.count++;
map.put(word, node);
}
// 转成Node数组
Node[] arr = new Node[map.size()];
int i = 0;
for (Node node : map.values()) arr[i++] = node;
// 【随机快选】:选出第K大元素
Comparator<Node> comparator = new NodeComparator();
Node kthNode = getTheKthNode(arr, k, comparator); // 选出第K大的Node(门槛)
// 遍历Node数组,收集前K大元素
Node[] nodes = new Node[k];
i = 0;
for (Node node : arr) {
if (comparator.compare(node, kthNode) >= 0) { // 大于门槛的元素,收集
nodes[i++] = node;
if (i == k) break;
}
}
// K个元素排序,加入答案集
Arrays.sort(nodes, comparator);
LinkedList<String> ans = new LinkedList<>();
for (i = k-1; i >= 0; i--) {
ans.add(nodes[i].s);
}
return ans;
}
// 选出第K大的Node,单路递归转迭代,时间:O(N)
private static Node getTheKthNode(Node[] arr, int k, Comparator<Node> comparator) {
k--; // 转成下标
int l = 0, r = arr.length-1;
while (l <= r) {
Node pivot = arr[l + (int)(Math.random()*(r-l))];
int[] range = partition(arr, l, r, pivot, comparator);
if (k < range[0]) {
r = range[0]-1;
} else if (k > range[1]) {
l = range[1] + 1;
} else {
return arr[range[0]];
}
}
return null;
}
// 分区
private static int[] partition(Node[] arr, int l, int r, Node pivot, Comparator<Node> comparator) {
int less = l-1, index = l, more = r+1;
while (index < more) {
if (comparator.compare(arr[index], pivot) > 0) {
swap(arr, index++, ++less);
} else if (comparator.compare(arr[index], pivot) < 0) {
swap(arr, index, --more);
} else {
index++;
}
}
return new int[]{less+1, more-1};
}
private static void swap(Node[] arr, int i, int j) {
Node tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
复杂度
- 时间复杂度:O(N + K * logK)
- 空间:O(N)
参考链接:https://leetcode-cn.com/problems/top-k-frequent-words/solution/xiao-gen-dui-huo-zhe-hashbiao-pai-xu-by-9uj06/
参考链接:https://leetcode-cn.com/problems/top-k-frequent-words/solution/hen-hao-li-jie-de-javaban-quan-dang-xue-mf8m8/
参考链接:https://leetcode-cn.com/problems/top-k-frequent-words/solution/liang-chong-jie-fa-xiao-gen-dui-nlogk-vs-76yf/