引言
二叉堆是对优先队列的一种高效实现,左式堆是针对二叉堆合并操作困难的缺点,而提出的另外一种优先队列实现方式。
线性结构合并困难是显而易见的,而二叉堆那样高效的支持合并操作而且只使用一个数组更是难得。
这是因为,合并似乎需要把一个数组拷贝到另一个数组中去,对于相同大小的堆,这将花费O(N)。
但这区区O(N)还不够,所以就不能使用顺序存储结构,应该使用链式指针。有一句话说的特别好:所有支持高效合并的高级数据结构都需要使用指针。
能更高效完成合并的左式堆和二项队列显然都是使用了指针,是链接存储的。
左式堆详解
从npl属性看左式堆
注意理解 npl 这个属性,npl 是 null path length 的缩写,意为从该结点到达一个没有两个孩子的结点的最短距离(一个孩子的结点或者叶子结点)。
一般定义 null 的 npl 为 -1 以使计算简便。
容易得到,任意结点的 npl 是它的子结点的 npl 中较小的那个结点的 npl+1 。
即 root.npl = min(root.left.npl, root.right.npl)+1
(前提是root != null && root.left != null && root.right != null,否则空指针……)
任意结点的左孩子的 npl 大于等于右孩子的 npl 。这个特性决定了左式堆的不平衡性,并且左边会明显比较深,这就是“左式堆”名称的由来。
由上面提到的性质可以得到:左式堆任意结点的npl为右孩子的npl+1。
重要结论
左式堆任意结点的npl为右孩子的npl+1
沿右路共有r个结点的左式堆至少有2r-1个结点
左式堆的核心算法
- void insert(x) → Insert x
- Comparable deleteMin() → Return and remove smallest item
- Comparable findMin() → Return smallest item
- boolean isEmpty() → Return true if empty; else false
- void makeEmpty() → Remove all items
- void merge(rhs) → Absorb rhs into this heap
不难理解,最核心的算法还是那三个:merge、insert、deleteMin,下面加以简单分析。
- 左式堆的最基本的操作就是合并(merge),之所以把它构造成这样一个不平衡的堆,就是为了使它相对于一般的二叉堆来说,合并变得非常地容易。左式堆的合并共有四步:
- 如果有一棵树是空树,则返回另一棵树;
否则递归地合并根结点较小的堆的右子树和根结点较大的堆。 - 使形成的新堆作为较小堆的右子树。
- 如果违反了左式堆的特性,交换两个子树的位置。
- 更新npl。
- 如果有一棵树是空树,则返回另一棵树;
- 左式堆的插入(insert)很简单,其实也就是一个单结点和原堆的合并。
- 左式堆的deleteMin也很简单,就是把根结点删除,把两棵子树合并。左式堆的删除可以考虑懒惰删除(Lazy Delete)。
核心算法效率
操作均基于合并,而合并仅对右路做合并,而右路结点的数量为总数量的对数关系,所以左式堆的三个操作所花的时间为O(logN)。不过我在另一文中也分析了,根据建堆O(logN)的事实,虽然根据大O的定义,插入是O(logN)肯定没争议,你说是O(N)都没错……但仔细想想,是不是平均效率是O(1)的呢?这个问题我也还在思考,到底能不能是O(1)呢?
反正且当三个O(logN)也行,至少合并很高效了。
左式堆与二叉堆
左式堆和二叉堆都具有一样的堆序性(大根堆和小根堆)。此外,左式堆主要保留了以下两个堆属性:
- 左式堆仍然以二叉树的形式构建
- 左式堆的任意结点的值比其子树任意结点值均大/小(大根堆/小根堆的特性)
但左式堆和二叉堆还是有很大的差别的。
二叉堆是完全二叉树,左式堆不是完全二叉树,可能具有非常明显的不平衡特征。
左式堆的编程实现
/**
* Implements a leftist heap.
* Note that all "matching" is based on the compareTo method.
*/
public class LeftistHeap<T extends Comparable<? super T>> {
private LeftistNode<T> root; // root
/**
* Construct the leftist heap.
*/
public LeftistHeap() {
root = null;
}
/**
* Merge rhs into the priority queue.
* rhs becomes empty. rhs must be different from this.
* @param rhs the other leftist heap.
*/
public void merge(LeftistHeap<T> rhs) {
if(this == rhs) { // Avoid aliasing problems
return;
}
root = merge(root, rhs.root);
rhs.root = null;
}
/**
* Internal method to merge two roots.
* Deals with deviant cases and calls recursive merge1.
*/
private LeftistNode<T> merge(LeftistNode<T> h1, LeftistNode<T> 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);
}
}
/**
* Internal method to merge two roots.
* Assumes trees are not empty, and h1's root contains smallest item.
*/
private LeftistNode<T> merge1(LeftistNode<T> h1, LeftistNode<T> h2) {
if(h1.left == null) { // Single node
h1.left = h2; // Other fields in h1 already accurate
} else {
h1.right = merge(h1.right, h2);
if(h1.left.npl < h1.right.npl) {
swapChildren(h1);
}
h1.npl = h1.right.npl + 1;
}
return h1;
}
/**
* Swaps t's two children.
*/
private static <T> void swapChildren(LeftistNode<T> t) {
LeftistNode<T> tmp = t.left;
t.left = t.right;
t.right = tmp;
}
/**
* Insert into the priority queue, maintaining heap order.
* @param x the item to insert.
*/
public void insert(T x) {
root = merge(new LeftistNode<>(x), root);
}
/**
* Find the smallest item in the priority queue.
* @return the smallest item, or throw UnderflowException if empty.
*/
public T findMin() {
if(isEmpty()) {
throw new UnderflowException();
}
return root.element;
}
/**
* Remove the smallest item from the priority queue.
* @return the smallest item, or throw UnderflowException if empty.
*/
public T deleteMin() {
if(isEmpty()) {
throw new UnderflowException();
}
T minItem = root.element;
root = merge(root.left, root.right);
return minItem;
}
/**
* Test if the priority queue is logically empty.
* @return true if empty, false otherwise.
*/
public boolean isEmpty() {
return root == null;
}
/**
* Make the priority queue logically empty.
*/
public void makeEmpty() {
root = null;
}
private static class LeftistNode<T> {
LeftistNode(T theElement) {
this(theElement, null, null);
}
LeftistNode(T theElement, LeftistNode<T> lt, LeftistNode<T> rt) {
element = theElement;
left = lt;
right = rt;
npl = 0;
}
T element; // The data in the node
LeftistNode<T> left; // Left child
LeftistNode<T> right; // Right child
int npl; // null path length
}
}
测试
public class LeftistHeapTest {
public static void main(String [] args) {
int numItems = 100;
LeftistHeap<Integer> h = new LeftistHeap<>();
LeftistHeap<Integer> h1 = new LeftistHeap<>();
int i;
for(i = 37; i != 0; i = (i+37) % numItems) {
if(i % 2 == 0) {
h1.insert(i);
} else {
h.insert(i);
}
}
h.merge(h1);
for(i = 1; i < numItems; i++) {
if(h.deleteMin() != i) {
System.out.println("Oops! " + i);
}
}
}
}
来源:CSDN
作者:进阶的JFarmer
链接:https://blog.csdn.net/weixin_43896318/article/details/104468471