算法练习-堆
创始人
2025-06-01 08:26:15
0

1. 堆的定义和存储

  1. 堆必须是一个完全二叉树
  2. 堆中的每个节点的值必须大于等于或者小于等于子树中每个节点的值

如果堆中每个节点的值都大于等于子树中每个节点的值,这种堆叫做大根堆

如果堆中每个节点的值都小于等于子树中每个节点的值,这种堆叫做小根堆

由于堆是完全二叉树,所以堆适合用数组来进行存储

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)

堆排序包含两个大的步骤:

  1. 建堆,将数组中的数组原地组织成一个堆
  2. 排序,基于堆排序数据

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 minQ = new PriorityQueue<>(new Comparator() {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

  • 问题
  1. 针对静态数据(查询TopK的操作)
  2. 针对动态数据(只包含添加数据操作和查询TopK操作)
  • 解决思路
  1. 排序,然后取数组中第k个元素 -> 静态数据
  2. 利用快速排序的算法,可以做到O(n) -> 静态数据
  3. 利用堆,插入 : 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 counts = new HashMap<>();for (int num : nums) {counts.put(num, counts.getOrDefault(num, 0) + 1);}PriorityQueue queue = new PriorityQueue<>(new Comparator() {public int compare(QElement e1, QElement e2) {return e1.count - e2.count;}});for (Map.Entry 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 minQueue = new PriorityQueue(new Comparator() {public int compare(Integer o1, Integer o2) {return o1 - o2;}});private PriorityQueue maxQueue = new PriorityQueue(new Comparator() {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();*/

相关内容

热门资讯

五年之约已过,小米二把手林斌减... 2025年12月28日,小米集团发布公告表示,公司联合创始人、执行董事、副董事长林斌计划自2026年...
千方科技:9.56亿募资变更用... 12月26日晚,千方科技(002373)公告,为把握AI与交通深度融合的战略机遇,公司拟将原项目“下...
新年,预期的差异 核心观点:1.谁知未来?既是新年伊始,又是“十五五”开局,自然增添对经济的憧憬。然而,期许未必是必然...
宝马网红销冠张增威回应试行“一... 红星资本局12月28日消息,近日,宝马网红销冠张增威在个人社交平台发布视频内容,称自己在宝马销售中尝...
振芯科技三项议案被否!控股股东... 微成都报道 继4月底年度股东大会之后,振芯科技(300101)控股股东与创始团队董事会矛盾再次上演。...
236.88%,公募基金年度收... 截至12月26日,公募主动权益基金年内最高收益达到236.88%,不仅锁定年度冠军位置,还刷新纪录,...
展出米芾作品遭质疑 江西省博物... 中新网12月28日电 据江西省博物馆微信公众号消息,江西省博物馆28日发布声明称,近日,有观众质疑江...
重塑银保产品逻辑,海保人寿鑫玺... 近日,海保人寿旗下创新产品“鑫玺越(鑫享版)终身寿险”凭借其在财富规划与终身保障领域的卓越表现,成功...
吉宏股份:股东西藏永悦诗超企业... 新京报贝壳财经讯 吉宏股份(02603)发布公告,公司于近日收到控股股东、实际控制人庄浩女士及其一致...
锚定全球唯一,核聚变寡头,接住... 可控核聚变的信号枪,正中西部超导靶心!11月12日,合肥物质院等离子体所密集发布了13个亿的氚工厂招...