斐波那契堆——算法导论(26)

1. 写在前面

在很久之前学习过这种数据结构。这次再来学习一种比较特别的“堆”——斐波那契堆。下文首先会介绍斐波那契堆的结构,然后会介绍在其上的操作,最后再分析这些操作的效率,以及一些理论的证明。

2. 结构

斐波那契堆是一系列具有最小堆序的有根树的集合,即斐波那契堆中的每棵树均遵循最小堆性质

所谓最小堆性质是指:树中的每个结点的关键字大于或等于它的父结点(若存在)的关键字。具有最小堆性质的堆,我们称之为最小堆

举个栗子,下图便是一个斐波那契堆

斐波那契堆

从图中可以看出,它是由5个最小堆组成的,每个堆的根节点以链表的形式相互连接。

以上便是斐波那契堆的基本结构,除此之外,它还具有如下特点:

  1. 每个最小堆中的每一个结点\(x\)包含一个指向它父结点的指针\(x.p\)和一个指向它某一孩子结点的指针\(x.child\)
  2. 每个结点\(x\)的所有孩子被链接成一个环形的双向链表(称为\(x\)孩子链表(child list)),即\(x\)的每一个孩子\(y\)均有指针\(y.left, y.right\)分别指向它的左兄弟和右兄弟。特别地,如果孩子链表中只有一个结点,则\(y.left, y.right\)指向自己,即\(y.left = y.right = y\)
  3. 每个结点\(x\)还具有另外两个属性:一个是\(x.degree\),表示自己孩子的数目。另一个是一个布尔类型的属性\(x.mark\),表示结点\(x\)自从上一次成为某一个结点的孩子后,是否失去过孩子。
  4. 对于一个给定的斐波那契堆\(H\),它有一个属性\(min\),指向具有最小关键字的最小堆的根结点(称为斐波那契堆的最小结点(minimum node))。
  5. 所有组成斐波那契堆的最小堆的根结点也链接形成一个双向链表,它称为斐波那契堆的根链表(root list)

因此,若要完整的把之前图中所示的斐波那契堆的的结点关系画出来,结果如下:

3. 操作

斐波那契堆支持可合并堆操作。所谓可合并堆(mergeable),是指支持以下5种操作的一种数据结构:

  1. make-heap():创建和返回一个新的不含任何元素的堆;
  2. insert(H, x):将元素\(x\)插入堆\(H\)中;
  3. minimum(H):返回堆H中具有最小关键字的结点;
  4. extract-min(H):从堆H中删除具有最小关键字的结点并返回;
  5. union(\(H_1, H_2\)):创建并返回一个包含堆\(H_1\)和堆\(H_2\)的中所有元素的新堆。

除此之外,斐波那契堆还支持以下两种操作:

  1. decrease-key(H, x, k):将堆H中元素x的关键字赋予新值k(k不大于当前的关键字)。
  2. delete(H, x):从堆中删除元素x。

下面用Java来实现斐波那契堆。

3.1 创建一个新的斐波那契堆

首先定义出斐波那契堆的数据结构:

/**
 * 斐波那契堆
 */
public class FibonacciHeap<T extends Comparable<T>> {
    private int size; // 堆中元素的个数
    private Node<T> minNode; // 指向堆中的最小元素
    
    /**
    * 堆节点 
    */
    private static class Node<T extends Comparable> {
        Node<T> parent;
        List<Node<T>> children;
        Node<T> left; // 左兄弟
        Node<T> right; // 右兄弟
        int degree;
        boolean mark;
        T data;
        
        Node(T data) {
            this.data = data;
            left = this;
            right = this;
        } 
    }
}

其中Node静态内部类是来封装我们插入的数据和维护数据之间的关联关系,在学习基本数据结构链表或者二叉树时应该接触过,接下来实现各操作。

3.2 insert

插入结点的算法很简单,就是将结点插入根链表中,然后更新最小根节点(minNode)。更新的逻辑是:若此时堆是空的,直接将minNode指向插入的节点即可;若堆非空,我们将节点插入到最小节点的左边,再重新判断最小节点。

下面Java实现给出代码:

/**
 * 插入数据
 * @param data
 */
public void insert(T data) {
    if (data == null) {
        throw new NullPointerException("不能插入null值");
    }
    Node<T> node = new Node<>(data);
    if (minNode == null) {
        minNode = node;
    } else {
        node.insertLeftOf(minNode);
        if (data.compareTo(minNode.data) < 0)
            minNode = node;
    }
    size++;
}

insertLeftOf方法用于将某节点插入到指定节点的左边,它定义在Node内部类中,如下:

private static class Node<T extends Comparable<T>> {
    // 省略成员变量以及其他方法...
    
    private void insertLeftOf(Node<T> node) {
        left = node.left;
        right = node;
        node.left.right = this;
        node.left = this;
    }   
}

3.3 minimum

minimum方法用于返回堆中具有最小关键字的结点。由于我们用一个叫做minNode的变量指向堆中的最小节点,因此minimum过程只需返回该变量封装的数据即可:

/**
 * 获取最小的数据
 * @return 最小的数据
 */
public T minimum() {
    return minNode == null ? null : minNode.data;
}

3.4 extractMin

extractMin方法用于删除堆中具有最小关键字的结点,并返回。extractMin是斐波那契堆中复杂的操作(其实也并不复杂)。它分以下几个操作步骤完成:

  1. 首先把最小节点的所有子节点移动到根链表中,具体移动到根链表中的哪个位置不做要求;接着从根链表中删除最小节点。
  2. 判断原先堆中是否只有一个元素,若是,那么操作到此结束;否则继续下一步。
  3. 将根链表中度数相同的节点进行合并。合并的方式是将两个度数相同的节点中数据