📫作者简介:小明java问道之路,2022年度博客之星全国TOP3,专注于后端、中间件、计算机底层、架构设计演进与稳定性建工设优化。文章内容兼具广度深度、大厂技术方案,对待技术喜欢推理加验证,就职于知名金融公司后端高级工程师。
📫 热衷分享,喜欢原创~ 关注我会给你带来一些不一样的认知和成长。
🏆 2022博客之星TOP3 | CSDN博客专家 | 后端领域优质创作者 | CSDN内容合伙人
🏆 InfoQ(极客邦)签约作者、阿里云专家 | 签约博主、51CTO专家 | TOP红人、华为云享专家
🔥如果此文还不错的话,还请👍关注、点赞、收藏三连支持👍一下博主~
🍅 文末获取联系 🍅 👇🏻 精彩专栏推荐订阅收藏 👇🏻
专栏系列(点击解锁)
学习路线(点击解锁)
知识定位
全面讲解MySQL知识与企业级MySQL实战 🔥计算机底层原理🔥
本文目录
本文导读
本文针对堆算法笔试,首先图解大根堆,了解大根堆的概念以及创建堆的过程。
题目选择,最小的K个数、设计一个找数据流中第 k 大元素的类、数组中的第 k 大的数字为简单和中等题目。
进阶题目剑指Offer中的数据流中的中位数,由于面试不常考,想算法进阶、学习了解的读者可以自行学习。
一、图解大根堆
1、大根堆的概念
大根堆就是根节点是整棵树的最大值(根节点大于等于左右子树的最大值),对于他的任意子树,根节点也是最大值。
大根堆有两个操作,一个创建堆heapInsert时间复杂度是O(N),一个是当大根堆里的某个节点的值,发生变化的时候,需要对这个大根堆进行调整,每一次调整时间复杂度是O(lg(N)),调整的次数是跟这个堆的高度有关。
2、创建堆的过程(heapInsert)
比如一棵树有N个元素,存放在数组里分别对应0~N-1,假设数组中从0到 i-1 位置的元素是一个大根堆,然后把第i个位置的元素插入大根堆里。
构造一个新的大根堆,就需要从第 i 个位置的元素开始,依次看它的父节点的值是否小于它,如果小于就进行交换,直到它的父节点不小于它,或者到了该大根堆的最顶端的根节点,这一次过程才算彻底结束。
二、最小的K个数
题目描述:输入整数数组 arr ,找出其中最小的 k 个数。
例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
本题是求前 K 小,因此用一个容量为 K 的大根堆,每次 poll 出最大的数(相当于删除比第k+1大的数,保存就是前K小),那堆中保留的就是前 K 小。
虽然小根堆也可以实现,小根堆的话需要把全部的元素都入堆,那是 O(NlogN),就不是 O(NlogK)
这个方法比快排(O(N))慢,但是因为 Java 中提供了现成的 PriorityQueue(默认小根堆)
/**
* 保持堆的大小为K,然后遍历数组中的数字,遍历的时候做如下判断:
* 1. 若目前堆的大小小于K,将当前数字放入堆中。
* 2. 否则判断当前数字与大根堆堆顶元素的大小关系,如果当前数字比大根堆堆顶还大,这个数就直接跳过;
* <p>
* 反之如果当前数字比大根堆堆顶小,先poll掉堆顶,再将该数字放入堆中。
*
* @param arr
* @param k
* @return
*/
public int[] getLeastNumbers(int[] arr, int k) {
// PriorityQueue 默认是小根堆,实现大根堆需要重写一下比较器。
Queue<Integer> pq = new PriorityQueue<>((v1, v2) -> v2 - v1);
for (int num : arr) {
if (pq.size() < k) {
pq.offer(num);
} else if (num < pq.peek()) { // 检索但不删除此队列的头部
pq.poll(); // 删除头结点
pq.offer(num); // 讲num插入
}
}
// 返回堆中的元素
int[] res = new int[pq.size()];
int idx = 0;
for (int num : pq) {
res[idx++] = num;
}
return res;
}
三、设计一个找数据流中第 k 大元素的类
题目描述:设计一个找到数据流中第 k 大元素的类(class)。注意是排序后的第 k 大元素,不是第 k 个不同的元素。
请实现 KthLargest 类:
KthLargest(int k, int[] nums) :使用整数 k 和整数流 nums 初始化对象。
int add(int val) :将 val 插入数据流 nums 后,返回当前数据流中第 k 大的元素。
使用小根堆实现
构造的时候,使用一个大小为 k 的小根堆(优先队列)来存储前 k 大的元素,其中优先队列的队头为队列中最小的元素,也就是第 k 大的元素。
添加的时候,将元素 val 加入到优先队列中,如果优先队列的大于 k,队头元素弹出,以保证优先队列的大小为 k。
class KthLargest {
PriorityQueue<Integer> pq;
int k;
/**
* 使用一个大小为 k 的小根堆(优先队列)来存储前 k 大的元素
* 其中优先队列的队头为队列中最小的元素,也就是第 k 大的元素。
*
* @param k
* @param nums
*/
public KthLargest(int k, int[] nums) {
this.k = k;
pq = new PriorityQueue<>();
for (int x : nums) {
add(x);
}
}
public int add(int val) {
pq.offer(val); // 将元素 val 加入到优先队列中
// 如果优先队列的大于 k,队头元素弹出,以保证优先队列的大小为 k
if (pq.size() > k) {
pq.poll();
}
return pq.peek();
}
}
使用数组实现小根堆
构造的时候,先对数组进行排序,如果接收的数组nums的长度小于k,就直接将nums的元素都加入到Arraylist中,如果大于,只因后k个元素(前k大的元素)加入到ArrayList中
添加的时候,直接添加到ArrayList中,只有当ArrayList的大小大于1才进行后续操作,再排序,如果ArrayList的大小大于k,就移除第一项,最后ArrayList的大小等于k返回第一项的值,否则就是不存在第k大的值返回null
class KthLargest {
List<Integer> kNums = new ArrayList<Integer>();
int k;
public KthLargest(int k, int[] nums) {
this.k = k;
if (null != nums && nums.length > 0) {
Arrays.sort(nums); // 先对数组进行排序
if (nums.length <= k) {
for (int i = 0; i < nums.length; i++) { // 如果接收的数组nums的长度小于k,就直接将nums的元素都加入到Arraylist中
kNums.add(nums[i]);
}
} else { // 如果大于,就只取后k个元素(前k大的元素)加入到ArrayList中
for (int i = nums.length - 1; i >= nums.length - k; i--) {
kNums.add(nums[i]);
}
}
}
}
public Integer add(int val) {
kNums.add(val); // 直接添加到ArrayList中,只有当ArrayList的大小大于1才进行后续操作
if (kNums.size() > 1) {
kNums.sort(Integer::compare);
// 如果ArrayList的大小大于k,就移除第一项
if (kNums.size() > k) {
kNums.remove(0);
}
}
// 最后ArrayList的大小等于k返回第一项的值,否则就是不存在第k大的值返回null
return kNums.size() == k ? kNums.get(0) : null;
}
}
四、数组中的第 k 大的数字
题目描述:有一个整数数组,请你根据快速排序的思路,找出数组中第 k 大的数。
给定一个整数数组 a ,同时给定它的大小n和要找的 k ,请返回第 k 大的数(包括重复的元素,不用去重),保证答案存在。
题目解析:
优先队列(小根堆)默认是自然排序(升序),队头元素(根)是堆内的最小元素,对于小根堆来说,只要没满就可以加入(不需要比较);
如果满了,才判断是否需要替换第一个元素,在小根堆内,存储着K个较大的元素,根是这K个中最小的,如果出现比根还要大的元素,说明可以替换根
public int findKth(int[] a, int n, int K) {
// 优先队列(小根堆)默认是自然排序(升序),队头元素(根)是堆内的最小元素
PriorityQueue<Integer> queue = new PriorityQueue<>(K);
// 遍历每一个元素,调整小根堆
for (int num : a) {
// 对于小根堆来说,只要没满就可以加入(不需要比较);如果满了,才判断是否需要替换第一个元素
if (queue.size() < K) {
queue.add(num);
} else {
// 在小根堆内,存储着K个较大的元素,根是这K个中最小的,如果出现比根还要大的元素,说明可以替换根
if (num > queue.peek()) {
queue.poll(); // 删除小跟,留大的
queue.add(num);
}
}
}
return queue.isEmpty() ? 0 : queue.peek();
}
总结
本文针对堆算法笔试,首先图解大根堆,了解大根堆的概念以及创建堆的过程。
题目选择,最小的K个数、设计一个找数据流中第 k 大元素的类、数组中的第 k 大的数字为简单和中等题目。
进阶题目剑指Offer中的数据流中的中位数,由于面试不常考,想算法进阶、学习了解的读者可以自行学习。