堆
- 堆的概念
- 手搓简化版堆
- 拓展
-
- PriorityQueue
- [Top K 问题](#Top K 问题)
注意:不要将本文的"堆"与 JVM 内存中的"堆内存"混淆。前者是组织数据的方式,后者是存放对象的内存区域。
堆的概念
什么是堆?
堆 是一种特殊的 完全二叉树 ,并且满足以下 堆性质:
- 大顶堆 :每个节点的值都 大于或等于 其左右子节点的值(根节点最大)。
- 小顶堆 :每个节点的值都 小于或等于 其左右子节点的值(根节点最小)。
为什么用数组存储堆?
由于堆是一棵完全二叉树,可以用 数组 来高效存储,不需要维护复杂的左右指针。
父子节点下标关系(假设数组索引从 0 开始):
- 父节点索引 =
(i - 1) / 2 - 左子节点索引 =
2 * i + 1 - 右子节点索引 =
2 * i + 2
手搓简化版堆
MyHeap的成员变量和构造函数
java
private int[] heap;
private int size;
private int capacity;
public MyHeap(int capacity) {
this.capacity = capacity;
this.heap = new int[capacity];
this.size = 0;
}
MyHeap的辅助函数
获取父节点和左右孩子的索引
java
// 获取父节点和左右孩子
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; }
交换数组中两个位置的元素
java
private void swap(int i, int j) {
int temp = heap[i];
heap[i] = heap[j];
heap[j] = temp;
}
获取堆顶元素
java
public int peek() {
if (size == 0) throw new IllegalStateException("Heap is empty");
return heap[0];
}
判空
java
public boolean isEmpty() { return size == 0; }
求大小
java
public int size() { return size; }
打印堆
java
public void printHeap() {
System.out.print("Heap: ");
for (int i = 0; i < size; i++) {
System.out.print(heap[i] + " ");
}
System.out.println();
}
MyHeap插入元素
java
/**
* 向大顶堆中插入新元素
*
* 【算法思想】:上浮(Sift Up / Bubble Up)
*
* 核心原理:
* 1. 将新元素添加到堆的末尾(完全二叉树的最后一个位置)
* 2. 由于新元素可能破坏堆的性质(父节点 ≥ 子节点),需要向上调整
* 3. 比较新节点与其父节点,如果新节点更大,则交换位置
* 4. 重复步骤3,直到新节点不大于父节点或到达根节点
*
* 时间复杂度:O(log n),其中n为堆中元素个数
* - 最坏情况:从叶子节点上浮到根节点,需要比较树的高度次
* - 完全二叉树的高度为 ⌊log₂n⌋
*
* 空间复杂度:O(1),原地调整
*
* 举例说明:
* 初始大顶堆:[50, 30, 40, 20, 25]
* 插入新元素 60:
* 1. 添加到末尾:[50, 30, 40, 20, 25, 60]
* 2. 60的父节点是40,60>40,交换:[50, 30, 60, 20, 25, 40]
* 3. 60的父节点是50,60>50,交换:[60, 30, 50, 20, 25, 40]
* 4. 60到达根节点,完成插入
*
* @param val 要插入的元素值
* @throws IllegalStateException 如果堆已满
*/
public void insert(int val) {
// 1. 边界检查:堆容量是否已满
if (size == capacity) {
throw new IllegalStateException("Heap is full");
}
// 2. 将新元素放在数组末尾(完全二叉树的最后一个位置)
heap[size] = val;
// 3. 上浮调整:从新插入的位置开始向上调整
siftUp(size);
// 4. 堆大小增加1
size++;
}
java
/**
* 上浮操作:将指定位置的节点向上调整,维护大顶堆性质
*
* 【算法思想】:自底向上调整(Bottom-Up Heapification)
*
* 核心原理:
* 1. 大顶堆要求每个父节点 ≥ 子节点
* 2. 当子节点大于父节点时,违反堆性质,需要交换
* 3. 交换后,子节点上移成为父节点,继续与新的父节点比较
* 4. 直到满足堆性质或到达根节点
*
* 循环不变式:
* - 每次循环开始时,childIndex位置的节点是待调整的节点
* - 交换后,以parentIndex为根的子树满足堆性质
*
* 时间复杂度:O(log n)
* - 最多比较树的高度次:⌊log₂(n+1)⌋
*
* 空间复杂度:O(1)
*
* 示意图:
* 50 60
* / \ / \
* 30 60 → 30 50
* / \ / / \ /
* 20 25 40 20 25 40
*
* 60上浮到根节点
*
* @param childIndex 需要上浮的节点索引(通常是新插入的节点)
*/
private void siftUp(int childIndex) {
// 1. 计算当前节点的父节点索引
int parentIndex = parent(childIndex);
// 2. 循环条件:未到达根节点 且 子节点 > 父节点(违反大顶堆性质)
while (childIndex > 0 && heap[childIndex] > heap[parentIndex]) {
// 3. 交换子节点和父节点的值
swap(parentIndex, childIndex);
// 4. 更新索引,继续向上检查
childIndex = parentIndex;
parentIndex = parent(childIndex);
}
// 5. 循环结束时,堆性质已恢复
}
MyHeap删除堆顶
java
/**
* 删除并返回堆顶元素(最大值)
*
* 【算法思想】:替换 + 下沉(Replace + Sift Down)
*
* 核心原理:
* 1. 大顶堆的堆顶就是最大值,需要返回给调用者
* 2. 直接删除堆顶会破坏完全二叉树结构
* 3. 解决方法:将最后一个元素移到堆顶,然后向下调整
* 4. 堆顶元素下沉到合适位置,重新满足堆性质
*
* 操作步骤:
* - 保存堆顶元素(待返回值)
* - 将最后一个元素移到堆顶
* - 删除最后一个元素(size-1)
* - 从堆顶开始向下调整
*
* 时间复杂度:O(log n)
* - 主要耗时在siftDown操作,最多下沉树的高度次
*
* 空间复杂度:O(1)
*
* 举例说明:
* 大顶堆:[90, 80, 70, 60, 50, 40, 30]
* 执行poll():
* 1. 保存堆顶90
* 2. 最后一个元素30移到堆顶:[30, 80, 70, 60, 50, 40]
* 3. 从堆顶开始下沉:
* - 30与较大的子节点80比较,30<80,交换:[80, 30, 70, 60, 50, 40]
* - 30与较大的子节点60比较,30<60,交换:[80, 60, 70, 30, 50, 40]
* - 30与子节点40比较,30<40,交换:[80, 60, 70, 40, 50, 30]
* 4. 返回90
*
* @return 堆顶元素(最大值)
* @throws IllegalStateException 如果堆为空
*/
public int poll() {
// 1. 边界检查:堆是否为空
if (size == 0) {
throw new IllegalStateException("Heap is empty");
}
// 2. 保存堆顶元素(即将返回的值)
int root = heap[0];
// 3. 堆大小减1(逻辑删除最后一个元素)
size--;
// 4. 如果堆不为空,将最后一个元素移到堆顶
if (size > 0) {
heap[0] = heap[size];
// 5. 从堆顶开始下沉调整
siftDown(0);
}
// 6. 返回原堆顶元素
return root;
}
java
/**
* 下沉操作:将指定位置的节点向下调整,维护大顶堆性质
*
* 【算法思想】:自顶向下调整(Top-Down Heapification)
*
* 核心原理:
* 1. 大顶堆要求父节点 ≥ 两个子节点
* 2. 当父节点小于某个子节点时,违反堆性质
* 3. 选择较大的子节点与父节点交换(确保交换后父节点最大)
* 4. 交换后,原父节点下沉到子节点位置,继续向下比较
* 5. 直到满足堆性质或到达叶子节点
*
* 为什么选择较大的子节点?
* - 如果与较小的子节点交换,交换后的父节点可能仍小于另一个子节点
* - 示例:父节点=20,左子=30,右子=40
* 与左子交换后:[30, 20, 40] → 20 < 40,仍违反性质
* 与右子交换后:[40, 30, 20] → 满足性质
*
* 循环不变式:
* - parentIndex始终指向待调整的节点
* - 每次循环选择较大的子节点进行比较
*
* 时间复杂度:O(log n)
* - 最多下沉树的高度次:⌊log₂n⌋
*
* 空间复杂度:O(1)
*
* 示意图:
* 30 80
* / \ / \
* 80 70 → 30 70
* / \ / / \ /
* 40 50 60 40 50 60
*
* 30下沉,与较大的子节点80交换
*
* @param parentIndex 需要下沉的节点索引(通常是堆顶或某个父节点)
*/
private void siftDown(int parentIndex) {
// 1. 获取左子节点索引(作为循环起始条件)
int left = leftChild(parentIndex);
// 2. 循环条件:左子节点存在(说明有子节点)
while (left < size) {
// 3. 找出左右子节点中较大的一个
int maxIndex = left; // 假设左子节点最大
int right = rightChild(parentIndex);
// 4. 如果右子节点存在且大于左子节点,则更新最大索引
if (right < size && heap[right] > heap[left]) {
maxIndex = right;
}
// 5. 如果最大的子节点 > 父节点,违反堆性质,需要交换
if (heap[maxIndex] > heap[parentIndex]) {
// 6. 交换父节点和较大的子节点
swap(maxIndex, parentIndex);
// 7. 更新索引,继续向下调整
parentIndex = maxIndex;
left = leftChild(parentIndex);
} else {
// 8. 如果父节点已经是最大的,满足堆性质,结束循环
break;
}
}
}
MyHeap批量建堆
java
/**
* 从无序数组批量构建大顶堆
*
* 【算法思想】:Floyd建堆算法(自底向上堆化)
*
* 核心原理:
* 1. 方法一(低效):逐个插入,时间复杂度 O(n log n)
* 2. 方法二(高效):从最后一个非叶子节点开始,自底向上执行siftDown
* - 叶子节点无需调整(它们没有子节点)
* - 从最后一个父节点开始,依次向前对每个父节点执行siftDown
* - 保证每个子树都满足堆性质,最终整个树满足堆性质
*
* 为什么从最后一个非叶子节点开始?
* - 叶子节点已经满足堆性质(没有子节点)
* - 最后一个非叶子节点是第一个可能需要调整的节点
* - 计算公式:(size/2) - 1
*
* 时间复杂度:O(n),数学推导证明
* - 看似有n/2次siftDown,但每层节点数和下沉高度不同
* - 第h层的节点下沉h次,总时间 = Σ(h * 2^(h))
* - 最终收敛到O(n)
*
* 空间复杂度:O(1)
*
* 对比分析:
* 逐个插入:O(n log n)
* Floyd建堆:O(n)
* 当n=100000时,Floyd算法快约10倍
*
* 举例说明:
* 无序数组:[20, 50, 30, 70, 40, 60, 10]
*
* 步骤1:找到最后一个父节点(索引2,值30)
* 20
* / \
* 50 30 ← 从这开始
* / \ / \
* 70 40 60 10
*
* 步骤2:调整索引2的节点(30与60交换)
* 20
* / \
* 50 60
* / \ / \
* 70 40 30 10
*
* 步骤3:调整索引1的节点(50与70交换)
* 20
* / \
* 70 60
* / \ / \
* 50 40 30 10
*
* 步骤4:调整索引0的节点(20与70交换,然后20与50交换)
* 70
* / \
* 50 60
* / \ / \
* 20 40 30 10
*
* 最终得到大顶堆
*
* @param arr 无序数组(会复制到堆内部数组中)
* @throws IllegalArgumentException 如果数组大小超过堆容量
*/
public void buildHeap(int[] arr) {
// 1. 边界检查:数组是否超过堆容量
if (arr.length > capacity) {
throw new IllegalArgumentException("Array size exceeds capacity");
}
// 2. 复制数组到堆内部存储
System.arraycopy(arr, 0, heap, 0, arr.length);
size = arr.length;
// 3. 从最后一个非叶子节点开始,自底向上执行siftDown
// 最后一个非叶子节点 = (size - 2) / 2
// 因为最后一个节点的索引是size-1,其父节点就是最后一个非叶子节点
for (int i = (size - 2) / 2; i >= 0; i--) {
siftDown(i);
}
// 4. 循环结束时,整个数组已经满足大顶堆性质
}
完整测试
java
import java.util.Arrays;
import java.util.Random;
public class MyHeap {
private int[] heap;
private int size;
private int capacity;
public MyHeap(int capacity) {
this.capacity = capacity;
this.heap = new int[capacity];
this.size = 0;
}
// 获取父节点和左右孩子
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; }
// 插入元素
public void insert(int val) {
if (size == capacity) {
throw new IllegalStateException("Heap is full");
}
heap[size] = val;
siftUp(size);
size++;
}
private void siftUp(int childIndex) {
int parentIndex = parent(childIndex);
while (parentIndex >= 0 && childIndex > 0) {
if (heap[childIndex] > heap[parentIndex]) {
swap(parentIndex, childIndex);
childIndex = parentIndex;
parentIndex = parent(childIndex);
} else {
break;
}
}
}
// 删除堆顶
public int poll() {
if (size == 0) throw new IllegalStateException("Heap is empty");
int root = heap[0];
size--; // 先减少size
if (size > 0) {
heap[0] = heap[size];
siftDown(0);
}
return root;
}
// 批量建堆(从无序数组构建堆)
public void buildHeap(int[] arr) {
if (arr.length > capacity) {
throw new IllegalArgumentException("Array size exceeds capacity");
}
System.arraycopy(arr, 0, heap, 0, arr.length);
size = arr.length;
// 从最后一个非叶子节点开始下沉
for (int i = (size - 2) / 2; i >= 0; i--) {
siftDown(i);
}
}
// 下沉操作
private void siftDown(int parentIndex) {
int left = leftChild(parentIndex);
int right = rightChild(parentIndex);
while (left < size) {
int max = heap[left];
int index = left;
if (right < size && heap[right] > heap[left]) {
max = heap[right];
index = right;
}
if (max > heap[parentIndex]) {
swap(index, parentIndex);
parentIndex = index;
left = leftChild(parentIndex);
right = rightChild(parentIndex);
} else {
break;
}
}
}
private void swap(int i, int j) {
int temp = heap[i];
heap[i] = heap[j];
heap[j] = temp;
}
public int peek() {
if (size == 0) throw new IllegalStateException("Heap is empty");
return heap[0];
}
public boolean isEmpty() { return size == 0; }
public int size() { return size; }
public void printHeap() {
System.out.print("Heap: ");
for (int i = 0; i < size; i++) {
System.out.print(heap[i] + " ");
}
System.out.println();
}
// 测试主函数
public static void main(String[] args) {
System.out.println("========== 大顶堆测试 ==========\n");
// 测试1:基本插入和删除
System.out.println("【测试1】基本插入和删除操作");
MyHeap heap1 = new MyHeap(10);
int[] testData = {50, 30, 80, 20, 90, 10, 70, 40, 60, 100};
System.out.println("插入数据: " + Arrays.toString(testData));
for (int num : testData) {
heap1.insert(num);
}
heap1.printHeap();
System.out.println("堆顶元素: " + heap1.peek());
System.out.println("\n依次取出堆顶元素:");
while (!heap1.isEmpty()) {
System.out.print(heap1.poll() + " ");
}
System.out.println("\n");
// 测试2:批量建堆
System.out.println("【测试2】批量建堆测试");
MyHeap heap2 = new MyHeap(15);
int[] randomArray = {15, 5, 25, 10, 30, 20, 8, 35, 3, 28};
System.out.println("原始数组: " + Arrays.toString(randomArray));
heap2.buildHeap(randomArray);
heap2.printHeap();
System.out.println("堆顶元素: " + heap2.peek());
System.out.println("\n降序输出(堆排序):");
while (!heap2.isEmpty()) {
System.out.print(heap2.poll() + " ");
}
System.out.println("\n");
// 测试3:边界情况
System.out.println("【测试3】边界情况测试");
MyHeap heap3 = new MyHeap(5);
System.out.println("堆是否为空: " + heap3.isEmpty());
try {
heap3.peek();
} catch (IllegalStateException e) {
System.out.println("正确捕获: " + e.getMessage());
}
try {
heap3.poll();
} catch (IllegalStateException e) {
System.out.println("正确捕获: " + e.getMessage());
}
heap3.insert(100);
heap3.insert(200);
heap3.insert(300);
heap3.insert(400);
heap3.insert(500);
heap3.printHeap();
try {
heap3.insert(600);
} catch (IllegalStateException e) {
System.out.println("正确捕获: " + e.getMessage());
}
System.out.println();
// 测试4:随机数据验证堆性质
System.out.println("【测试4】随机数据验证堆性质");
MyHeap heap4 = new MyHeap(100);
Random rand = new Random(42);
int[] randomData = new int[20];
for (int i = 0; i < 20; i++) {
randomData[i] = rand.nextInt(100);
heap4.insert(randomData[i]);
}
System.out.println("随机数据: " + Arrays.toString(randomData));
heap4.printHeap();
// 验证堆性质:每个父节点都大于等于子节点
boolean isValid = true;
for (int i = 0; i < heap4.size() / 2; i++) {
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left < heap4.size() && heap4.heap[i] < heap4.heap[left]) {
isValid = false;
System.out.printf("违反堆性质: 父节点[%d]=%d < 左孩子[%d]=%d\n",
i, heap4.heap[i], left, heap4.heap[left]);
}
if (right < heap4.size() && heap4.heap[i] < heap4.heap[right]) {
isValid = false;
System.out.printf("违反堆性质: 父节点[%d]=%d < 右孩子[%d]=%d\n",
i, heap4.heap[i], right, heap4.heap[right]);
}
}
System.out.println("堆性质验证: " + (isValid ? "✓ 通过" : "✗ 失败"));
System.out.println();
// 测试5:堆排序(降序)
System.out.println("【测试5】堆排序演示");
MyHeap heap5 = new MyHeap(10);
int[] sortData = {42, 17, 89, 33, 6, 71, 25, 94, 53, 28};
System.out.println("原始数据: " + Arrays.toString(sortData));
for (int num : sortData) {
heap5.insert(num);
}
System.out.print("堆排序结果: ");
while (!heap5.isEmpty()) {
System.out.print(heap5.poll() + " ");
}
System.out.println("\n");
// 测试6:相同元素处理
System.out.println("【测试6】重复元素测试");
MyHeap heap6 = new MyHeap(10);
int[] duplicateData = {10, 20, 10, 30, 20, 40, 10};
System.out.println("重复数据: " + Arrays.toString(duplicateData));
for (int num : duplicateData) {
heap6.insert(num);
}
heap6.printHeap();
System.out.print("依次取出: ");
while (!heap6.isEmpty()) {
System.out.print(heap6.poll() + " ");
}
System.out.println();
}
}
输出结果:
========== 大顶堆测试 ==========
【测试1】基本插入和删除操作
插入数据: [50, 30, 80, 20, 90, 10, 70, 40, 60, 100]
Heap: 100 90 70 60 80 10 50 20 40 30
堆顶元素: 100
依次取出堆顶元素:
100 90 80 70 60 50 40 30 20 10
【测试2】批量建堆测试
原始数组: [15, 5, 25, 10, 30, 20, 8, 35, 3, 28]
Heap: 35 30 25 10 28 20 8 5 3 15
堆顶元素: 35
降序输出(堆排序):
35 30 28 25 20 15 10 8 5 3
【测试3】边界情况测试
堆是否为空: true
正确捕获: Heap is empty
正确捕获: Heap is empty
Heap: 500 400 200 100 300
正确捕获: Heap is full
【测试4】随机数据验证堆性质
随机数据: [30, 63, 48, 84, 70, 25, 5, 18, 19, 93, 82, 2, 76, 92, 76, 32, 56, 70, 43, 9]
Heap: 93 84 92 70 82 48 76 32 56 63 70 2 25 5 76 18 30 19 43 9
堆性质验证: ✓ 通过
【测试5】堆排序演示
原始数据: [42, 17, 89, 33, 6, 71, 25, 94, 53, 28]
堆排序结果: 94 89 71 53 42 33 28 25 17 6
【测试6】重复元素测试
重复数据: [10, 20, 10, 30, 20, 40, 10]
Heap: 40 20 30 10 20 10 10
依次取出: 40 30 20 20 10 10 10
拓展
PriorityQueue
补充
Top K 问题
补充Top K 问题