文章目录
零、前言
首先说说我写这篇系列文的原因吧,最近准备面试在刷算法题,题也刷了不少,但遇到新题总是不能很快的想到好的解决方案,总是被算法猛撕。想想还是没有总结到位,于是从今天开始《算法撕裂者》这系列的文章,抽空常更新文章,争取成为算法撕裂者。腹肌都能撕裂的我不信撕不裂算法,干就完了~ 加油~ 奥利给!
一、算法必学:经典的 Top K 问题
什么是 Top K 问题?简单来说就是在一堆数据里面找到前 K 大(当然也可以是前 K 小)的数。
这个问题也是十分经典的算法问题,不论是面试中还是实际开发中,都非常典型。面对这种问题,你知道怎么解决吗?
二、很容易想到的方案之排序法
既然是要前 K 个,最简单粗暴的方法就是排序咯,来~ 下面有常见的十种排序方法,任您挑任您选!
但是就算高效的排序也需要在平均 O(nlogn)的时间复杂度找到结果。而且还有个很大的问题就是当数据量很庞大的时候,使用这种方法要消耗大量的内存空间,甚至一台机器的内存都无法完成的情况。
三、分布式思想处理海量数据
既然考虑到海量数据,那我们可以往分布式的方向去思考。
我们可以将数据分散在多台机器中,然后每台机器并行计算各自的 TopK 数据,最后汇总,再计算得到最终的 TopK 数据
这种数据分片的分布式思想在面试中非常值得一提,在实际项目中也十分常见
四、最经典的方法之堆
面对 Top K 问题,最经典的解法是利用 堆。(堆是数据结构里的知识,学习算法的一个重要前提需要了解常用的数据结构,既然大胆点开了算法文章,这里默认大家对前提知识已经了解,在此就不多做介绍,不懂请自行百度,后期有时间我再写相关文章。)
这里简单给大家介绍一下堆,以防陷入知识盲区的旁友。
堆(数据结构):堆可以被看成是一棵树【树就不用多说了吧,对,就是你家门外的那棵树 (手动滑稽)】
最大堆(也叫大根堆、大顶堆):根结点的键值是所有堆结点键值中最大者,且每个结点的值都比其孩子的值大。
最小堆(也叫小根堆、小顶堆):根结点的键值是所有堆结点键值中最小者,且每个结点的值都比其孩子的值小。
维护一个容量为 K 的最小堆(需求而定,同理最大堆),依次将数据放入堆中,当堆的容量满了时,只需将堆顶元素与下一个元素作比较,如果下一个元素比堆顶元素大,则将当前堆顶元素删除,并将该元素插入到堆中。遍历完全部数据后,Top K 的数据也就在堆中了。
至于是构造最小堆还是最大堆,就根据需求而定:
如果是要前 K 个最大的元素,那就构造最小堆。
(逻辑就是,要前 K 个最大值时,如果待添加的元素大于堆中的最小值,就可以添加。 )
如果是要前 K 个最小的元素,那就构造最大堆。
(逻辑就是,要前 K 个最小值时,如果待添加的元素小于堆中的最大值,就可以添加。 )
用堆实现的好处:
- 即使海量数据,我们使用一台计算机就可以解决。因为我们不需要一次性将全部数据取出来,可以一次只取一部分,因为我们只需要将数据一个个拿来与堆顶比较。
- 时间复杂度,整个操作中,遍历数组需要 O(n) 的时间复杂度,一次堆化操作需要 O(logK),加起来就是 O(nlogK) 的复杂度,如果 K 远小于 n 的话, O(nlogK) 其实就接近于 O(n) 了,甚至会更快,因此是十分高效的。
五、纸上得来终觉浅,绝知此事要躬行。
Talk is cheap ,Show me the code !
开胃前菜已经品过了,接下来上两道硬菜。
1、LeetCode面试题–最小的 K 个数
题目描述:
输入整数数组 arr ,找出其中最小的 k 个数。
例如,输入4、5、1、6、2、7、3、8这8个数字,
则最小的4个数字是1、2、3、4。
示例 1:
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]
这个问题就是求前 K 个最小的数,所以使用的是最大堆
废话少说,放码过来
package Array;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.PriorityQueue;
/**
* 最小的 K 个数
* 本题知识点: 数组、堆
*/
public class $40_GetLeastNumbers_Solution {
public static void main(String[] args) {
$40_GetLeastNumbers_Solution solution = new $40_GetLeastNumbers_Solution();
int[] arr = {4, 5, 1, 6, 2, 7, 3, 8};
int k = 4;
ArrayList<Integer> res = solution.GetLeastNumbers_Solution(arr, k);
System.out.println("res:" + res); // [4,3,1,2]
}
public ArrayList<Integer> GetLeastNumbers_Solution(int[] input, int k) {
ArrayList<Integer> result = new ArrayList<>();
int length = input.length;
if (k > length || k == 0) {
return result;
}
// 构造最大堆(Java PriorityQueue 默认是实现最小堆)
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(k,
// 助记 默认升序 o1 - o2
// 降序则为 o2 - o1
// (o1, o2) -> o2 - o1 // Java8 后支持 lambda 表达式
new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1); // 并非从大到小排序,确保维持队首值为最大值
}
}
);
for (int i = 0; i < length; i++) {
// 如果队列中元素个数未到k,直接添加
if (maxHeap.size() != k) {
maxHeap.offer(input[i]);
}
// 达到k数,判断下一个值是否小于队首值,小于则将队首值删除,将此值添加
else if (input[i] < maxHeap.peek()) {
Integer temp = maxHeap.poll();
temp = null; //GC回收
maxHeap.offer(input[i]);
}
}
// 遍历最大堆,取出结果
for (Integer integer : maxHeap) {
result.add(integer);
}
return result;
}
}
个人认为注释还是写得挺详细的,就不多文字解释了。理解了前面的思路,这个代码应该也不难懂,最好自己手敲一遍在IDE中调式运行,看着嗯嗯嗯我懂~ 敲完就哎哎哎卧槽!看代码和写代码是两回事!
2、LeetCode面试题–前 K 个高频元素
题目描述:
给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
这个问题很明显,就是求前 K 个频率最多元素,所以我们使用最小堆
这道题麻烦点的就是多了统计频率的步骤,我们使用Map
来统计key
的频率value
,然后构造最小堆比较的时候是根据频率value
比较
下面开撸
package Array;
import java.util.Comparator;
import java.util.List;
import java.util.LinkedList;
import java.util.Collections;
import java.util.PriorityQueue;
import java.util.TreeMap;
/**
* @description: 前 K 个高频元素 (中等)
* 本题知识点:数组、哈希、堆
* @author: Kevin
* @createDate: 2020/2/24
* @version: 1.0
*/
public class TopKFrequent {
public static void main(String[] args) {
TopKFrequent solution = new TopKFrequent();
int[] nums = {1, 1, 2, 3, 3, 3, 4, 5, 5, 5, 5};
int k = 3;
List<Integer> res = solution.TopKFrequent(nums, k);
System.out.println(res); //[5, 3, 1]
}
// 解决方法
public List<Integer> TopKFrequent(int[] nums, int k) {
// 1. 使用 map 统计频率
TreeMap<Integer, Integer> map = new TreeMap<>();
for (int num : nums) {
if (map.containsKey(num)) {
map.put(num, map.get(num) + 1);
} else {
map.put(num, 1);
}
}
// 2. 根据频率,将频率添加到优先队列(Java默认实现最小堆)
PriorityQueue<Integer> pq = new PriorityQueue<>(
// (o1, o2) -> map.get(o1) - map.get(o2)
new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
// 按照频率,小的在前 (即 默认升序)
return map.get(o1) - map.get(o2);
}
}
);
for (int key : map.keySet()) {
// 如果队列中元素个数未到k,直接添加
if (pq.size() < k) {
pq.add(key);
}
// 达到k数,判断下一个值是否大于队首值,大于则将队首值删除,将此值添加
else if (map.get(key) > map.get(pq.peek())) {
pq.poll();
pq.add(key);
}
}
List<Integer> res = new LinkedList<>();
while (!pq.isEmpty()) {
res.add(pq.poll());
}
Collections.reverse(res); // 翻转数组,输出结果频率由高到低
return res;
}
}
六、总结(文末有彩蛋!!!)
总结一下这篇文章,我们先提出了TopK问题,以及解决思路,排序虽然简单但是效率和空间都不佳;分布式是个好思想但是需要物资;最经典的方法则是利用堆。然后介绍了最大堆和最小堆以及什么情况用什么堆。最后用两个算法面试题实操运用。
为了帮助大家记忆,我特地作了首助记诗, 献丑奉上。
要是觉得不错的话666点赞来一波呀~ 你们的鼓励就是我的动力!谢谢!
最最后~~
推广一下我的个人公众号,用作个人分享生活、分享技术、分享资源的。最近在找工作比较忙,抽空不定期更新,欢迎大家关注一波~ 微信搜索 “KeviniFree” 或扫下图二维码(公众号首发文章)
还有欢迎大家去全球最大的同性交友网站GitHub,我的不少代码和项目都将其开源分享,供大家学习交流。喜欢就Follow & Star me !!!
End-------------------------------感谢