数据结构与算法---优先队列-堆-Java
优先队列(堆)
二叉堆
堆序性质
让操作快速执行的性质是堆序性质,由于我们想要快速找出最小元,因此最小元应该在根上。我们考虑任意子树也应该是一个堆,那么任意节点就应该小于它的所有后裔。
结构性质
堆是一棵完全填满的二叉树,有可能的例外是在底层,底层上的元素从左到右填入,这样的树成为完全二叉树。一个重要的发现,因为完全二叉树这么有规律,所以它可以用一个数组表示而不需要用链。该数组有一个位置0:
基本的堆操作
insert插入
将一个元素X插入到堆中,我们运用一种策略叫做 上滤(percolate up):新元素在堆中上滤直到找出正确的位置。过程如下图所示:
代码如下:
public void insert(E x) {
if (currentSize == array.length - 1)
enlargeArray(array.length * 2 + 1);
//percolate up
int hole = ++currentSize;
for (array[hole] = x; x.compareTo(array[hole]) < 0; hole /= 2)
array[hole] = array[hole / 2];
array[hole] = x;
}
deleteMin(删除最小元)
deleteMin以类似插入的方式处理,我们运用一种相反的策略叫做 下滤(percolate down),删除的元素一直是在根处,所以需要向下移动空穴,最后只需将最后一个元素移动到空穴处。
public E deleteMin() {
if (isEmpty()) throw new BufferUnderflowException();
E minItem = findMin();
array[1] = array[currentSize--];
percolateDown(1);
return minItem;
}
private void percolateDown(int hole) {
int child;
E temp = array[hole];
for (; hole * 2 <= currentSize; hole = child) {
child = hole * 2;
if (child != currentSize && array[child + 1].compareTo(array[child]) < 0)
child++;
if (array[child].compareTo(temp) < 0)
array[hole] = array[child];
else break;
}
array[hole] = temp;
}
buildHeap(构建堆)
public BinaryHeap(E[] items) {
currentSize = items.length;
array = (E[]) new Comparable[(currentSize * 2) * 11 / 10];
int i = 1;
for (E item : items)
array[i++] = item;
buildHeap();
}
private void buildHeap() {
for (int i = currentSize / 2; i > 0; i--)
percolateDown(i);
}
下面用图来解释buildHeap()的具体过程:
d-堆
d-堆是二叉堆的简单推广,它就像一个二叉堆,只是所有的节点都有d个儿子(因此,二叉堆是2-堆)
上图表示的是3-堆。d-堆要比二叉堆浅的多,它将insert
操作的运行时间改进为O( log~d N)
,然而对于大的deleteMin操作费时的多,因为虽然树是浅了,但必须找到d个儿子中最小者,会耗时。
左式堆(leftist heap)
设计一种堆结构像二叉树那样有效地支持合并操作而且只使用一个数组似乎很困难,所有支持有效合并的高级数据结构都需要使用链式数据结构。
左式堆 像二叉堆那样也具有结构性和有序性。左式堆和二叉堆唯一区别是:左式堆不是理想平衡的,而实际上趋向于非常不平衡的。
左式堆性质
我们把任一节点X的零路径长npl(x)定义为从X到一个不具有两个儿子的节点的最短路径的长。因此具有0个或一个儿子的节点的npl为0,而npl(null)=-1。在树中,零路径长标记在树的节点内。
注意 ,任意以节点的零路径长比它的个儿子节点的零路径长的最小值大1.这个结论也适用少于两个儿子的节点,因为null的零路径长是-1。
左式堆的性质:对于堆中的每一个节点X,左儿子的零路径长至少与右儿子的零路径上相等。图中只有一棵树,即左边的那棵树满足此性质。这个性质实际上超出了它确保树不平衡的要求,因为他显然偏重于使树向左增加深度。确实有可能存在由左节点形成的长路径构成的树(而且实际上更便于合并操作)——因此,有名称左式堆。
左式堆操作
对左式堆的基本操作是合并。注意插入只是合并的特殊情形,因为我们可以把插入看成是单节点堆与一个大的堆的merge。 注意,最小元素在根处,除数据、左引用和右引用所用空间外,每个节点还要有一个指示零路径的项。merge
操作,当有一个为空时,只需返回另外一个堆。否则合并另外一个堆:递归的将根值大的堆与根值小的堆的右子堆合并。
public void merge(LeftistHeap<E> rhs){
if (this==rhs){
return;
}
root=merge(root,rhs.root);
rhs.root=null;
}
private LeftistNode<E> merge(LeftistNode<E> h1,LeftistNode<E> h2){
if (h1==null)return h2;
if (h2==null)return h1;
if (h1.element.compareTo(h2.element)<0)
return merge1(h1,h2);
else return merge1(h2,h1);
}
private LeftistNode<E> merge1(LeftistNode<E> h1,LeftistNode<E> h2){
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 static <E> void swapChildren(LeftistNode<E> t){
LeftistNode<E> tmp=t.left;
t.left=t.right;
t.right=tmp;
}
insert
操作略,
public void insert(E x) {
root = merge(new LeftistNode<>(x), root);
}
deleteMin
操作比较简单,删除根节点后,直接合并左右堆。
public E deleteMin() {
if (isEmpty())
throw new BufferUnderflowException();
E minItem = root.element;
root = merge(root.left, root.right);
return minItem;
}
二项队列
二项队列结构
二项队列 与我们已经看到的所有优先队列的实现的区别在于,一个二项队列不是一棵堆序的树,而是堆序的树的集合,称为森林(forest)。每一棵堆序树都是有约束的形式,叫做二项树。每个高度上至多存在一棵二项树。高度为0的二项树是一棵单节点树;高度为K的二项树Bk 通过将一棵二项树Bk-1附接到另一颗二项树Bk-1的根上而构成。
从图上可以看到,二项树Bk 由一个带有儿子B0, B1,…,Bk-1 的根组成。高度为k的二项树恰好有2^k 个节点。
二项队列操作
作为一个例子,我们通过依序插入1到7来构成一个二项队列。4的插入展现一种坏的情形。我们把4和B0合并,得到一棵新的高度为1的树。然后将该树与B1合并,得到一棵高度为2的树,它是新的优先队列。在插入7以后的下一次插入又是一个坏情形,需要3次树的合并操作。
deleteMin
可以通过首先找出一棵具有最小根的二项树来完成。令该树为Bk,并另原始的优先队列为H。我们从H的树的森林中除去二项树Bk ,形成新的二项树队列H’ 。再出去Bk的根,得到一些二项树B0,B1,…,Bk-1,它们共同形成优先队列H’‘。合并H‘和H’‘,操作结束。 对H3,执行一次deleteMin,如图6-46,最小根是12,因此我们得到图6-47和6-48中的两个优先队列H’和H‘’。合并H‘和H’‘得到的二项队列是最后答案,如图6-49
二项队列的实现
二项队列是二项树的数组。
二项树的每一个节点将包含数据、第一个儿子以及右兄弟。二项树中的各个儿子以降秩次排列。
上一篇: google机器学习速成教程学习笔记
下一篇: python flask库编写网页入门