左式堆的完整实现(含比较器,Java)描述

我的梦境 提交于 2020-02-24 03:55:30

引言

二叉堆是对优先队列的一种高效实现,左式堆是针对二叉堆合并操作困难的缺点,而提出的另外一种优先队列实现方式。

线性结构合并困难是显而易见的,而二叉堆那样高效的支持合并操作而且只使用一个数组更是难得。
这是因为,合并似乎需要把一个数组拷贝到另一个数组中去,对于相同大小的堆,这将花费O(N)。
但这区区O(N)还不够,所以就不能使用顺序存储结构,应该使用链式指针。有一句话说的特别好:所有支持高效合并的高级数据结构都需要使用指针
能更高效完成合并的左式堆二项队列显然都是使用了指针,是链接存储的。

左式堆详解

这里有一篇比较详细的讲解,可看

从npl属性看左式堆

注意理解 npl 这个属性,nplnull 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),之所以把它构造成这样一个不平衡的堆,就是为了使它相对于一般的二叉堆来说,合并变得非常地容易。左式堆的合并共有四步:
    1. 如果有一棵树是空树,则返回另一棵树;
      否则递归地合并根结点较小的堆的右子树和根结点较大的堆。
    2. 使形成的新堆作为较小堆的右子树。
    3. 如果违反了左式堆的特性,交换两个子树的位置。
    4. 更新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);
            }
        }
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!