二叉堆与优先级队列学习

今天准备学习优先级阻塞队列PriorityBlockingQueue,但是它是用二叉堆实现的,所以必须先学习二叉堆。

二叉堆详解

二叉堆就结构性质上说就是一个完全填满的二叉树,满足结构性和堆序性。结构性就是完全二叉树应该满足的树结构。而堆序性指的是:父节点的键值总是大于或等于(小于或等于)任何一个子节点的键值且每个节点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆),根据堆序性二叉堆分成两种堆:

最大堆:当父节点的键值总是大于或等于任何一个子节点的键值。

最小堆:当父节点的键值总是小于或等于任何一个子节点的键值。

PriorityBlockingQueue是使用最小堆实现,所以这里以最小堆为例进行说明。由于堆要保证堆序性,所以在插入、弹出数据时必须要做一些操作保证堆的堆序性,那么是如何保证的呢?

在插入的时候通过上浮,在删除的时候通过下沉来实现,说起来比较空洞,我画了一个图来解释这个过程,图流程比较长,分4次插入2次删除,详情如下图:


最开始是一个最简单的二叉树只有三个节点两层。

当新增一个节点15,他在堆的最后,挂在第三层最左侧,父节点是25。所谓的上浮就是新增节点放到最后,然后与父节点比较,它比父节点小则和父节点交换位置,然后再和新的父级比较,直到顶级或者父级比它小结束

所以15会和25交换位置,然后发现他的上级20还是比他大继续交换,上浮到顶级就没有父节点,就完成了上浮。这样就保证了最小的在最上面

删除节点则是从顶级弹出,也就是弹出最小值,弹出后顶级节点就变成了空节点,我们就把最后那个节点尾结点取出来,去和顶级节点的两个子节点中较小的比较,如果较小的节点小于尾结点则上浮(顶级节点变成新的最小节点),然后再次用小节点的两个子节点中较小节点和尾节点比较,如此往下递归下去,直到尾结点小于较小子节点,那么就把尾结点设置到当前节点,这就叫下沉。

通过新增节点时上浮,删除节点的下沉永远保证堆顶是最小值,并且每个节点的左子树和右子树都是一个最小堆。

二叉堆实现思考

明白了二叉堆的运行方式,接下来的重点就是如何实现了。从上一步上浮、下沉的分析中可以看到,最重要的是找到节点的子节点和父节点。也就是节点与节点之间的关系,那么如何实现呢?

如果把每个节点标记一个位置索引,顶节点为0,第二层第一个为1,第二层第二个为3,依次从上往下从左往右递增标记他们的位置(上图中最大堆已标出),可以发现一个特点:节点的左侧子节点索引是当前节点索引的2倍再加1,右侧子节点肯定是再加1。反之要找到父节点则是当前节点所以减1在除于2(取整)

根据以上总结的特点,我们就可以把二叉堆的数据按照顺序放到数组中,那么在数组n处节点的子节点位置分别是2n+1、2n+2,父节点的位置为n/2

PriorityBlockingQueue实现二叉堆

如果通过上一步的总结我们就能够通过算法实现二叉堆了。那么我们直接来看Java中PriorityBlockingQueue对二叉堆的实现,关键代码如下图:


阻塞队列的功能都差不多,这里就不再介绍了,take方法还是依赖的是dequeue方法,dequeue方法比较简单,就是把数组第0个元素获取出来返回,不过要先把数组第n(二叉树数量长度)个取出来,然后数组n处置为null(从二叉树中移除)。

然后调用siftDownComparable方法把获取到的节点x与顶级节点较小子节点e比较,如果x大于e,则把e升级到父节点位置(此时e的父级为顶级节点或者已经审计,可以覆盖),然后继续去比较e的较小节点,直到x小于e节点则停止,把e设置到当前位置。

put、add、offer等方法依赖offer方法,offer方法会先拿到二叉树的数量n作为siftUpComparable方法的参数k,所以k代表的是二叉树的尾节点在数组中为索引。

找到k节点的父节点(k-1)>>>1)与要添加的节点x比较,如果x小于父节点父节点就下沉,然后继续验证父节点的父节点,循环直到x大于父节点就停止上浮。

PriorityBlockingQueue其他注意点

讲了PriorityBlockingQueue利用二叉树实现优先功能,还是要记录一些其他需要注意的地方;

首先我们知道底层实现是数组,然后在不知道初始化长度的时候数组默认长度是11,在offer方法中如果判断二叉树数据数量等于数组长度,也就是数组满了会调用tryGrow方法扩容,当数组长度小于64时每次扩容是当前长度再加2,当大于64后每次扩容当前长度的一半。最大长度是(Integer.MAX_VALUE - 8);

PriorityBlockingQueue中保存的数据必须实现Comparable接口,否则没法比较

总结

二叉树是很重要的数据结构,通过一个完整的二叉树添加删除,已经PriorityBlockingQueue中的实现,希望能够真正的理解。

综合上一篇文字可以看出,无界阻塞队列并不是真正的无界,只是上限设置的很大,基本不可能达到。

Java程序员日常学习笔记,如理解有误欢迎各位交流讨论!


你可能感兴趣的:(二叉堆与优先级队列学习)