什么是堆?

堆其实就是一种特殊的队列——优先队列。

普通的队列游戏规则很简单:就是先进先出;但这种优先队列搞特殊,不是按照进队列的时间顺序,而是按照每个元素的优先级来比拼,优先级高的在堆顶。

这也很容易理解吧,比如各种软件都有会员制度,某软件用了会员就能加速下载的,不同等级的会员速度还不一样,那就是优先级不同呀。

还有其实每个人回复微信消息也是默默的把消息放进堆里排个序:先回男朋友女朋友的,然后再回其他人的。

这里要区别于操作系统里的那个“堆”,这两个虽然都叫堆,但是没有半毛钱关系,都是借用了 Heap 这个英文单词而已。

我们再来回顾一下「堆」在整个 Java 集合框架中的位置:

也就是说,PriorityQueue 是一个类 (class);

PriorityQueue 继承自 Queue 这个接口 (Interface);

那 heap 在哪呢?

heap 其实是一个抽象的数据结构,或者说是逻辑上的数据结构,并不是一个物理上真实存在的数据结构。heap 其实有很多种实现方式,比如 binomial heap, Fibonacci heap 等等。但是面试最常考的,也是最经典的,就是 binary heap 二叉堆,也就是用一棵完全二叉树来实现的。

那完全二叉树是怎么实现的?

其实是用数组来实现的!所以 binaryheap/PriorityQueue 实际上是用数组来实现的。这个数组的排列方式有点特别,因为它总会维护你定义的(或者默认的)优先级最高的元素在数组的首位,所以不是随便一个数组都叫「堆」,实际上,它在你心里,应该是一棵「完全二叉树」。这棵完全二叉树,只存在你心里和各大书本上;实际在在内存里,哪有什么树?就是数组罢了。那为什么完全二叉树可以用数组来实现?是不是所有的树都能用数组来实现?这个就涉及完全二叉树的性质了,我们下一篇会细讲,简单来说,因为完全二叉树的定义要求了它在层序遍历的时候没有气泡,也就是连续存储的,所以可以用数组来存放;第二个问题当然是否。

堆的特点堆是一棵完全二叉树;

堆序性 (heap order): 任意节点都优于它的所有孩子。a. 如果是任意节点都大于它的所有孩子,这样的堆叫大顶堆,Max Heap;b. 如果是任意节点都小于它的所有孩子,这样的堆叫小顶堆,Min Heap;左图是小顶堆,可以看出对于每个节点来说,都是小于它的所有孩子的,注意是所有孩子,包括孙子,曾孙...既然堆是用数组来实现的,那么我们可以找到每个节点和它的父母/孩子之间的关系,从而可以直接访问到它们。比如对于节点 3 来说,它的 Index = 1,

它的 parent index = 0,

左孩子 left child index = 3,

右孩子 right child index = 4.可以归纳出如下规律:设当前节点的 index = x,

那么 parent index = (x-1)/2,

左孩子 left child index = 2*x + 1,

右孩子 right child index = 2*x + 2.有些书上可能写法稍有不同,是因为它们的数组是从 1 开始的,而我这里数组的下标是从 0 开始的,都是可以的。这样就可以从任意一个点,一步找到它的孙子、曾孙子,真的太方便了,在后文讲具体操作时大家可以更深刻的体会到。

基本操作任何一个数据结构,无非就是增删改查四大类:功能方法时间复杂度增offer(E e)O(logn)

删poll()O(logn)

改无直接的 API删 + 增

查peek()O(1)这里peek()的时间复杂度很好理解,因为堆的用途就是能够快速的拿到一组数据里的最大/最小值,所以这一步的时间复杂度一定是O(1)的,这就是堆的意义所在。那么我们具体来看offer(E e)和poll()的过程。

offer(E e)比如我们新加一个0到刚才这个最小堆里面:那很明显,0 是要放在最上面的,可是,直接放上去就不是一棵完全二叉树了啊。。所以说,我们先保证加了元素之后这棵树还是一棵完全二叉树,

然后再通过 swap 的方式进行微调,来满足堆序性。这样就保证满足了堆的两个特点,也就是保证了加入新元素之后它还是个堆。那具体怎么做呢:

Step 1.先把 0 放在最后接上,别一上来就想着上位;OK!总算先上岸了,然后我们再一步步往上走。这里「能否往上走」的标准在于:

是否满足堆序性。也就是说,现在 5 和 0 之间不满足堆序性,那么交换位置,换到直到满足堆序性为止。这里对于最小堆来说的堆序性,就是小的数要在上面。

Step 2. 与 5 交换此时 0 和 3 不满足堆序性了,那么再交换。

Step 3. 与 3 交换还不行,0 还比 1 小,所以继续换。

Step 4. 与 1 交换OK!这样就换好了,一个新的堆诞生了~总结一下这个方法:先把新元素加入数组的末尾,再通过不断比较与 parent 的值的大小,决定是否交换,直到满足堆序性为止。这个过程就是siftUp(),源码如下:

时间复杂度这里不难发现,其实我们只交换了一条支路上的元素,也就是最多交换O(height)次。那么对于完全二叉树来说,除了最后一层都是满的,O(height) = O(logn)。所以offer(E e)的时间复杂度就是O(logn)啦。

poll()poll()就是把最顶端的元素拿走。对了,没有办法拿走中间的元素,毕竟要 VIP 先出去,小弟才能出去。那么最顶端元素拿走后,这个位置就空了:我们还是先来满足堆序性,因为比较容易满足嘛,直接从最后面拿一个来补上就好了,先放个傀儡上来。

Step1. 末尾元素上位这样一来,堆序性又不满足了,开始交换元素。那 8 比 7 和 3 都大,应该和谁交换呢?假设与 7 交换,那么 7 还是比 3 大,还得 7 和 3 换,麻烦。所以是与左右孩子中较小的那个交换。

Step 2. 与 3 交换下去之后,还比 5 和 4 大,那再和 4 换一下。

Step 3. 与 4 交换OK!这样这棵树总算是稳定了。总结一下这个方法:先把数组的末位元素加到顶端,再通过不断比较与左右孩子的值的大小,决定是否交换,直到满足堆序性为止。这个过程就是siftDown(),源码如下:

时间复杂度同样道理,也只交换了一条支路上的元素,也就是最多交换O(height)次。所以offer(E e)的时间复杂度就是O(logn)啦。

heapify()还有一个大名鼎鼎的非常重要的操作,就是heapify()了,它是一个很神奇的操作,

可以用O(n)的时间把一个乱序的数组变成一个 heap。但是呢,heapify()并不是一个 public API,看:所以我们没有办法直接使用。唯一使用heapify()的方式呢,就是使用PriorityQueue(Collection extends E> c)这个 constructor 的时候,人家会自动调用 heapify() 这个操作。

那具体是怎么做的呢?

哈哈源码已经暴露了:

从最后一个非叶子节点开始,从后往前做siftDown().因为叶子节点没必要操作嘛,已经到了最下面了,还能和谁 swap?举个例子:我们想把这个数组进行heapify()操作,想把它变成一个最小堆,拿到它的最小值。那就要从 3 开始,对 3,7,5进行siftDown().

Step 1.尴尬 ?,3 并不用交换,因为以它为顶点的这棵小树已经满足了堆序性。

Step 2.7 比它的两个孩子都要大,所以和较小的那个交换一下。交换完成后;

Step 3.最后一个要处理的就是 5 了,那这里 5 比它的两个孩子都要大,所以也和较小的那个交换一下。换完之后结果如下,注意并没有满足堆序性,因为 4 还比 5 小呢。所以接着和 4 换,结果如下:这样整个heapify()的过程就完成了。

好了难点来了,为什么时间复杂度是 O(n) 的呢?怎么计算这个时间复杂度呢?其实我们在这个过程里做的操作无非就是交换交换。那到底交换了多少次呢?没错,交换了多少次,时间复杂度就是多少。那我们可以看出来,其实同一层的节点最多交换的次数都是相同的。那么这个总的交换次数 = 每层的节点数 * 每个节点最多交换的次数这里设 k 为层数,那么这个例子里 k=3.每层的节点数是从上到下以指数增长:每个节点交换的次数,从下往上就是:那么总的交换次数 S(k) 就是两者相乘再相加:这是一个等比等差数列,标准的求和方式就是错位相减法。那么两者相减得:化简一下:(不好意思我实在受不了这个编辑器了。。。所以heapify()时间复杂度是O(n).以上就是堆的三大重要操作,最后一个heapify()虽然不能直接操作,但是堆排序中用到了这种思路。有道无术,术可成;有术无道,止于术

欢迎大家关注Java之道公众号

好文章,我在看❤️

java堆与非堆的一些研究_堆和堆傻傻分不清?一文告诉你 Java 集合中「堆」的最佳打开方式...相关推荐

  1. 堆和堆傻傻分不清?一文告诉你 Java 集合中「堆」的最佳打开方式

    上一篇的 「Java 集合框架」里,还剩下一个大问题没有说的,那就是 PriorityQueue,优先队列,也就是堆,Heap. 什么是堆? 堆其实就是一种特殊的队列--优先队列. 普通的队列游戏规则 ...

  2. JAVA数组首位末位互换_堆和堆傻傻分不清?一文告诉你 Java 集合中「堆」的最佳打开方式...

    上一篇的 「Java 集合框架」里,还剩下一个大问题没有说的,那就是 PriorityQueue,优先队列,也就是堆,Heap. 什么是堆? 堆其实就是一种特殊的队列--优先队列. 普通的队列游戏规则 ...

  3. 一文告诉你 Java RMI 和 RPC 的区别

    转载自  一文告诉你 Java RMI 和 RPC 的区别 RPC 远程过程调用 RPC(Remote Procedure Call Protocol)远程过程调用协议,通过网络从远程计算机上请求调用 ...

  4. 混沌序列 java,基于Logistic映射混沌加密算法的研究_韩凤英

    第7卷第1期长沙航空职业技术学院学报 Vo1.7No .1 2007年3月 CHANGS HA AERO NAUT I CAL VOCATI ONAL AND TECHN I CAL COLLEGE ...

  5. 一文告诉你Java日期时间API到底有多烂

    前言 你好,我是A哥(YourBatman). 好看的代码,千篇一律!难看的代码,卧槽卧槽~其实没有什么代码是"史上最烂"的,要有也只有"史上更烂". 日期是商 ...

  6. java 寻找和为定值的多个数_算法笔记_037:寻找和为定值的两个数(Java)

    1 问题描述 输入一个整数数组和一个整数,在数组中查找两个数,满足他们的和正好是输入的那个整数.如果有多对数的和等于输入的整数,输出任意一对即可.例如,如果输入数组[1,2,4,5,7,11,15]和 ...

  7. 一文告诉你 Java RMI 和 RPC 的区别!

    RPC 远程过程调用 RPC(Remote Procedure Call Protocol)远程过程调用协议,通过网络从远程计算机上请求调用某种服务. 一次RPC调用的过程大概有10步: 1.执行客户 ...

  8. java浮点数数转二进制的数吗_深入理解计算机系统(2.7)-二进制小数和IEEE浮点标准 - Java 技术驿站-Java 技术驿站...

    整数的表示和运算我们已经讲完了,在实际应用中,整数能够解决我们大部分问题.但是某些需要精确表示的数,比如某件商品的价格,某两地之间的距离等等,我们如果用整数表示将会有很大的出入,这时候浮点数就产生了. ...

  9. java属于高级语言_一文告诉你java是高级语言吗?

    很多刚接触java的朋友可能会疑惑,java是高级语言吗? 可以肯定的说,Java语言是高级语言.Java和C都是计算机的高级语言.相对低级语言来说,高级语言更接近人类的思维方式,更容易理解和掌握,用 ...

最新文章

  1. Websphere设备、企业部署应用程序 【应用】
  2. LeetCode Longest Substring with At Least K Repeating Characters(递归)
  3. 解决逆向工程mapper映射文件不发布问题
  4. 深度残差网络和Highway网络
  5. 微软全都要!Win10引入真Linux内核
  6. POJ2411-Mondriaan's Dream【状态压缩dp】
  7. css 列表属性详细总结
  8. 17年,寻找出路的一年
  9. java字段映射替换框架一对多_java之mybatis之字段映射及多对一
  10. Spring异步切面源码解析
  11. J2ME模拟器加载RMS时突然失效的原因
  12. linux c编译 utf-8,在Linux C编程中使用Unicode和UTF-8
  13. flash builder 4 序列号
  14. Pascal 转 C++ 教程1
  15. 洛谷P2736 “破锣摇滚”乐队 Raucous Rockers
  16. 游戏内容安全:运营如何筛查和辨别黑灰产
  17. 能不能算是PLSQL Developer的锅?
  18. 非诚勿扰:比舒淇更孤单的是谁?
  19. java 循环详解_Java for循环详解
  20. 0范数,1范数,2范数

热门文章

  1. xgboost防止过拟合
  2. 机器学习笔记:高斯过程
  3. Linux疑难杂症解决方案100篇(十五)-万字长文带你深入Linux 内核学习:环境搭建和内核编译
  4. MATLAB可视化实战系列(二十四)-三维可视化如何利用圆锥图显示向量场?
  5. 拉格朗日插值法的MATLAB源程序
  6. 特征选择常用算法综述
  7. Python输出py文件模拟代码高亮
  8. 启动ipython出错_python-在异常情况下启动IPython shell
  9. python能解密java的_实现Java加密,Python解密的RSA非对称加密算法功能
  10. python数据结构的列表_Python数据结构之列表