《算法之美:二叉堆与优先级的艺术》

前言

我曾写过一篇关于数据结构堆的文章 链接,但效果并不理想,因为看完之后仍然很难手写一个堆。

通常情况下,标准库中的 priority_queue(C++)或 PriorityQueue(Java)已经足够使用,但它们也有局限性。对于需要自定义操作的算法,比如调整优先级或改写堆中元素位置,标准库不支持这些功能。此外,这些库仅允许访问堆顶元素,不支持迭代或通过下标访问堆。

因此,对于更灵活的需求,掌握手写堆显得尤为重要,特别是在结合反向索引堆或控制堆高度时,会更加得心应手。

编程语言:C++/Java
注意:本篇不过多涉及二叉堆的原理详解, 笔者应当熟悉二叉堆这种数据结构, 这里只是提供一种较快手写堆的思路和代码。

堆的简单回顾

  1. 堆的结构

    • 堆是一棵完全二叉树,所有层的节点都被从左到右依次填满,只有最后一层可能未满。
    • 因为是完全二叉树,堆更适合使用数组实现,通过下标关系快速定位父节点和子节点,而不需要额外指针,避免链式结构的复杂性。
  2. 堆顶元素的存储位置

    • 数组下标:堆顶元素通常存储在数组下标 0(零基索引)处,也可以选择从 1(基于一的索引)开始。
    • 堆顶的意义
      • 堆顶是最大堆(大根堆)中最大的元素;
      • 堆顶是最小堆(小根堆)中最小的元素。
  3. 堆的分类

    • 大根堆(最大堆)
      每个父节点的值 ≥ 子节点的值,优先级高的在堆顶。
    • 小根堆(最小堆)
      每个父节点的值 ≤ 子节点的值,优先级低的在堆顶。

广义的优先级比较

传统上,堆用于比较数值(比如数值的大小),以决定堆的顺序。但堆的本质是对“优先级”进行组织管理,广义的优先级比较可以扩展到任意属性:

  1. 数值比较

    • 小根堆:数值越小优先级越高,堆顶存放最小值;
    • 大根堆:数值越大优先级越高,堆顶存放最大值。
  2. 逻辑优先级

    • 小根堆:父节点的某种属性(例如时间戳更早、距离更短等)小于子节点。
    • 大根堆:父节点的某种属性(例如权重更高、收益更大等)大于子节点。
  3. 优先级定义

    • 通过自定义比较函数,将逻辑上的“大小关系”映射到堆的组织结构中。
    • 两种语言内置的广义优先级体现:在 C++ 中可以使用 std::priority_queue 的第三个模板参数;在 Java 中可以通过 Comparator 实现类似功能。

通过这种扩展,堆不仅能比较数值,还可以根据任意属性或逻辑组织数据,从而实现多样化的优先级控制。

实现

下面以C++为例用数组快速实现堆
在我看来的堆操作只需要封装两个函数上浮push_up,下沉push_down
插入删除本质就是要么交换一下,或者底层数组尾部插入一个元素,然后取调上浮或者下沉接口罢了。

创建初始化

下列heap数组的形式不唯一, 它也可能int heap[N][2],int heap[N][3], 甚至不是int类型, 取决于维护什么信息的类型和数量。
在后续学习图论算法中。
比如在Dijkstra算法中,int heap[N][2]行下标是堆数组下标,列下标[0]维护顶点编号,[1]维护源点到当前顶点的已知最短距离。

const int N = 1e6+10;//数据范围
int heap[N],hz;//堆底层数组,hz跟踪当前堆的大小
//初始化操作
void init(){
   
    hz = 0;
}

初始化操作也很简单, 让数组指针hz重置为0即可。


插入

明白插入的底层操作。

  • 先进行数组尾插, 由于逻辑上要呈现处堆的特点, 但尾插元素可能破坏堆结构。 只需要对插入元素进行上浮push_up即可。
//插入操作
void push(int x){
   
    //底层数组尾部进行插入
    heap[hz] = x;
    //此时hz为有效下标,传入push_up上浮
    push_up(hz);
    //堆大小+1
    hz++;
}

上浮操作

while循环条件判断语句应该是逻辑上选出优先级高的。
这里仅仅以堆数组中存储的值更小的具有更高优先级这个逻辑距离。同样的, 这里是小根堆, 若想调整为大根堆,变换一下顺序即可。

while循环内部, 执行交换逻辑, 然后调整堆数组的i下标。
这里不要求i>0, 但注意不能写成heap[i]<=heap[(i-1)/2], 取等会死循环

//上浮操作
void push_up(int i){
   
    while (heap[i]<heap[(i-1)/2]){
   
        swap(heap[i], heap[(i-1)/2]);
        i = (i-1)/2;
    }
}

删除

删除堆顶元素, 且删除只能删堆顶元素, 不支持内部元素删除
删除逻辑也很好想。“懒删除”, 将堆数组最末尾的元素与堆顶元素进行交换, 然后堆大小整体减一。 这样原先的堆顶元素就移除出去了。
swap(heap[0], heap[--hz]); 这一条语句就是上面的代码。
由于原先末尾元素放置到了堆顶, 可能破坏堆顶。 因此选择下沉操作。
删除代码如下, 结合下沉代码看。

//删除堆顶元素
//堆化操作,将堆顶元素下沉,维持堆性质
void pop(){
   
    //堆顶元素与底层数字最末尾元素交换, 堆大小-1表示堆顶被删除了。
    swap(heap[0],heap[--hz]);
    //新堆顶下沉调整, 找到正确的堆顶
    push_down(0);
}
下沉操作

流程:’

  • 从左孩子,右孩子和自身进行比较。 如果比较处理的结果不是自身优先级最高, 那么依次交换更新下标。 否则,终止下沉操作。具体看代码
//下沉操作
void push_down(int i){
   
    int l=i*2+1;//l是左孩子
    while(l<hz){
   
        //找出最棒的孩子,优先级最高的孩子
        int best = l+1<hz&&heap[l+1]<heap[l]?l
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值