想象你发现了一个藏宝洞窟,洞里堆满了价值不一的金币。洞窟有个神奇特性:
-
自动整理规则:每天凌晨,金币堆会自动调整,让价值最高的金币浮到顶部(最大堆)
-
完全二叉树结构:洞窟的每个房间(节点)都有两个子房间,且从左到右紧密排列(完全二叉树)
-
数组地图:为快速定位金币,你用坐标记录位置(数组实现)
java Copy // 洞窟的金币布局(数组表示) int[] treasureMap = {90, 70, 80, 30, 50, 10}; // 结构: // 90(0) // / \ // 70(1) 80(2) // / \ / //30(3) 50(4) 10(5)
⚙️ 一、堆的核心特性:洞窟的运行法则
1. 完全二叉树的必然性
-
紧密排列无空洞:金币必须从左到右填满房间(数组中间无空位)
-
快速定位公式(设当前房间索引为
i):- 父房间坐标:
(i-1)/2 - 左子房间:
2*i+1 - 右子房间:
2*i+2
- 父房间坐标:
2. 堆序性:价值排序规则
-
最大堆(默认):父房间金币 ≥ 子房间金币 → 洞窟顶部总是最大金币
-
最小堆:父房间金币 ≤ 子房间金币 → 顶部是最小金币(适合快速取最小值)
3. 堆 vs 二叉搜索树:本质区别
| 特性 | 堆 | 二叉搜索树 |
|---|---|---|
| 有序性 | 仅父子有序,兄弟无序 | 左 < 父 < 右(全局有序) |
| 查询效率 | 不支持快速查找(弱序) | 支持O(log n)查找 |
| 核心用途 | 快速取极值、排序 | 动态数据检索 |
💡 为什么堆查询慢?
如图中70和80是兄弟,但70的子房间有50(<80),无法直接判断大小关系
⛏️ 二、Java实现堆:洞窟的自动整理算法
第1步:定义洞窟结构(数组+指针)
java
Copy
public class TreasureCave {
private int[] coins; // 金币数组
private int capacity; // 洞窟容量
private int size; // 当前金币数
public TreasureCave(int maxSize) {
capacity = maxSize;
coins = new int[capacity];
size = 0;
}
}
第2步:添加金币——上浮魔法(Trickle Up)
情景:新金币60放入洞窟尾部,需浮动到合适位置
java
Copy
public void addCoin(int value) {
if (size == capacity) throw new RuntimeException("洞窟满了!");
coins[size] = value; // 放入尾部
trickleUp(size); // 启动上浮魔法
size++;
}
private void trickleUp(int index) {
int childIndex = index;
int newCoin = coins[childIndex];
// 循环比较父节点
while (childIndex > 0) {
int parentIndex = (childIndex - 1) / 2;
if (coins[parentIndex] >= newCoin) break; // 父节点更大,停止上浮
coins[childIndex] = coins[parentIndex]; // 父节点下沉
childIndex = parentIndex; // 向上移动指针
}
coins[childIndex] = newCoin; // 新金币落位
}
上浮过程演示:
Copy
初始: [90, 70, 80, 30, 50, 10, 20, **60**]
Step1: 60的父=50(索引4) → 60>50 → 交换 → [..50位置变60, 原50下沉]
Step2: 60的新父=70(索引1) → 60<70 → 停止
最终: 60停在索引4
第3步:取走顶部金币——下沉魔法(Trickle Down)
情景:取走顶部金币90后,尾部金币20临时顶替,再下沉调整
java
Copy
public int takeTopCoin() {
if (size == 0) throw new RuntimeException("洞窟空了!");
int top = coins[0]; // 拿走顶部金币
coins[0] = coins[size - 1]; // 尾部金币放顶部
size--;
trickleDown(0); // 启动下沉魔法
return top;
}
private void trickleDown(int index) {
int topCoin = coins[index];
while (index < size / 2) { // 只需遍历到非叶节点
int leftChild = 2 * index + 1;
int rightChild = leftChild + 1;
// 找出更大的子节点
int largerChild = rightChild < size && coins[rightChild] > coins[leftChild]
? rightChild : leftChild;
if (topCoin >= coins[largerChild]) break; // 当前节点更大,停止下沉
coins[index] = coins[largerChild]; // 子节点上浮
index = largerChild; // 向下移动指针
}
coins[index] = topCoin; // 临时金币落位
}
下沉过程演示:
Copy
取走90后:顶部=20 → [**20**, 70, 80, 30, 60, 10]
Step1: 20的子=70和80 → 80更大 → 20<80 → 交换
Step2: 20的新子=10 → 20>10 → 停止
最终: 20停在索引2
⚡ 三、堆的实战应用:洞窟的三大神器
1. 优先队列(Priority Queue)
java
Copy
// Java自带的最小堆优先队列(洞窟顶部是最小金币)
PriorityQueue<Integer> caveQueue = new PriorityQueue<>();
caveQueue.add(50);
caveQueue.add(10); // 10会自动浮到顶部
System.out.println(caveQueue.poll()); // 输出10(取走顶部最小)
2. 堆排序(时间复杂度O(n log n))
步骤:
- 将无序数组构建成堆(从最后一个非叶节点调整)
- 重复取堆顶元素 → 放入结果数组 → 调整剩余堆
java
Copy
public static void heapSort(int[] arr) {
// 1. 构建最大堆(从最后一个非叶节点开始下沉)
for (int i = arr.length/2 - 1; i >= 0; i--) {
trickleDown(arr, i, arr.length);
}
// 2. 逐个取堆顶排序
for (int i = arr.length - 1; i > 0; i--) {
swap(arr, 0, i); // 堆顶元素移到末尾
trickleDown(arr, 0, i); // 调整剩余元素为堆
}
}
3. 高性能调度系统
- 操作系统:用堆管理进程优先级(高优先级任务先执行)
- 游戏引擎:用堆快速获取最近敌人(距离作为优先级)
- 实时推荐系统:用堆维护Top K热门商品
💎 四、堆的终极总结
Image
Code
Copy
graph LR
堆本质 --> 完全二叉树[完全二叉树:数组紧凑存储]
堆本质 --> 堆序性[堆序性:父≥子或父≤子]
堆操作 --> 插入[插入:尾部上浮 O(log n)]
堆操作 --> 删除[删除:头尾交换+下沉 O(log n)]
堆应用 --> 优先队列[优先队列:Java的PriorityQueue]
堆应用 --> 堆排序[堆排序:O(n log n)原地排序]
堆应用 --> 实时调度[实时调度:操作系统/游戏引擎]
Generation failed. Please try asking in a different way
面试高频考点:
-
为什么堆插入/删除是O(log n)? → 树高决定操作次数
-
如何用堆找第K大元素? → 维护大小为K的最小堆
-
堆 vs 快速选择 找中位数? → 堆更稳定,快选更快速
试着运行文中的Java代码,亲手体验金币在洞窟中浮动的过程。堆的精髓在于用部分有序换取极值高效访问,掌握这一点就抓住了本质!