最小堆的基本概念
- 完全二叉树:堆通常被实现为一个完全二叉树。这意味着除了最后一层外,所有层都是满的,而且最后一层的所有节点都尽可能地靠左。
- 堆属性:在最小堆中,父节点的值总是小于或等于它的任何子节点的值。这意味着根节点是整个堆中的最小元素。
数组表示法
由于堆是一个完全二叉树,我们可以用数组来高效地表示它。对于数组中的任意索引i:
- 父节点的索引为
Math.floor((i - 1) / 2)。 - 左子节点的索引为
2 * i + 1。 - 右子节点的索引为
2 * i + 2。
实现步骤
- 定义类和基本属性:创建一个类来表示堆,并初始化一个空数组来存储堆的数据。
- 插入操作:添加新元素到堆中,并调整堆以保持堆的性质。
- 上浮操作:当新元素插入时,需要与父节点比较并交换位置,直到满足堆的条件。
- 提取最小值:移除并返回堆顶元素(即最小值),然后重新调整堆。
- 下沉操作:当堆顶元素被移除后,需要用最后一个元素替换它,并向下调整堆以恢复堆的性质。
JavaScript代码示例
class Head {
// 获取最后一个父节点
getLastNonLeafIndex() {
return Math.floor((this.heap.length - 2) >> 1)
}
// 获取虚拟左节点
getLeftIndex(index) {
return index * 2 + 1
}
// 获取虚拟右节点
getRightIndex(index) {
return index * 2 + 2
}
// 获取虚拟父节点
getParentIndex(index) {
return Math.floor((this.heap.length - 1) >> 1)
}
constructor(arr = []) {
this.heap = [...arr];
this.compare = (i, j) => (this.heap[i] < this.heap[j])
if (arr.length) {
// 初次未构建过的 需要从后往前构建
for (let i = this.getLastNonLeafIndex(); i >= 0; i--)
this.siftDown(i)
}
}
size() {
return this.heap.length
}
_compare(frist, sen, callback) {
if (this.size() > frist && callback(frist, sen)) {
return frist
}
return sen
}
siftDown(index) {
const leftIndex = this.getLeftIndex(index)
const rightIndex = this.getRightIndex(index)
let swapIndex = this._compare(leftIndex, index, this.compare)
swapIndex = this._compare(rightIndex, swapIndex, this.compare)
if (swapIndex != index) {
this._swap(swapIndex, index)
this.siftDown(swapIndex)
}
}
siftUp(index) {
let parentIndex = this.getParentIndex(index)
let swapIndex = index;
if (parentIndex >= 0 && this.heap[parentIndex] > this.heap[index]) {
swapIndex = index
}
if (swapIndex != index) {
this._swap(swapIndex, index)
this.siftUp(swapIndex)
}
}
_swap(i, j) {
[this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]]
}
insert(val) {
this.heap.push(val)
this.siftUp(this.heap.length - 1)
}
shift() {
if (this.heap.length === 0) return null;
let val = this.heap[0]
let size = this.size()
this._swap(0, size - 1)
this.heap.pop()
this.siftDown(0)
return val
}
}
堆数据结构因其独特的性质而被广泛应用于多种场景中。以下是一些典型的应用场景,引用了之前提供的内容中的细节:
-
优先级队列: 堆的一个最直接的应用是实现优先级队列。在优先级队列中,每个元素都有一个相关的优先级,优先级最高的元素最先被处理。例如,在操作系统中调度任务时,可以使用堆来确保最重要的任务首先执行
-
堆排序: 堆排序是一种基于堆的排序算法,利用最大堆或最小堆的性质来对数据进行排序。它的时间复杂度为O(n log n),并且不需要额外的存储空间,这使得它成为一种非常高效的排序方法
-
图算法: 在许多图算法中,比如计算最短路径的Dijkstra算法和构建最小生成树的Prim算法,使用优先队列来选择下一个要处理的顶点。堆作为优先队列的实现,可以有效地减少这些算法的运行时间
-
动态数据流的中值查找: 在处理动态数据流时,堆可以用来快速计算数据的中值或其他顺序统计量。通过维护一个最大堆和一个最小堆,可以保证两个堆的顶部元素表示当前所有数据的中间值
-
带权调度问题: 在处理带权任务调度问题时,可以使用最小堆来安排任务的执行顺序,以最小化等待时间或优化其他指标
-
Top K问题: 堆经常用于解决Top K问题,即从一组数据中找到最大或最小的K个元素。使用最小堆(或最大堆)可以有效地解决这类问题,特别是在处理大数据流或数据集合非常大时
-
频率统计和数据压缩: 在频率统计和数据压缩领域,如Huffman编码算法,使用最小堆来构建Huffman树,以实现高效的编码方案
-
合并有序小文件: 如果有多个有序的小文件需要合并成一个大的有序文件,可以通过将每个文件的第一个元素放入最小堆中,并依次取出堆顶元素的方式来完成这一过程
-
定时任务轮训问题: 对于定时任务或者分布式定时任务的场景,可以使用堆来高效地管理任务的执行时间,确保最早需要执行的任务首先被执行
-
求中位数: 类似于动态数据流的中值查找,通过同时维护两个堆(一个最大堆和一个最小堆),可以在不断变化的数据集中快速获得中位数
综上所述,堆作为一种数据结构,其应用场景十分广泛,从简单的排序到复杂的图算法、数据流处理等都有它的身影。通过合理地应用堆,可以显著提高算法效率,特别是在需要频繁访问最小或最大元素的情况下