内容简介:前面一片文章提过,完全二叉树非常适合用数组这种数据结构来实现。所以堆作为一个完全二叉树肯定用数组来实现最合适。而且规律也很简单,我们再总结一遍就是:如果一个节点的下标为i,那么他的左子节点的下标就是2i,右子节点的下标就是2i+1
前面一片文章提过,完全二叉树非常适合用数组这种数据结构来实现。所以堆作为一个完全二叉树肯定用数组来实现最合适。
而且规律也很简单,我们再总结一遍就是:
如果一个节点的下标为i,那么他的左子节点的下标就是2i,右子节点的下标就是2i+1
父节点就是i/2.
还不理解上述 完全二叉树---数组 这种映射关系的 可以看前一篇文章或者自己画一画。
那实现一个堆无非就是要完成对这个堆的 插入数据和删除数据操作。
插入算法
插入操作真的蛮简单的,我们可以看一下刚才那张图,以这个大顶堆为例。 假设我们插入一个值,我们先把这个值 插入在这个 堆的 末尾位置,然后 将这个值 与他的父节点对比,如果比父节点大 那么就交换位置,如果比父节点小 那么就插入完毕。
比方说我们对大顶堆插入一个10的数据,
可以看出来 这个 自下而上 的插入算法还是很好理解的。
删除堆顶元素算法
根据大顶堆和小顶堆的定义可以得知,堆顶元素永远都是最大或者最小的。那么删除堆顶元素以后,新的堆顶元素的值 自然就是 原来堆顶元素的左儿子和右儿子了。
所以显然我们天真的以为删除堆顶元素的算法应该是(假设是大顶堆):
删除堆顶元素以后,我们将左儿子的值和右儿子的值比较,谁的值比较大,那么就去堆顶呆着。 等儿子去了堆顶位置以后,
儿子的儿子也是一样进行如上操作 看谁大就往上挪一个位置。 看起来这个算法无懈可击也好理解,但是这种算法容易
出现下述异常情况。
所以看的出来这里的算法会导致某种情况下 堆的完全二叉树被破坏掉。
所以我们要将其改进一下,改进后的算法如下:
要删除堆顶元素的时候,先删除堆顶元素然后把堆的最后一个位置的数据放到堆顶,然后自上而下的比对大小,交换位置。
我们来看看,这种算法的图解:
显然这种算法 就会避免第一种算法带来的数组空洞问题。
传说中的堆 排序 就是用这个 堆来实现的吗,具体怎么做的?
是的,堆排序中的堆就是用这个堆来实现数据结构的。实现起来稍微复杂了一丢丢,简单说一下吧(不详细说是因为堆排序实在没啥人用):
给定一个无序的数组,问,如何用堆这种数据结构来进行排序?
首选把这个无序数组进行建堆的操作,前面说过了插入一个数据的算法,那么对于这个无序数组来说,我们可以假定数组的第一个元素就是堆顶的位置,然后把后面的元素依次按照我们的插入算法 插入即可。(实际上有更加友好的建堆方法,但这不是今天重点,有兴趣的同学可以自己搜索一下)
有了这个堆以后,如何进行排序呢? 假设是大顶堆,其实对这个堆进行排序就好像我们的删除堆顶元素的算法, 你删了一个堆顶元素以后,第二打的元素不就到对顶了么?以此类推。。。
堆排序为啥没人用呢?
实际上堆排序性能看起来“似乎还可以”,具体时间复杂度和快排序是一样的,时间复杂度都是O(nlogn),而且还是稳定排序算法。 但是这东西有个致命的缺陷:
堆的操作太多依赖于交换操作,看起来时间复杂度和快排序一样,但是因为你交换次数太多了,所以实际表现远远不如快排序。
就好比举重比赛一样,大家都是一样的成绩,你比我重一点,当然冠军是我。
罗嗦半天堆排序这么垃圾那你还讲他干嘛?
天生我才必有用啊,兄弟,堆这东西 干纯排序不行,其他东西可是有妙用的。
优先级队列 几乎就等于 “堆”
所谓优先级队列就是,不管进入的顺序如何,出去的顺序 是按照优先级来的,仔细想想是不是和我们的大顶堆或者小顶堆的定义 无限趋于一致?熟悉android看过volley源码的同学应该都知道,volley里面的PriorityBlockingQueue 不就是一个线程安全的 用堆这种数据结构来构成的优先级队列么?
再来看经典的TOP K 问题
有了上述基础,我们再来看TOP K问题就简单许多了。leetcode 703 题: leetcode-cn.com/problems/kt…
简单来说,就是给定你一个数组,每次插入数据的时候,要你返回这个数组里面第K大的元素。
下面来说说这道问题的解法思路:
1。 数组中每次插入一个数据的时候,我们都进行快排序,然后取出第k个大小就可以。这是思路上最简单但是最蠢的方法。 因为每次插入一个数据都要全部排序 那也太慢了。
- 在知道有优先级队列这么个东西以后,我们就可以维持一个 k大小的 小顶堆,每次有新元素进来的时候 我们就看看这个新元素 的值 是不是比这个小顶堆的值要大,如果比他大 那么就把这个小顶堆的堆顶元素删掉,然后插入我们这个新元素的值以后, 再取这个小顶堆的堆顶元素 就是我们的第k大元素了。
class KthLargest {
PriorityQueue<Integer> queue;
int maxHeapSize;
public KthLargest(int k, int[] nums) {
//初始化一个大小为k的小顶堆
queue = new PriorityQueue<Integer>(k);
maxHeapSize=k;
if (k < nums.length) {
//先构造一个大小为k的 堆
for (int i = 0; i < k; i++) {
queue.add(nums[i]);
}
//下面构造的时候要判断大小
for (int j = k; j < nums.length; j++) {
if (nums[j] > queue.peek()) {
queue.poll();
queue.offer(nums[j]);
} else {
}
}
} else {
for (Integer integer : nums) {
queue.add(integer);
}
}
}
public int add(int val) {
if(queue.size()<maxHeapSize)
{
queue.offer(val);
return queue.peek();
}
if (val > queue.peek()) {
queue.poll();
queue.offer(val);
}
return queue.peek();
}
}
/**
* Your KthLargest object will be instantiated and called as such:
* KthLargest obj = new KthLargest(k, nums);
* int param_1 = obj.add(val);
*/
复制代码
看下效果:
嗯 还可以。
然后最后看下leetcode 239问题 leetcode-cn.com/problems/sl…
有了优先级队列以后这个问题也变的简单了吧。只要遍历数组的时候维护一个大顶堆,问题就迎刃而解了。(代码就不上了,很简单)
当然其实这个问题还有更好的解法,就是不用堆,而用双端队列,有兴趣的同学可以研究下。
堆或者说优先级队列还能解决什么实际生产问题吗?
其实能解决的生产问题很多,比方说,我们做Android定制系统的,假设用户搞了N个任务在那里,每个任务都有特定的执行时间, 到了时间就要执行用户的任务,比如说闹钟。那我们怎么完成这个需求呢?
每过一秒就检查 任务列表 有没有时间跟当前时间符合的吗?显然不行,这样效率太低。
我们可以把这些任务存储成为一个小顶堆,这样只要系统时间过一秒,我们就看看是不是和堆顶元素时间一样即可。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 【1】JavaScript 基础深入——数据类型深入理解与总结
- 深入理解java虚拟机(1) -- 理解HotSpot内存区域
- 深入理解 HTTPS
- 深入理解 HTTPS
- 深入理解 SecurityConfigurer
- 深入理解 HTTP 协议
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Mastering Regular Expressions, Second Edition
Jeffrey E F Friedl / O'Reilly Media / 2002-07-15 / USD 39.95
Regular expressions are an extremely powerful tool for manipulating text and data. They have spread like wildfire in recent years, now offered as standard features in Perl, Java, VB.NET and C# (and an......一起来看看 《Mastering Regular Expressions, Second Edition》 这本书的介绍吧!