算法练习-堆
1. 堆的定义和存储
- 堆必须是一个完全二叉树
- 堆中的每个节点的值必须大于等于或者小于等于子树中每个节点的值
如果堆中每个节点的值都大于等于子树中每个节点的值,这种堆叫做大根堆
如果堆中每个节点的值都小于等于子树中每个节点的值,这种堆叫做小根堆
由于堆是完全二叉树,所以堆适合用数组来进行存储
2. 堆的操作
2…1 往堆中插入数据
将新数据插入到数组末尾,然后执行自下而上的堆化操作
拿大顶堆举例,如果插入的数据让堆不满足大顶堆的要求就需要自下而上进行堆化
不断和父节点进行比较,如果比父节点的值大,就和父节点交换
满足大顶堆条件或者到达根节点时,停止交换
public class Heap {
private int[] a; // 数组,从下标1开始存数据
private int n; // 堆可以存储的最大数据个数
private int count; // 堆中已经存储的数据的个数
public Heap(int capacity) {
a = new int[capacity + 1];
n = capacity;
count = 0;
}
public int insert(int data) {
if (count >= n) return; // 堆满了
++count;
a[count] = data;
int i = count;
while (i / 2 > 0 && a[i] > a[i / 2]) { // 自下往上堆化
swap(a, i, i / 2);
i = i / 2;
}
}
private void swap(int[] a, int i, int j) {
int tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
}
2.2取堆顶元素
大顶堆的堆顶元素是堆中最大的元素
小顶堆的堆顶元素是堆中最小的元素
public int top() {
if (count == 0) return Integer.MIN_VALUE;
return a[1];
}
2.3 删除堆顶元素
将最后一个节点放到堆顶,然后利用自上而下的堆化方式让堆满足条件
如果不交换节点,直接进行自上而下的堆化方式,容易产生空洞,从而不满足完全二叉树的条件
public void removeTop() {
if (count == 0) return; // 堆中没有数据
a[1] = a[count];
--count;
heapify(a, count, 1);
}
private void heapify(int[] a, int n, int i) { // 自上往下堆化
while (true) {
int maxPos = i;
if (i * 2 <= n && a[i] < a[i * 2]) maxPos = i * 2;
if (i * 2 + 1 <= n && a[maxPos] < a[i * 2 + 1]) maxPos = i * 2 + 1;
if (maxPos == i) break;
swap(a, i, maxPos);
i = maxPos;
}
}
2.4 更新元素值
如果更新之后的值变小了,就进行自上往下的堆化
如果更新之后的值变大了,就进行自下往上的堆化
3 堆排序
堆排序是原地排序算法但不是稳定排序算法,时间复杂度:O(nlogn),空间复杂度:O(1)
堆排序包含两个大的步骤:
- 建堆,将数组中的数组原地组织成一个堆
- 排序,基于堆排序数据
3.1 建堆
第一种实现:从前往后处理每个元素,对每个元素执行自下往上的堆化
第二种实现:从后往前处理每个元素,对每个元素执行自上往下的堆化
private void buildHeap(int[] a, int n) {
for (int i = n / 2; i >= 1; --i) {
heapify(a, n, i);
}
}
private void heapify(int[] a, int n, int i) {
while (true) {
int maxPos = i;
if (i * 2 <= n && a[i] < a[i * 2]) maxPos = i * 2;
if (i * 2 <= n && a[maxPos] < a[i * 2 + 1]) maxPos = i * 2 + 1;
if (maxPos == i) break;
swap(a, i, maxPos);
i = maxPos;
}
}
3.2 排序
- 将栈顶元素与最后一个元素进行交换。最大元素就放到了下标为n的位置,堆大小减一
- 交换之后的栈顶元素,自上往下堆化,重新构建成堆
- 重复这个过程,直到堆只剩下一个元素
// 方法名称参照上面的代码
public void sort(int[] a, int n) {
buildHeap(a, n);
int k = n;
while (k > 1) {
swap(a, 1, k);
--k;
heapify(a, k, 1);
}
}
4 题型
4.1 优先级队列
4.1.1 合并k个升序链表(hard)
链接:https://leetcode.cn/problems/merge-k-sorted-lists
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例 1:
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
1->4->5,
1->3->4,
2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6
示例 2:
输入:lists = []
输出:[]
示例 3:
输入:lists = [[]]
输出:[]
提示:
k == lists.length
0 <= k <= 10^4
0 <= lists[i].length <= 500
-10^4 <= lists[i][j] <= 10^4
lists[i] 按 升序 排列
lists[i].length 的总和不超过 10^4
由于数组中封装的是一个一个的链表,所以可以先将所有链表的头节点的数据传到小顶堆中,每次传出根节点之后,再传入根节点对应的链表的下一个节点的数据
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0) return null;
int k = lists.length;
PriorityQueue<ListNode> minQ = new PriorityQueue<>(new Comparator<ListNode>() {
public int compare(ListNode p1, ListNode p2) {
return p1.val - p2.val;
}
});
for (int i = 0; i < k; ++i) {
if (lists[i] != null) {
minQ.offer(lists[i]);
}
}
ListNode head = new ListNode();
ListNode tail = head;
while(!minQ.isEmpty()) {
ListNode curNode = minQ.poll();
tail.next = curNode;
tail = tail.next;
if (curNode.next != null) {
minQ.offer(curNode.next);
}
}
return head.next;
}
}
4.2 TopK
- 问题
- 针对静态数据(查询TopK的操作)
- 针对动态数据(只包含添加数据操作和查询TopK操作)
- 解决思路
- 排序,然后取数组中第k个元素 -> 静态数据
- 利用快速排序的算法,可以做到O(n) -> 静态数据
- 利用堆,插入 : O(logk),获取 : O(1) -> 动态数据
维护一个大小为k的小顶堆,有数据被添加时
如果堆中的数据个数小于k,直接将新数据插入小顶堆
如果堆中的数据等于k,就拿新添加的数据与堆顶元素比较
如果新添加的数据大于堆顶元素,就将堆顶元素删除,将新添加的数据插入堆中
如果新添加的数据小于等于堆顶元素,就不对堆进行任何处理
4.2.1 前k个高频元素
链接:https://leetcode.cn/problems/top-k-frequent-elements
给你一个整数数组 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 个高频元素的集合是唯一的
class Solution {
private class QElement {
int val;
int count;
public QElement(int val, int count) {
this.val = val;
this.count = count;
}
}
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> counts = new HashMap<>();
for (int num : nums) {
counts.put(num, counts.getOrDefault(num, 0) + 1);
}
PriorityQueue<QElement> queue = new PriorityQueue<>(new Comparator<QElement>() {
public int compare(QElement e1, QElement e2) {
return e1.count - e2.count;
}
});
for (Map.Entry<Integer, Integer> entry : counts.entrySet()) {
int num = entry.getKey();
int count = entry.getValue();
if (queue.size() < k) {
queue.offer(new QElement(num, count));
} else {
if (queue.peek().count < count) {
queue.poll();
queue.offer(new QElement(num, count));
}
}
}
int[] result = new int[k];
for (int i = 0; i < k; ++i) {
result[i] = queue.poll().val;
}
return result;
}
}
4.3 求中位数&百分数
求中位数要维护具有以下两个特征的堆
- 一个大顶堆,一个小顶堆
- 每个堆中的元素个数接近n / 2 ,如果n是偶数,两个堆中的数据个数都是n / 2; 如果n是奇数,大顶堆有n / 2 + 1 个数据,小顶堆有n / 2 个数据
- 大顶堆中的元素都小于等于小顶堆中的元素
那么大顶堆中堆顶的元素就是中位数
如果新数据小于等于大顶堆的堆顶元素,就将新数据插入到大顶堆,否则,插入到小顶堆
此时有可能出现两个堆中的数据个数不符合前面的规定。可以通过一个堆中不停的将堆顶元素移动到另一个堆,让两个堆中的数据个数满足前面的约定
求百分位数和中位数类似,同样需要维护两个堆
假设当前数据个数为n,大顶堆中保存前99%*n个数据,小顶堆中保存后1% * n个数据。
大顶堆堆顶元素就是要找的99百分位数据
其余基本和中位数一样
4.3.1 数据流的中位数(hard)
链接:https://leetcode.cn/problems/find-median-from-data-stream
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
例如 arr = [2,3,4] 的中位数是 3 。
例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5 。
实现 MedianFinder 类:
MedianFinder() 初始化 MedianFinder 对象。
void addNum(int num) 将数据流中的整数 num 添加到数据结构中。
double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10-5 以内的答案将被接受。
示例 1:
输入
[“MedianFinder”, “addNum”, “addNum”, “findMedian”, “addNum”, “findMedian”]
[[], [1], [2], [], [3], []]
输出
[null, null, null, 1.5, null, 2.0]
解释
MedianFinder medianFinder = new MedianFinder();
medianFinder.addNum(1); // arr = [1]
medianFinder.addNum(2); // arr = [1, 2]
medianFinder.findMedian(); // 返回 1.5 ((1 + 2) / 2)
medianFinder.addNum(3); // arr[1, 2, 3]
medianFinder.findMedian(); // return 2.0
提示:
-105 <= num <= 105
在调用 findMedian 之前,数据结构中至少有一个元素
最多 5 * 104 次调用 addNum 和 findMedian
class MedianFinder {
private PriorityQueue<Integer> minQueue = new PriorityQueue(new Comparator<Integer>() {
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
});
private PriorityQueue<Integer> maxQueue = new PriorityQueue(new Comparator<Integer>() {
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
public MedianFinder() {
}
public void addNum(int num) {
if (maxQueue.isEmpty() || num <= maxQueue.peek()) {
maxQueue.add(num);
} else {
minQueue.add(num);
}
while (maxQueue.size() < minQueue.size()) {
Integer e = minQueue.poll();
maxQueue.add(e);
}
while (minQueue.size() < maxQueue.size() - 1) {
Integer e = maxQueue.poll();
minQueue.add(e);
}
}
public double findMedian() {
if (maxQueue.size() > minQueue.size()) {
return maxQueue.peek();
} else {
return (maxQueue.peek() + minQueue.peek()) / 2f;
}
}
}
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder obj = new MedianFinder();
* obj.addNum(num);
* double param_2 = obj.findMedian();
*/