优先队列是容许至少下列两种操做的数据结构:insert 以及 deleteMin(找出、返回并删除优先队列中最小的元素)。node
insert 操做等价于 enqueue(入队),而 deleteMin 则是运算 dequeue(出队)在优先队列中的等价操做。算法
可使用简单链表进行不排序的插入,则插入操做为 O(1),可是删除须要遍历链表为 O(N)。
另外一种方法是让链表保持排序状态:插入代价高昂 O(N),但删除为 O(1).可是 deleteMin 的操做比插入操做少,前者可能更好。数组
另一种方法是使用二叉查找树,它对这两种操做的平均运行时间都为 O(log N)。
可是,因为咱们删除的惟一元素是最小元,反复出去左子树的节点会损害树的平衡使得右子树加剧,在最坏状况下 deleteMin 将左子树删空。数据结构
另外,使用查找树有不少咱们数据结构不须要的链。eclipse
咱们将要使用的数据结构叫作二叉堆(binary heap)
,它的使用对于优先队列的实现至关广泛,以致于堆(heap)这个词不加修饰的用在优先队列的上下文中时,通常都是指数据结构的这种实现。函数
二叉堆有两个性质:结构性和堆序性。ui
堆是一棵被彻底填满的二叉树,有可能的例外是在底层,底层上的元素从左到右填入
。这样的树被称为彻底二叉树(complete binary tree)。this
一棵高为 h 的彻底二叉树有 2h 到 2h+1 - 1 个节点。它的高度为 Log N,显然它是 O(log N)。spa
由于二叉堆是满二叉树,因此在高度为 h-1 的树包含
20 + ... + 2h-1 = 2h -1 个元素,
在高度为 h 的层上有 1 至 2h 个元素,因此应该有 2h 至 2h+1 - 1 个元素。设计
由于彻底二叉树这么有规律,因此它能够用一个数组表示而不须要使用链。
对于数组中任意位置 i 上的元素,其左儿子在位置 2i 上,右儿子在左儿子后的单元 (2i + 1)上。它的父亲则在位置 [i / 2] 上。
让操做快速执行的性质是堆序性质(heap-order property)
。因为咱们想要快速找出最小元,所以最小的元应该在根上。若是咱们考虑任意子树也应该是一个堆,那么任意节点就给应该小于它的全部后裔。
为了将一个元素 X 插入到堆中,咱们在下一个可用位置建立一个空穴,不然该堆将不是彻底树。若是 X 能够放在该空穴中而并不破坏对的序,那么插入完成。
不然,咱们把空穴的父节点移入该空穴中,这样空穴就朝着根的方向上冒一步。继续该过程直到 X 可以放入空穴中为止。
以下图所示:为了插入 14,咱们在堆的下一个可用位置上创建一个空穴,因为将 14 插入空穴破坏了堆序性质,所以将 31 移入该空穴。
在图中,将元素从作到右执行插入,因此下一个空穴的位置应该在 31 的右节点上
。
当删除一个最小元是,要在根节点创建一个空穴。因为如今少了一个元素,所以堆中最后一个元素 X 必须移动带该堆的某个地方。<u>这是为了知足二叉堆的结构性质 -- 它是一棵彻底二叉树,空穴只能在最后一层的最后一个元素以后</u>
所以,咱们将空穴的两个儿子中较小者移入空穴这样就把空穴向下推了一层。
重复该步骤直到 X 能够被放入空穴中。
例如,对于下面的例子中,咱们先删除元素 13,这将在二叉堆的根节点上创建一个空穴。随后往里面插入数字 31.
在堆的实现中常常发生的错误是当堆中存在偶数个元素的时候,可能会遇到某个节点只有一个子节点的状况(只会在最下层出现)。
package com.mosby.ch06; /** * @author dhy */ public class BinaryHeap <T extends Comparable<? super T>> { public BinaryHeap(){ this(DEFAULT_CAPACITY); } public BinaryHeap(int capacity){ currentSize = 0; array = new Comparable[capacity]; } /** * 向堆插入一个元素<br><br> * * <blockquote> * * 在这里咱们的代码使用了一个小技巧:咱们如今的目的是要将当前堆中的空穴(初始为数组中最后一个元素以后) * 移动到一个知足将 X 插入该空穴后不影响堆的性质的位置。<br><br> * * 若是咱们每次都将当前空穴的位置和它的父元素交换,那么对于一个元素上滤 d 层, * 那么因为交换而执行的赋值次数就是 3d。<br><br> * * 而这里,咱们每次只是在知足条件时将父节点的值赋给了这个空穴而没有将空穴的值上滤。<br> * 这样上滤 d 层将只须要 d 次对空穴的赋值和一次最后将 X 插入的赋值。总共 d+1 次赋值。 * * </blockquote> * * @param x */ public void insert(T x){ //由于堆内部的数组实现的第一个元素是空 if(currentSize == array.length - 1){ enlargeArray(array.length * 2 + 1); } //当前空穴的位置在最后一个元素的后一位,同时插入空穴以后 currentSize 增长一。等同于下面的代码 //int hole = currentSize + 1; //currentSize++; int hole = ++currentSize; for(; hole > 1 && x.compareTo((T) array[hole / 2]) < 0; hole /= 2){ array[hole] = array[hole / 2]; } array[hole] = x; } public T findMin(){ if(isEmpty()){ return null; } return (T) array[1]; } public T deleteMin(){ if(isEmpty()){ throw new RuntimeException("Under flow"); } T minItem = findMin(); array[1] = array[currentSize--]; percolateDown(1); return minItem; } public boolean isEmpty(){ return currentSize == 0; } public void makeEmpty(){ currentSize = 0; } private static final int DEFAULT_CAPACITY = 100; private int currentSize;//当前堆中元素个数 private Comparable<? super T>[] array;//堆内部的以数组的形式存放 /** * 对空穴进行下滤 * @param hole */ private void percolateDown(int hole){ //这里 child 的初值不会影响程序的正确性,可是 eclipse 的编译器有 bug, int child; 是没法经过编译的 //在 IDEA 下能够直接 int child; int child = hole * 2; Comparable<? super T> tmp = array[hole]; /** * 这里注意一点,hole * 2 <= currentSize,由于数组的第一个元素为空<br> * 数组中的实际元素应该是 array[i] - array[currentSize] */ for(; hole * 2 <= currentSize; hole = child){ child = hole * 2; /** * 在下滤的过程当中,咱们每次将当前节点的两个子节点中较小的那个子节点跟空穴交换<br> * * 可是这必需要考虑一个问题,在最下层的时候,可能会有某个节点只有一个子节点<br> * * 而在非最下层则不会有这个问题,由于二叉堆是一个彻底二叉树。<br> * * 而根据二叉堆的插入性质(从左往右插入),那么只有一个元素的节点,这个元素的子节点确定 * 就是二叉堆的最后一个节点。此时 hole == currentSize. */ if(child != currentSize && array[child + 1].compareTo((T) array[child]) < 0){ child++; } if(array[child].compareTo((T) tmp) < 0){ array[hole] = array[child]; }else{ break; } } array[hole] = tmp; } /** * 若是提供了经过数组初始化二叉堆的方式,那么在传入一个数组后调用该方法便可获得一个二叉堆。 */ private void buildHeap(){ for(int i = currentSize / 2; i > 0; i--){ percolateDown(i); } } public void enlargeArray(int newSize){ Comparable[] newArray = new Comparable[newSize]; for(int i = 1; i <= currentSize; i++){ newArray[i] = array[i]; } array = newArray; } public int size(){ return currentSize; } }
设计一种堆结构像二叉堆那样有效的支持合并操做(即以 O(N) 时间处理一个 merge)并且只使用一个数组彷佛很困难。缘由在于,合并彷佛须要把一个数组拷贝到另一个数组中去。
<u>正由于如此,全部支持有效合并的高级数据结构都须要使用链式数据结构</u>
左式堆 (leftist heap)
像二叉堆同样具备结构性和有序性。左式堆也是二叉树,左式堆和二叉堆的惟一区别是:左式堆不是理想平衡(perfectly balanced),而实际上趋向于很是不平衡。
咱们把任意节点 X 的零路径长(null path length) npl(X) 定义为从 X 到一个不具备两个儿子的节点的最短路径长。所以,具备 0 个或一个儿子的节点的 npl 为 0,而 npl(null) = -1。
任意节点的零路径长比它的全部儿子节点的零路径长的最小值
大1.这个结论也适用于少于两个儿子的节点,由于 null 的零路径长是 -1.
左式堆的性质是:对于堆中的
每个节点 X
,左儿子的零路径长大于等于右儿子的零路径长。
实际上,对于左式堆的任意一个节点只能有三种状况,有两个子节点、没有子节点、仅有一个节点且该节点为左子节点。
也就是说,若是存在任意一个节点只有右节点,那么这个堆必定不是左式堆。可是,若是一个节点每一个节点都知足上面的条件,它不必定是左式堆,还须要知足零路径长的条件。
显然,在上路中,左图是一棵左式堆;而右图则不是,由于右图的根节点的左子节点的左子节点的零路径长 == 0,而根节点的左子节点的右子节点的零路径长 == 1.
这个性质使得它不是一棵理想平衡树,由于它显然偏重于使树向左增长深度。
由于左式堆趋向于加深左路径,因此右路径应该短。事实上,沿左式堆的右路径是该堆中的最短路径。不然,就会存在某个节点 X 的左儿子的最短路径长小于右儿子的最短路径长。
node / \ node `node` / \ / \ node node `null` node
例如,对于上面这个树,对于标记的 node 节点是不符合左式堆的,由于它的左子节点的零路径长是 -1,而右子节点的零路径长是 0.
左式堆的基本操做是合并。注意,插入只是合并的特殊性状况。
3 | 6 | / \ | / \ | 10 8 | 12 7 | / \ / | / \ / \ | 21 14 17 | 18 24 37 18 | / / | / | 23 26 | 33 |
合并具备大的 root 的堆与具备较小的 root 的堆的右节点
函数栈最上层 | 6 | | / \ | 8 | 12 7 | / | / \ / \ | 17 | 18 24 37 18 | / | / | 26 | 33 |
函数栈的底层,该层等待上层函数的返回 3 | / | 10 | / \ | 21 14 | / | 23 |
递归的去进行 merge 操做
函数栈最上层 8 | 7 | / | / \ | 17 | 37 18 | / | | 26 | |
函数栈第二层 | 6 | | / | | 12 | | / \ | | 18 24 | | / | | 33 |
函数栈的底层,该层等待上层函数的返回 3 | / | 10 | / \ | 21 14 | / | 23 |
继续进行递归 merge
函数栈最上层 8 | | / | | 17 | 18 | / | | 26 | |
函数栈第二层 | 7 | | / | | 37 | | | | |
函数栈第三层 | 6 | | / | | 12 | | / \ | | 18 24 | | / | | 33 |
函数栈的底层,该层等待上层函数的返回 3 | / | 10 | / \ | 21 14 | / | 23 |
继续进行递归 merge
函数栈最上层,这个时候函数栈开始退出 null | 18 |
函数栈最二层 8 | | / | | 17 | | / | | 26 | |
函数栈第三层 | 7 | | / | | 37 | | | | |
函数栈第四层 | 6 | | / | | 12 | | / \ | | 18 24 | | / | | 33 |
函数栈的底层,该层等待上层函数的返回 3 | / | 10 | / \ | 21 14 | / | 23 |
函数栈开始退出
函数栈最上层,上层函数退出,同时必须更新 root 节点的 npl 8 | | / \ | | 17 18 | | / | | 26 | |
函数栈第二层 | 7 | | / | | 37 | | | | |
函数栈第三层 | 6 | | / | | 12 | | / \ | | 18 24 | | / | | 33 |
函数栈的底层,该层等待上层函数的返回 3 | / | 10 | / \ | 21 14 | / | 23 |
函数栈继续退出,同时若是root左子树的零路径长小于右子树的零路径长则必须翻转两个子树
函数栈最上层,上层函数退出,同时必须更新 root 节点的 npl 7 / \ 8 37 | | / \ | | 17 18 | | / | | 26 | |
函数栈第二层 | 6 | | / | | 12 | | / \ | | 18 24 | | / | | 33 |
函数栈的底层,该层等待上层函数的返回 3 | / | 10 | / \ | 21 14 | / | 23 |
最后获得的结果为 图 6-24 所示。
递归的退出条件是
:
被 merge 的两个左式堆中任意一个为 null,则返回另外一个;
两个左式堆中那么具备较小 root 节点的左子节点为 null 时,将具备较大 root 的节点做为具备较小 root 的节点的左子节点,并返回具备较小 root 的几点。这里隐含了一个信息:当一个左式堆的左子节点为 null 时,它的右子节点一定为 null。由于若是右子节点不为 null,那么它就不知足左式堆的条件了。
若是这两个堆中有一个为空,那么咱们能够返回另一个堆。
不然合并他们:
首先,咱们递归的将具备大的 root 的堆与具备小的 root的堆的右子堆合并。咱们在递归算法中须要保证递归获得的这棵树是左式堆。
为何这里是合并较大 root 的堆和较小 root 的堆的右子堆呢?
由于,咱们合并出来的这个堆须要作为原来那个堆的右子堆,而根据左式堆的性质,一个节点全部的子节点都必须大于该节点。
图 6-23 获得的不是左式堆。左式的性质在根处被破坏。
在咱们步骤 1. 中获得的新的子树是左式堆,而右子树自己就是左式堆,因此这棵树是否是知足左式堆,只要左子树的零路径长大于新的右子树的零路径长便可。
若是不知足,咱们只须要将左子树和右子树的节点交换并更新零路径长就能够了。
package com.mosby.ch06; /** * 左式堆:与普通二叉堆区别在于,左式堆不是一个彻底二叉树,而且左式堆不是一个理想平衡二叉树。 */ public class LeftistHeap <E extends Comparable<? super E>> { public LeftistHeap(){ root = null; } /** * 公有的 merge 方法将 anotherLeftistHeap 合并到控制堆中。 * 随后 anotherLeftistHeap 变成了空的。 * 在第一趟,咱们经过合并两个堆的右路径创建一棵新的树。为此,以排序的方式安排 H<sub>1</sub> 和 H<sub>2</sub> * 右路径上的节点,保持他们各自的左儿子不变。 * 第二趟构成堆,儿子的交换工做在左式堆性质被破坏的那些节点上进行。 * <br> * @param anotherLeftistHeap 被合并的左式树 */ public void merge(LeftistHeap<E> anotherLeftistHeap){ if(this == null){ return ; } root = merge(root, this.root); anotherLeftistHeap.root = null; } /** * 向左式树中插入新的元素 * <br> * @param x */ public void insert(E x){ root = merge(new Node<>(x), root); } /** * 寻找左式堆中最小的元素 * <br> * @return 左式堆最小元素 */ public E findMin(){ if(isEmpty()){ return null; } return root.theElement; } /** * 删除左式堆中最小元素,并返回该元素 * <br> * @return 被删除的元素 */ public E deleteMin(){ if(isEmpty()){ return null; } E minItem = root.theElement; root = merge(root.left, root.right); return minItem; } /** * 返回左式堆是否为空 * <br> * @return */ public boolean isEmpty(){ return root == null; } /** * 将左式堆设置为空堆 */ public void makeEmpty(){ root = null; } /** * 内部类用于表示左式堆的节点,相对于普通的二叉树多了 npl(null path length)用于记录空路径长 * <br> * @param <E> 节点中的存储的对象 */ private static class Node<E>{ Node(E theElement){ this(theElement, null, null); } Node(E theElement, Node<E> left, Node<E> right){ this.theElement = theElement; this.left = left; this.right = right; npl = 0; } E theElement; Node<E> left; Node<E> right; int npl; } private Node<E> root; /** * merge 方法被用于消除一些特殊情形并保证 H<sub>1</sub> 有较小的根。 * <br> * @param h1 * @param h2 * @return */ private Node<E> merge(Node<E> h1, Node<E> h2){ if(h1 == null){ return h2; } if(h2 == null){ return h1; } if(h1.theElement.compareTo(h2.theElement) < 0){ return merge1(h1, h2); }else{ return merge1(h2, h1); } } /** * merge1 执行实际的合并操做,而且在 merge1 的调用中,h<sub>1</sub> 小于 h<sub>2</sub> * <br> * @param h1 * @param h2 * @return */ private Node<E> merge1(Node<E> h1, Node<E> h2){ //根据左式堆的性质,若是 h1.left == null,那么 h1.right == null 也成立 if(h1.left == null){ h1.left = h2; }else{ h1.right = merge(h1.right, h2); if(h1.left.npl < h1.right.npl){ swapChildren(h1); } h1.npl = h1.right.npl + 1; } return h1; } private void swapChildren(Node<E> t){ Node<E> tmp = t.left; t.right = t.left; t.left = tmp; } }