前 K 个高频元素

题目

给定一个非空的整数数组,返回其中出现频率前 k 高的元素。

  • 示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
  • 示例 2:
输入: nums = [1], k = 1
输出: [1]
  • 提示:

你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。

你的算法的时间复杂度必须优于 O(n log n) , n 是数组的大小。

题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。

你可以按任意顺序返回答案。

思路1-列表排序

我们直接遍历数组,统计对应的次数。

然后对所有的次数进行排序,取出 TOPK 的次数。

再次遍历 map,对比次数,获取结果。

public static int[] topKFrequent(int[] nums, int k) {
    // 参数校验
    if(nums == null || k <= 0) {
        return null;
    }
    // 次数统计
    Map<Integer, Integer> countMap = new HashMap<>(nums.length);
    for(int num : nums) {
        int count = countMap.getOrDefault(num, 0) + 1;
        countMap.put(num, count);
    }

    // 计算 topK 的次数
    List<Integer> countList = new ArrayList<>(countMap.values());
    countList.sort(new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            // 倒排,次数多的放在前面
            return o2 - o1;
        }
    });

    // 1 2 3  TOP3 就是对应最后一个
    int countLimit = countList.get(k - 1);
    // 5 4 3 2 1
    int[] results = new int[k];
    int resultIndex = 0;
    for(Map.Entry<Integer, Integer> entry : countMap.entrySet()) {
        int num = entry.getKey();
        int numCount = entry.getValue();
        if(numCount >= countLimit) {
            results[resultIndex++]  = num;
        }
    }
    return results;
}

效果:

Runtime: 10 ms, faster than 64.94% of Java online submissions for Top K Frequent Elements.
Memory Usage: 41.6 MB, less than 62.33% of Java online submissions for Top K Frequent Elements.

思路2-优先级队列排序

也就是大小堆排序。

public static int[] topKFrequent(int[] nums, int k) {
    // 次数统计
    Map<Integer, Integer> countMap = new HashMap<>(nums.length);
    for(int num : nums) {
        int count = countMap.getOrDefault(num, 0) + 1;
        countMap.put(num, count);
    }
    // 计算 topK 的次数
    PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(countMap.values());
    int minCount = 0;
    int times = priorityQueue.size() - k;
    for(int i = 0; i < times; i++) {
        minCount = priorityQueue.poll();
    }
    // 构建结果
    int[] results = new int[k];
    int resultIndex = 0;
    for(Map.Entry<Integer, Integer> entry : countMap.entrySet()) {
        int num = entry.getKey();
        int numCount = entry.getValue();
        if(numCount > minCount) {
            results[resultIndex++]  = num;
        }
    }
    return results;
}

效果:

Runtime: 9 ms, faster than 84.95% of Java online submissions for Top K Frequent Elements.
Memory Usage: 42.2 MB, less than 18.01% of Java online submissions for Top K Frequent Elements.

思路3-TreeMap 实现

TreeMap 也可以达到类似的排序效果:

public static int[] topKFrequent(int[] nums, int k) {
    // 次数统计
    Map<Integer, Integer> countMap = new HashMap<>(nums.length);
    for(int num : nums) {
        int count = countMap.getOrDefault(num, 0) + 1;
        countMap.put(num, count);
    }
    TreeMap<Integer, List<Integer>> sortedMap = new TreeMap<>(new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            // 次數的在前面
            return o2 - o1;
        }
    });

    // 设置次数(因为结果唯一,实际上次数并不唯一,可能相同。比如 [1,2],返回2)
    // 如果次数相同,会导致覆盖。
    for(Map.Entry<Integer, Integer> entry : countMap.entrySet()) {
        // 次数 - 元素
        int times = entry.getValue();
        List<Integer> list = sortedMap.getOrDefault(times, new ArrayList<>());
        list.add(entry.getKey());
        sortedMap.put(entry.getValue(), list);
    }
    // 返回前 k 个
    int[] results = new int[k];
    int index = 0;
    for(Map.Entry<Integer, List<Integer>> entry : sortedMap.entrySet()) {
        for(Integer integer : entry.getValue()) {
            if(index >= k) {
                break;
            }
            results[index++] = integer;
        }
    }
    return results;
}

不过这里需要注意,因为 sortedMap 是用次数当做唯一 key。

相同的次数可能对应多个数,比如 [1,2],所以对应的 value 应该是一个链表。

效果:

Runtime: 9 ms, faster than 84.95% of Java online submissions for Top K Frequent Elements.
Memory Usage: 41.5 MB, less than 62.33% of Java online submissions for Top K Frequent Elements.

思路4-摩尔投票法

我们在 如何找到数组中出现次数最多的元素? 中介绍过摩尔投票法。

不过感觉这里并不适用,因为当 k 较大的时候,投票的复杂度就是 k*n,如果 k=n 就是 n^2,不符合题意。

所以这里没有实战下去。

692. 前K个高频单词

题目

给一非空的单词列表,返回前 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) 空间复杂度解决。

## 思路分析

这一题和上面一题,某种角度而言是一模一样,只不过 key 从数字变成了字符串。

还有一点,这里的次数可能相同,如果不同的单词有相同出现频率,按字母顺序排序。

## 思路1-TreeMap

```java
public static List<String> topKFrequent(String[] words, int k) {
    // 防御
    // 列表为空?内容为空?k 大于 words 长度?
    // 次数统计
    Map<String, Integer> countMap = new HashMap<>();
    for(String string : words) {
        countMap.put(string, countMap.getOrDefault(string, 0) + 1);
    }

    // 次数排序
    TreeMap<Integer, List<String>> treeMap = new TreeMap<>((o1, o2) -> o2-o1);
    for(Map.Entry<String, Integer> entry : countMap.entrySet()) {
        int count = entry.getValue();
        List<String> list = treeMap.getOrDefault(count, new ArrayList<>());
        list.add(entry.getKey());
        treeMap.put(count, list);
    }

    // 返回结果
    List<String> results = new ArrayList<>(k);
    for(List<String> values : treeMap.values()) {
        // 排序
        Collections.sort(values);
        // 添加元素
        for(String value : values) {
            results.add(value);
            if(results.size() >= k) {
                return results;
            }
        }
    }
    return results;
}

最后的字符串,如果次数相同,则需要执行一次排序。

效果:

Runtime: 7 ms, faster than 23.26% of Java online submissions for Top K Frequent Words.
Memory Usage: 39.3 MB, less than 36.81% of Java online submissions for Top K Frequent Words.

思路2-排序

其实 TreeMap 承担了 2 个职责,一个是次数统计,还有一个就是排序。

当然,我们可以完全不借助 TreeMap,而是把一切都交给排序来做。

public static List<String> topKFrequent(String[] words, int k) {
    // 次数统计
    Map<String, Integer> countMap = new HashMap<>(words.length);
    for(String string : words) {
        countMap.put(string, countMap.getOrDefault(string, 0) + 1);
    }

    // 次数排序
    List<String> sortList = new ArrayList<>(countMap.keySet());
    sortList.sort(new Comparator<String>() {
        @Override
        public int compare(String o1, String o2) {
            //1. 比较次数
            int count1 = countMap.get(o1);
            int count2 = countMap.get(o2);
            // 次数不等
            if (count1 != count2) {
                return count2 - count1;
            } else {
                // 自然排序
                return o1.compareTo(o2);
            }
        }
    });

    // 截取结果
    return sortList.subList(0, k);
}

效果

Runtime: 5 ms, faster than 86.38% of Java online submissions for Top K Frequent Words.
Memory Usage: 39.3 MB, less than 36.81% of Java online submissions for Top K Frequent Words.

辅助度

时间:主要是排序。N(logN)

空间:countMap O(N)

ps: 实际上可以发现 Leetcode 中的很多题目,解法是大同小异的。

有时候我们掌握一题,这个系列的题目都会解决了。

所以刷题在于精,而不在于多。

215. 数组中的第K个最大元素

题目

在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

  • 示例 1:
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5
  • 示例 2:
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4

说明:

你可以假设 k 总是有效的,且 1 ≤ k ≤ 数组的长度。

思路1-排序

最简单的方式,就是直接倒排排序,然后返回指定位置的元素即可。

不过 java 的 Arrays.sort 方法对 int[] 指定比较器不友好,我们从后往前返回即可:

效果

public static int findKthLargest(int[] nums, int k) {
    Arrays.sort(nums);
    return nums[nums.length - k];
}

效果

Runtime: 1 ms, faster than 98.08% of Java online submissions for Kth Largest Element in an Array.
Memory Usage: 39.2 MB, less than 60.04% of Java online submissions for Kth Largest Element in an Array.

思路2-堆

当然,这种 topK 的问题,使用堆还是比较简单的。

public static int findKthLargest(int[] nums, int k) {
    int len = nums.length;
    // 使用一个含有 len 个元素的最大堆,lambda 表达式应写成:(a, b) -> b - a
    PriorityQueue<Integer> maxHeap = new PriorityQueue<>(len, (a, b) -> b - a);
    for (int num : nums) {
        maxHeap.add(num);
    }
    for (int i = 0; i < k - 1; i++) {
        maxHeap.poll();
    }
    return maxHeap.peek();
}

效果:

Runtime: 4 ms, faster than 62.52% of Java online submissions for Kth Largest Element in an Array.
Memory Usage: 39.2 MB, less than 60.04% of Java online submissions for Kth Largest Element in an Array.

不过性能却不是很好。

这个也可以优化,因为 PriorityQueue 无法直接 O(1) 访问,如果 k 很大,则寻找起来特别慢。

这个可以根据 k 是否超过 num.length/2 进行优化。

思路3-自定义堆

感觉最好的方式,实际上是利用数组实现一个大小堆。

这样可以 O(1) 访问,性能应该会好一些。

当然,这一题也可以使用 quickSort/mergeSort 等方法解决,我们后续还是重温一下排序算法。

小结

希望本文对你有帮助,如果有其他想法的话,也可以评论区和大家分享哦。

各位极客的点赞收藏转发,是老马持续写作的最大动力!

参考资料

https://leetcode-cn.com/problems/number-of-digit-one/