1. 堆的基本介绍
1.1 定义
堆必须是一个完全二叉树。
根据堆序性,可以把堆分为两类,分别为大顶堆和小顶堆。在大顶堆中,每个父节点元素要大于它的子节点元素。在小顶堆中,每个父节点元素都必须小于它的子节点元素。
Ex.大顶堆
小顶堆:
1.1.1 堆的存储:
首先我们按照层序遍历的顺序来给节点编号,从上到下,从左到右把这些元素对应到数组的下标,然后把树的元素存入到相应的下标里。
若节点的下标为i,那么左子节点的下标为2i+1,右子节点的下标为2i+2。
1.2 基本操作
运用以下两个操作基本能实现堆的所有功能。
1.2.1 下滤
我们把根节点向下调整的操作称为下滤。
例如图中这棵树,可以看到只有他的根元素不满足堆序性。因此我们通过操作可以把他调整为堆。
我们将这个节点与他的最大子节点进行比较(因为是大根堆,如果是小根堆则相反),如果小于则与最大子节点进行交换,直到大于它的最大子节点或者移动的底部为止。
下滤的时间复杂度为:
1.2.2 上滤
这个操作主要用于插入新元素到堆中
例如图中这棵树,只有树的最后一个元素破坏了堆序性。
我们将这个节点与他的父节点进行比较,如果大于则与父节点进行交换,直到小于它的父节点或者移动的顶部为止。
上滤的时间复杂度为:
1.2.3 建堆
如果有一个乱序的数组,要怎么操作才能把他转化为堆呢?
1.2.3.1 自顶而下
将新元素放到堆的最后一位,然后对其进行上滤操作。直到所有元素插入后,建堆完成。
1.2.3.2 自下而上
对每个父节点进行下滤,复杂度是O(n)
2. 堆的具体应用:优先级队列
优先级队列有两个操作,一个是插入队列,另一个为弹出最小元素。这种队列可以用小根堆来实现。因为小根堆的根节点是最小元素,所以直接弹出根节点即可完成操作。
弹出后要继续调整成堆,调整方法也很简单,将最后一个元素放到根节点的位置,然后进行下滤操作。
堆排序
将优先队列的所有元素依次弹出。
我们可以发现,用大根堆的堆排序结果是正序的,用小根堆是倒序的。
LeeCode 347: 前K个高频元素
给你一个整数数组 nums
和一个整数 k
,请你返回其中出现频率前 k
高的元素。你可以按 任意顺序 返回答案。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2 输出: [1,2]
示例 2:
输入: nums = [1], k = 1 输出: [1]
提示:
-
1 <= nums.length <= 105
-
k
的取值范围是[1, 数组中不相同的元素的个数]
-
题目数据保证答案唯一,换句话说,数组中前
k
个高频元素的集合是唯一的
进阶:你所设计算法的时间复杂度 必须 优于 O(n log n)
,其中 n
是数组大小。
1. 思路分析
这道题可以用大顶堆做也可以用小顶堆做,具体在Java中表现为对优先级队列的构造器里的比较器进行设置。但是确实这道题在做的时候还是有些问题的,一是对于优先级队列不了解,里面的元素可以是数组元素,并且要返回的是map里的key而不是value,也就是说不是次数而是数本身。话不多说,直接上代码进行分析。
2.代码
方法一:大顶堆实现
class Solution {
public int[] topKFrequent(int[] nums, int k) {
if(nums.length == 1) {
return new int[]{nums[0]};
}
int[] res = new int[k];
HashMap<Integer,Integer> map = new HashMap<>()
for(int i : nums) {
map.put(i, map.getOrDefault(i, 0) + 1);
}
PriorityQueue<int[]> que = new PriorityQueue<>(((pair1,pair2)->pair2[1]-pair1[1]));
for(Map.Entry<Integer,Integer> entry : map.entrySet()) {
que.add(new int[]{entry.getKey(),entry.getValue()});
}
for(int i = 0; i < k; i++) {
res[i] = que.poll()[0];
}
return res;
}
}
需要记忆的方法:
HashMap
中的map.entrySet()
是从来没有用过的方法。在这个地方需要重点注意一下啊。
方法二:用小顶堆实现
用小顶堆实现其实更符合这道题的要求,但是我一开始没有想到这个东西。
class Solution {
public int[] topKFrequent(int[] nums, int k) {
if(nums.length == 1) {
return new int[]{nums[0]};
}
int[] res = new int[k];
HashMap<Integer,Integer> map = new HashMap<>();
for(int i : nums) {
map.put(i, map.getOrDefault(i, 0) + 1);
}
PriorityQueue<int[]> que = new PriorityQueue<>(k,((pair1,pair2)->pair1[1]-pair2[1]));
for(Map.Entry<Integer,Integer> entry : map.entrySet()) {
if(que.size() < k) {
que.add(new int[]{entry.getKey(), entry.getValue()});
} else {
if(entry.getValue() > que.peek()[1]) {
que.poll();
que.add(new int[]{entry.getKey(), entry.getValue()});
}
}
}
for(int i = k - 1; i >= 0; i--) {
res[i] = que.poll()[0];
}
return res;
}
}
事实上使用小顶堆后会发现算法的时间复杂度下降了很多。以此做个记录,到时候反复复习。