引言
二项堆(Binomial Heap)是一种优雅的高级数据结构,它由一组满足特定性质的二项树组成。与普通二叉堆相比,二项堆的最大优势在于支持 O(log n) 时间复杂度的堆合并操作,而二叉堆合并需要 O(n)。这一特性使二项堆在需要频繁合并堆的场景(如图算法中的优先队列)中表现优异。本文将用 Java 完整实现二项堆,并深入讲解其核心算法。
二项树的定义与性质
二项树 Bₖ 是一棵递归定义的有序树:
– B₀ 是只有一个节点的树
– Bₖ 由两棵 Bₖ₋₁ 连接而成,其中一棵的根成为另一棵根的最左孩子
关键性质:Bₖ 恰好有 2ᵏ 个节点,高度为 k,根节点的度数为 k。
二项堆的结构
二项堆 H 满足以下不变性:
1. H 中的每棵树都是二项树
2. 堆中任意节点的关键字不小于其父节点的关键字(最小堆性质)
3. 对于每个度数 k,堆中至多存在一棵二项树 Bₖ
这意味着含有 n 个节点的二项堆,其二项树的数量等于 n 的二进制表示中 1 的个数,最多为 ⌊log₂n⌋ + 1 棵。
核心操作:级联合并(Union)
合并两个二项堆的算法类似于二进制加法:
– 将两个堆的根表按度数升序排列
– 从左到右遍历,当遇到两棵度数相同的二项树时,合并它们(度数小的根作为子树)
– 若合并后仍与下一棵同度数,则继续向上合并(类似进位)
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* 二项堆实现 —— 支持高效合并的最小堆
* @param <T> 元素类型,需实现Comparable接口
*/
public class BinomialHeap<T extends Comparable<T>> {
/**
* 二项树节点
*/
static class BinomialNode<T> {
T key; // 节点关键字
int degree; // 节点度数(孩子数量)
BinomialNode<T> parent; // 父节点
BinomialNode<T> child; // 最左孩子
BinomialNode<T> sibling; // 右兄弟
BinomialNode(T key) {
this.key = key;
this.degree = 0;
this.parent = null;
this.child = null;
this.sibling = null;
}
}
private BinomialNode<T> head; // 根链表的头指针
private int size; // 堆中元素总数
public BinomialHeap() {
this.head = null;
this.size = 0;
}
/**
* 判断堆是否为空
*/
public boolean isEmpty() {
return head == null;
}
/**
* 获取堆中元素个数
*/
public int size() {
return size;
}
/**
* 查找最小关键字节点
* 时间复杂度: O(log n)
*/
public T findMinimum() {
if (isEmpty()) {
throw new IllegalStateException("堆为空");
}
BinomialNode<T> minNode = head;
BinomialNode<T> curr = head.sibling;
// 遍历根链表,寻找最小关键字
while (curr != null) {
if (curr.key.compareTo(minNode.key) < 0) {
minNode = curr;
}
curr = curr.sibling;
}
return minNode.key;
}
/**
* 链接两棵度数相同的二项树
* 使 y 成为 z 的孩子,要求 z.key <= y.key
* 时间复杂度: O(1)
*/
private void link(BinomialNode<T> y, BinomialNode<T> z) {
y.parent = z;
y.sibling = z.child; // y 成为 z 的最左孩子
z.child = y;
z.degree++; // z 的度数增加
}
/**
* 合并两个二项堆的根链表(按度数升序)
* 这是 union 操作的辅助步骤
*/
private BinomialNode<T> mergeRoots(BinomialHeap<T> other) {
if (this.head == null) return other.head;
if (other.head == null) return this.head;
// 使用归并排序的思想合并两个有序链表
BinomialNode<T> dummy = new BinomialNode<>(null);
BinomialNode<T> tail = dummy;
BinomialNode<T> p1 = this.head;
BinomialNode<T> p2 = other.head;
while (p1 != null && p2 != null) {
if (p1.degree <= p2.degree) {
tail.sibling = p1;
p1 = p1.sibling;
} else {
tail.sibling = p2;
p2 = p2.sibling;
}
tail = tail.sibling;
}
tail.sibling = (p1 != null) ? p1 : p2;
return dummy.sibling;
}
/**
* 合并(Union)两个二项堆
* 时间复杂度: O(log n)
*/
public void union(BinomialHeap<T> other) {
if (other == null || other.isEmpty()) {
return;
}
// 合并根链表
BinomialNode<T> newHead = mergeRoots(other);
if (newHead == null) return;
BinomialNode<T> prev = null;
BinomialNode<T> curr = newHead;
BinomialNode<T> next = curr.sibling;
// 处理度数相同的树:类似二进制加法进位
while (next != null) {
// 情况1: 当前和下一棵度数不同,直接前进
// 情况2: 下下棵也存在且度数相同,先处理后面的(延迟进位)
if (curr.degree != next.degree ||
(next.sibling != null && next.sibling.degree == curr.degree)) {
prev = curr;
curr = next;
}
// 情况3: 当前节点关键字较小,将 next 链接为 curr 的孩子
else if (curr.key.compareTo(next.key) <= 0) {
curr.sibling = next.sibling;
link(next, curr);
}
// 情况4: next 节点关键字较小,将 curr 链接为 next 的孩子
else {
if (prev == null) {
newHead = next;
} else {
prev.sibling = next;
}
link(curr, next);
curr = next;
}
next = curr.sibling;
}
this.head = newHead;
this.size += other.size;
other.head = null;
other.size = 0;
}
/**
* 插入新元素
* 将单元素堆与当前堆合并
* 均摊时间复杂度: O(1)
*/
public void insert(T key) {
BinomialHeap<T> newHeap = new BinomialHeap<>();
newHeap.head = new BinomialNode<>(key);
newHeap.size = 1;
union(newHeap);
}
/**
* 提取(删除)最小元素
* 时间复杂度: O(log n)
*/
public T extractMin() {
if (isEmpty()) {
throw new IllegalStateException("堆为空");
}
// 1. 找到最小根节点
BinomialNode<T> minNode = head;
BinomialNode<T> minPrev = null;
BinomialNode<T> curr = head;
BinomialNode<T> prev = null;
while (curr.sibling != null) {
prev = curr;
curr = curr.sibling;
if (curr.key.compareTo(minNode.key) < 0) {
minNode = curr;
minPrev = prev;
}
}
// 2. 将 minNode 从根链表中移除
if (minPrev == null) {
head = minNode.sibling;
} else {
minPrev.sibling = minNode.sibling;
}
// 3. 将 minNode 的孩子反转,形成新的二项堆
BinomialHeap<T> childHeap = new BinomialHeap<>();
BinomialNode<T> child = minNode.child;
if (child != null) {
// 反转孩子链表(兄弟指针原本从左到右,需要反转为根链表)
List<BinomialNode<T>> children = new ArrayList<>();
while (child != null) {
child.parent = null; // 切断与父节点的连接
children.add(child);
child = child.sibling;
}
Collections.reverse(children);
childHeap.head = children.get(0);
for (int i = 0; i < children.size() - 1; i++) {
children.get(i).sibling = children.get(i + 1);
}
children.get(children.size() - 1).sibling = null;
childHeap.size = (1 << minNode.degree) - 1;
}
// 4. 合并当前堆与子堆
T minKey = minNode.key;
union(childHeap);
size--;
return minKey;
}
/**
* 减小指定节点的关键字值
* 时间复杂度: O(log n)
*/
public void decreaseKey(BinomialNode<T> node, T newKey) {
if (newKey.compareTo(node.key) > 0) {
throw new IllegalArgumentException("新关键字不能大于当前关键字");
}
node.key = newKey;
// 向上冒泡,维护最小堆性质
BinomialNode<T> curr = node;
BinomialNode<T> parent = curr.parent;
while (parent != null && curr.key.compareTo(parent.key) < 0) {
// 交换关键字(实际应用中可能需要交换卫星数据)
T temp = curr.key;
curr.key = parent.key;
parent.key = temp;
curr = parent;
parent = curr.parent;
}
}
/**
* 删除指定节点
* 先 decreaseKey 到负无穷,再 extractMin
* 时间复杂度: O(log n)
*/
public void delete(BinomialNode<T> node) {
// 将节点关键字设为最小可能值
// 实际使用时需要确保类型支持极小值
// 这里简化为先 decreaseKey 到比当前最小值还小的值
decreaseKey(node, findMinimum());
extractMin();
}
/**
* 打印堆的结构(用于调试)
*/
public void printHeap() {
System.out.println("二项堆结构(共 " + size + " 个节点):");
BinomialNode<T> curr = head;
while (curr != null) {
System.out.print("B_" + curr.degree + ": ");
printTree(curr, 0);
curr = curr.sibling;
System.out.println();
}
}
private void printTree(BinomialNode<T> node, int depth) {
if (node == null) return;
System.out.print(node.key + " ");
BinomialNode<T> child = node.child;
while (child != null) {
printTree(child, depth + 1);
child = child.sibling;
}
}
// ========== 测试入口 ==========
public static void main(String[] args) {
BinomialHeap<Integer> heap = new BinomialHeap<>();
// 批量插入测试
int[] values = {23, 7, 3, 17, 24, 18, 52, 38, 30, 26, 46, 39};
System.out.println("插入元素: ");
for (int v : values) {
heap.insert(v);
System.out.print(v + " ");
}
System.out.println("\n");
System.out.println("最小值: " + heap.findMinimum());
System.out.println("堆大小: " + heap.size());
// 合并测试
BinomialHeap<Integer> heap2 = new BinomialHeap<>();
heap2.insert(1);
heap2.insert(15);
heap2.insert(42);
System.out.println("\n合并前 heap2 最小值: " + heap2.findMinimum());
heap.union(heap2);
System.out.println("合并后最小值: " + heap.findMinimum());
// 提取最小值测试
System.out.println("\n按序提取最小值:");
while (!heap.isEmpty()) {
System.out.print(heap.extractMin() + " ");
}
System.out.println("\n提取完成,堆为空: " + heap.isEmpty());
}
}
复杂度分析
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
makeHeap |
O(1) | 初始化空堆 |
insert |
O(1) 均摊 | 单节点堆合并 |
findMinimum |
O(log n) | 遍历根链表 |
extractMin |
O(log n) | 找到最小值 + 反转子树 + 合并 |
union |
O(log n) | 类似二进制加法,最多处理 log n 棵树 |
decreaseKey |
O(log n) | 最坏情况需向上冒泡至根 |
delete |
O(log n) | decreaseKey + extractMin |
与斐波那契堆的对比
二项堆是斐波那契堆的基础。斐波那契堆通过更宽松的树结构,将 decreaseKey 优化到 O(1) 均摊,但实现更复杂。在不需要频繁 decreaseKey 的场景下,二项堆因其简洁性和确定性复杂度仍是优选。
总结
二项堆通过二项树的级联结构,实现了堆的高效合并。其核心思想是将合并操作类比为二进制加法,度数相同的树逐级合并。掌握二项堆不仅能加深对堆数据结构的理解,也是学习更高级数据结构(如斐波那契堆)的重要基石。