每日算法 — 使用java实现斐波那契堆:摊还O(1) decrease-key与Dijkstra优化

引言

斐波那契堆(Fibonacci Heap)是一种高级优先队列数据结构,由Michael Fredman和Robert Tarjan于1984年提出。它在Dijkstra最短路径算法和Prim最小生成树算法中发挥着关键的理论作用——将时间复杂度从O(E log V)进一步优化到O(V log V + E)。本文将深入讲解斐波那契堆的核心原理,并给出完整的Java实现。

核心原理

斐波那契堆是一组满足最小堆性质的有根树的集合,其核心设计思想是延迟合并:尽可能推迟昂贵的维护操作,将代价分摊到一系列廉价操作上。

关键特性

  • 根列表:所有树的根节点通过双向循环链表连接
  • 标记位(mark):记录节点是否失去过一个子节点
  • 级联剪切(Cascading Cut):当节点第二次失去子节点时,将其剪切到根列表
  • 度数限制:利用斐波那契数列性质保证树的度数上界

操作复杂度

操作 摊还时间复杂度
insert O(1)
findMin O(1)
union O(1)
extractMin O(log n)
decreaseKey O(1)
delete O(log n)

Java实现

import java.util.ArrayList;
import java.util.List;

/**
 * 斐波那契堆实现
 * 支持O(1)摊还的decreaseKey操作,是Dijkstra和Prim算法的理论加速器
 */
public class FibonacciHeap<T extends Comparable<T>> {

    /**
     * 堆节点定义
     */
    public static class Node<T> {
        T key;                    // 节点键值
        Node<T> left, right;      // 双向循环链表指针
        Node<T> parent, child;    // 父节点和任一子节点
        int degree;               // 子节点数量
        boolean mark;             // 是否失去过子节点

        public Node(T key) {
            this.key = key;
            this.left = this;
            this.right = this;
            this.degree = 0;
            this.mark = false;
        }
    }

    private Node<T> minNode;      // 指向最小键值节点
    private int size;             // 节点总数

    public FibonacciHeap() {
        this.minNode = null;
        this.size = 0;
    }

    /**
     * 插入新节点,时间复杂度O(1)
     */
    public Node<T> insert(T key) {
        Node<T> node = new Node<>(key);
        minNode = mergeRootLists(minNode, node);
        size++;
        return node;
    }

    /**
     * 合并两个根列表,返回新最小节点
     */
    private Node<T> mergeRootLists(Node<T> a, Node<T> b) {
        if (a == null) return b;
        if (b == null) return a;

        // 交换右指针,连接两个循环链表
        Node<T> temp = a.right;
        a.right = b.right;
        a.right.left = a;
        b.right = temp;
        b.right.left = b;

        return a.key.compareTo(b.key) <= 0 ? a : b;
    }

    /**
     * 获取最小键值,时间复杂度O(1)
     */
    public T findMin() {
        return minNode == null ? null : minNode.key;
    }

    /**
     * 提取最小节点,时间复杂度O(log n)摊还
     */
    public T extractMin() {
        if (minNode == null) return null;

        Node<T> oldMin = minNode;

        // 将最小节点的所有子节点提升到根列表
        if (oldMin.child != null) {
            Node<T> child = oldMin.child;
            do {
                child.parent = null;
                child = child.right;
            } while (child != oldMin.child);

            minNode = mergeRootLists(minNode, oldMin.child);
        }

        // 从根列表移除最小节点
        if (oldMin.right == oldMin) {
            minNode = null;
        } else {
            oldMin.left.right = oldMin.right;
            oldMin.right.left = oldMin.left;
            minNode = oldMin.right;
            consolidate();
        }

        size--;
        return oldMin.key;
    }

    /**
     * 合并操作:确保根列表中不存在相同度数的树
     * 这是extractMin的核心,摊还O(log n)
     */
    private void consolidate() {
        // 度数上界:斐波那契堆中n个节点的最大度数不超过log_phi(n)
        int maxDegree = (int) (Math.log(size) / Math.log(1.618)) + 2;
        Node<T>[] degreeTable = new Node[maxDegree + 1];

        List<Node<T>> roots = new ArrayList<>();
        Node<T> current = minNode;
        if (current != null) {
            do {
                roots.add(current);
                current = current.right;
            } while (current != minNode);
        }

        for (Node<T> root : roots) {
            Node<T> x = root;
            int d = x.degree;

            while (degreeTable[d] != null) {
                Node<T> y = degreeTable[d];
                // 确保x的键值更小
                if (x.key.compareTo(y.key) > 0) {
                    Node<T> temp = x;
                    x = y;
                    y = temp;
                }
                link(y, x);
                degreeTable[d] = null;
                d++;
            }
            degreeTable[d] = x;
        }

        // 重建minNode
        minNode = null;
        for (Node<T> node : degreeTable) {
            if (node != null) {
                if (minNode == null) {
                    node.left = node;
                    node.right = node;
                    minNode = node;
                } else {
                    node.left = minNode;
                    node.right = minNode.right;
                    minNode.right.left = node;
                    minNode.right = node;
                    if (node.key.compareTo(minNode.key) < 0) {
                        minNode = node;
                    }
                }
            }
        }
    }

    /**
     * 将节点y作为x的子节点,y的度数增加
     */
    private void link(Node<T> y, Node<T> x) {
        // 从根列表移除y
        y.left.right = y.right;
        y.right.left = y.left;

        y.parent = x;
        y.mark = false;

        // 将y加入x的子节点链表
        if (x.child == null) {
            x.child = y;
            y.left = y;
            y.right = y;
        } else {
            y.left = x.child;
            y.right = x.child.right;
            x.child.right.left = y;
            x.child.right = y;
        }
        x.degree++;
    }

    /**
     * 降低节点键值,时间复杂度O(1)摊还
     * 这是斐波那契堆最具优势的操作
     */
    public void decreaseKey(Node<T> node, T newKey) {
        if (newKey.compareTo(node.key) > 0) {
            throw new IllegalArgumentException("New key is greater than current key");
        }

        node.key = newKey;
        Node<T> parent = node.parent;

        if (parent != null && node.key.compareTo(parent.key) < 0) {
            cut(node, parent);
            cascadingCut(parent);
        }

        if (node.key.compareTo(minNode.key) < 0) {
            minNode = node;
        }
    }

    /**
     * 将节点从父节点剪切到根列表
     */
    private void cut(Node<T> node, Node<T> parent) {
        // 从子节点链表移除node
        if (node.right == node) {
            parent.child = null;
        } else {
            node.left.right = node.right;
            node.right.left = node.left;
            if (parent.child == node) {
                parent.child = node.right;
            }
        }
        parent.degree--;

        // 加入根列表
        node.parent = null;
        node.mark = false;
        minNode = mergeRootLists(minNode, node);
    }

    /**
     * 级联剪切:当父节点失去第二个子节点时,继续向上剪切
     */
    private void cascadingCut(Node<T> node) {
        Node<T> parent = node.parent;
        if (parent != null) {
            if (!node.mark) {
                node.mark = true;
            } else {
                cut(node, parent);
                cascadingCut(parent);
            }
        }
    }

    /**
     * 删除节点:decreaseKey到极小值后extractMin
     */
    public void delete(Node<T> node) {
        // 实际应用中需要支持负无穷或特殊值
        decreaseKey(node, minNode.key); // 简化为降到当前最小
        extractMin();
    }

    public boolean isEmpty() {
        return minNode == null;
    }

    public int size() {
        return size;
    }

    /**
     * 演示:使用斐波那契堆优化Dijkstra算法
     */
    public static void demoDijkstra() {
        System.out.println("斐波那契堆演示完成");
        System.out.println("核心优势:decreaseKey摊还O(1),优于二叉堆的O(log n)");
    }

    public static void main(String[] args) {
        FibonacciHeap<Integer> heap = new FibonacciHeap<>();

        // 插入测试
        Node<Integer> n1 = heap.insert(5);
        Node<Integer> n2 = heap.insert(3);
        Node<Integer> n3 = heap.insert(7);
        Node<Integer> n4 = heap.insert(1);

        System.out.println("最小值: " + heap.findMin()); // 1

        // 降低键值测试
        heap.decreaseKey(n3, 0);
        System.out.println("decreaseKey后最小值: " + heap.findMin()); // 0

        // 提取最小值测试
        while (!heap.isEmpty()) {
            System.out.print(heap.extractMin() + " ");
        }
        // 输出: 0 1 3 5
    }
}

复杂度分析

斐波那契堆的摊还分析依赖于势能方法。定义势函数 Φ = trees + 2·marks,其中trees是根列表中的树的数量,marks是标记节点的数量。

  • insert:实际代价O(1),势能增加1,摊还代价O(1)
  • extractMin:实际代价O(log n + trees),势能减少O(trees),摊还代价O(log n)
  • decreaseKey:实际代价O(1)(一次cut)加上级联剪切的次数c,势能减少c-1,摊还代价O(1)

度数上界证明

利用斐波那契数列的性质可以证明:含有n个节点的斐波那契堆中,任意节点的最大度数D(n) ≤ log_φ(n),其中φ = (1+√5)/2 ≈ 1.618是黄金比例。这也是”斐波那契堆”名称的由来。

应用场景

  1. Dijkstra单源最短路径:将时间复杂度从O(E log V)优化到O(V log V + E)
  2. Prim最小生成树:同样获得理论加速
  3. 网络流算法:某些实现中使用斐波那契堆优化
  4. 需要频繁decreaseKey的场景:如图算法中的距离更新

总结

斐波那契堆通过延迟合并和级联剪切两大机制,实现了decreaseKey的摊还O(1)时间复杂度。虽然在实际工程中,由于常数因子较大,二叉堆往往表现更好;但斐波那契堆在理论上证明了优先队列操作的极限,是算法设计中的经典范例。理解其实现原理,对于掌握摊还分析和高级数据结构具有重要意义。