引言
优先队列(Priority Queue)是计算机科学中最常用的数据结构之一,广泛应用于任务调度、图算法(如 Dijkstra)、Top-K 问题等场景。而二叉堆(Binary Heap)是实现优先队列最经典、最高效的方式之一。本文将用 Java 从零实现一个完整的二叉堆,深入讲解上浮(sift-up)、下沉(sift-down)与堆化(heapify)的核心机制。
核心原理
二叉堆是一棵完全二叉树,分为大根堆(父节点 ≥ 子节点)与小根堆(父节点 ≤ 子节点)。利用完全二叉树的性质,可以用数组紧凑存储:
- 父节点索引:
parent = (i - 1) / 2 - 左子节点索引:
left = 2 * i + 1 - 右子节点索引:
right = 2 * i + 2
核心操作:
– 插入:在末尾添加元素,然后上浮调整,时间复杂度 O(log n)
– 弹出堆顶:将堆顶与末尾交换,删除末尾,然后下沉调整,时间复杂度 O(log n)
– 堆化:从最后一个非叶子节点开始,逐个下沉,时间复杂度 O(n)
完整 Java 实现
import java.util.Arrays;
/**
* 泛型二叉堆实现(小根堆)
* @param <T> 元素类型,需实现 Comparable 接口
*/
public class BinaryHeap<T extends Comparable<T>> {
private Object[] data; // 堆数组
private int size; // 当前元素个数
private int capacity; // 容量
public BinaryHeap() {
this.capacity = 16;
this.data = new Object[capacity];
this.size = 0;
}
public BinaryHeap(T[] arr) {
this.capacity = Math.max(arr.length * 2, 16);
this.data = Arrays.copyOf(arr, capacity);
this.size = arr.length;
heapify(); // 堆化
}
/**
* 获取父节点索引
*/
private int parent(int i) {
return (i - 1) / 2;
}
/**
* 获取左子节点索引
*/
private int leftChild(int i) {
return 2 * i + 1;
}
/**
* 获取右子节点索引
*/
private int rightChild(int i) {
return 2 * i + 2;
}
/**
* 比较两个元素(小根堆:返回负数表示 a < b)
*/
@SuppressWarnings("unchecked")
private int compare(int i, int j) {
T a = (T) data[i];
T b = (T) data[j];
return a.compareTo(b);
}
/**
* 交换两个位置的元素
*/
private void swap(int i, int j) {
Object temp = data[i];
data[i] = data[j];
data[j] = temp;
}
/**
* 上浮操作:将索引 i 处的元素向上调整
*/
private void siftUp(int i) {
while (i > 0) {
int p = parent(i);
if (compare(i, p) >= 0) break; // 已满足堆性质
swap(i, p);
i = p;
}
}
/**
* 下沉操作:将索引 i 处的元素向下调整
*/
private void siftDown(int i) {
while (true) {
int minIdx = i;
int left = leftChild(i);
int right = rightChild(i);
if (left < size && compare(left, minIdx) < 0) {
minIdx = left;
}
if (right < size && compare(right, minIdx) < 0) {
minIdx = right;
}
if (minIdx == i) break; // 已满足堆性质
swap(i, minIdx);
i = minIdx;
}
}
/**
* 堆化:将无序数组调整为堆结构
* 时间复杂度 O(n)
*/
private void heapify() {
// 从最后一个非叶子节点开始下沉
for (int i = parent(size - 1); i >= 0; i--) {
siftDown(i);
}
}
/**
* 动态扩容
*/
private void ensureCapacity() {
if (size >= capacity) {
capacity *= 2;
data = Arrays.copyOf(data, capacity);
}
}
/**
* 插入元素
*/
public void push(T val) {
ensureCapacity();
data[size] = val;
siftUp(size);
size++;
}
/**
* 弹出堆顶元素
*/
@SuppressWarnings("unchecked")
public T pop() {
if (size == 0) return null;
T top = (T) data[0];
data[0] = data[size - 1];
size--;
if (size > 0) siftDown(0);
return top;
}
/**
* 查看堆顶元素
*/
@SuppressWarnings("unchecked")
public T peek() {
return size == 0 ? null : (T) data[0];
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
/**
* 原地堆排序:利用大根堆进行升序排序
*/
public static <T extends Comparable<T>> void heapSort(T[] arr) {
int n = arr.length;
// 构建大根堆
for (int i = (n - 2) / 2; i >= 0; i--) {
siftDown(arr, i, n);
}
// 逐个将堆顶放到末尾
for (int i = n - 1; i > 0; i--) {
T temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
siftDown(arr, 0, i);
}
}
private static <T extends Comparable<T>> void siftDown(T[] arr, int i, int n) {
while (true) {
int maxIdx = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left < n && arr[left].compareTo(arr[maxIdx]) > 0) maxIdx = left;
if (right < n && arr[right].compareTo(arr[maxIdx]) > 0) maxIdx = right;
if (maxIdx == i) break;
T temp = arr[i];
arr[i] = arr[maxIdx];
arr[maxIdx] = temp;
i = maxIdx;
}
}
public static void main(String[] args) {
// 测试优先队列
BinaryHeap<Integer> heap = new BinaryHeap<>();
int[] nums = {5, 3, 8, 1, 9, 2, 7};
for (int x : nums) heap.push(x);
System.out.print("小根堆弹出顺序: ");
while (!heap.isEmpty()) {
System.out.print(heap.pop() + " ");
}
System.out.println(); // 1 2 3 5 7 8 9
// 测试堆化构造
Integer[] arr = {5, 3, 8, 1, 9, 2, 7};
BinaryHeap<Integer> heap2 = new BinaryHeap<>(arr);
System.out.println("堆顶元素: " + heap2.peek()); // 1
// 测试堆排序
Integer[] sortArr = {5, 3, 8, 1, 9, 2, 7};
heapSort(sortArr);
System.out.println("堆排序结果: " + Arrays.toString(sortArr)); // [1, 2, 3, 5, 7, 8, 9]
}
}
复杂度分析
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 插入 (push) | O(log n) | 最坏情况从叶子上浮到根 |
| 弹出堆顶 (pop) | O(log n) | 最坏情况从根下沉到叶子 |
| 查看堆顶 (peek) | O(1) | 直接访问数组首元素 |
| 堆化 (heapify) | O(n) | 线性时间,优于 n 次插入的 O(n log n) |
| 堆排序 | O(n log n) | 原地排序,空间复杂度 O(1) |
关键洞察:堆化为何是 O(n)
堆化从最后一个非叶子节点开始逐个下沉。高度为 h 的节点最多有 ⌈n / 2^(h+1)⌉ 个,每个下沉耗时 O(h)。总时间为:
T(n) = Σ(h=0 to log n) ⌈n / 2^(h+1)⌉ * O(h) = O(n)
因为 Σ h / 2^h 收敛于常数 2。
应用场景
- Dijkstra 算法:用堆维护最短距离节点
- Top-K 问题:维护大小为 K 的堆
- 任务调度:按优先级取出任务
- 合并 K 个有序数组:K 路归并
- 堆排序:原地、不稳定排序
总结
二叉堆以其 O(log n) 的插入删除和 O(1) 的堆顶访问,成为优先队列的最佳实现之一。理解上浮与下沉的本质——即在父子之间维护堆序性质——是掌握所有堆变体(二项堆、斐波那契堆、配对堆)的基础。