当前位置: 首页 > news >正文

手写堆(Heap)

目录

  • 前言
  • 堆排序
    • 建堆(着重理解)
    • C++代码
  • 模拟堆
    • 映射和反映射
    • C++代码


前言

堆有个常见的应用就是堆排序(HeapSort),将数据排序是次要的,这里主要是理解这一数据结构以及它的几个相关操作。

注*: 堆还有其他应用如各语言的STL或API中优先队列(priority_queue),都是用的大根堆或小根堆实现的。

首先,堆是一个完全二叉树,用来表示一个完整的数据集合,每个结点就是一个数据,为了降低操作的复杂度,利用数组heap[]来存取堆,其根结点为heap[1]
对于heap[n],它的左结点为heap[2 * n],右结点为heap[2 * n + 1]

一般都是对堆的根结点感兴趣,由此引申两个概念——大根堆和小根堆。
大根堆是指对于堆中任意一个结点,它的父结点的值一定大于该结点以及该结点的兄弟结点,因此大根堆的根结点一定是整个集合中值最大的,小根堆的定义反之亦然。

为了构建和维护堆,有以下两个核心操作:

up(x)表示向上调整结点x以保证堆符合性质,down(x)同理表示向下调整结点x以保证堆符合性质。
其中,这两个操作的复杂度与堆的高度是成正比的,而由完全二叉树的性质得知n节点的树的高度为logn,因此复杂度都是 O(logn) 的,这也是为什么堆会比一些 O(n) 算法来维护集合争宠的原因。

比如下面的堆中,结点16向上调整前为:

在这里插入图片描述

调整后:

在这里插入图片描述


小根堆为例,一般的操作为如下5个:

在这里插入图片描述

值得注意的是,“删除xx”这一操作其实本质上是 “值的覆盖”,因为在数组中搬动大量的元素会非常麻烦,所以一般都是将堆的尾部元素heap[size]覆盖掉要抹去的结点,然后再①向上或下调整、②size --,整个集合大小减1。这样就变相删除了


堆排序

输入一个长度为 n 的整数数列,从小到大输出前 m 小的数。

输入格式
第一行包含整数 n 和 m。

第二行包含 n 个整数,表示整数数列。

输出格式
共一行,包含 m 个整数,表示整数数列中前 m 小的数。

数据范围
1≤m≤n≤105,
1≤数列中元素≤109

输入样例:

5 3
4 5 1 3 2

输出样例:

1 2 3

建堆(着重理解)

一开始读入数据时只会for ~ i然后顺序存入heap[]里,此时不构成堆,所以需要再调整元素一遍使得变成一个堆。
一种思路是利用down操作,要知道down操作是向下调整子根堆,如果从最后一个结点heap[size]开始一直往前down()操作,直到根heap[i]为止,这样整个堆就建起来了。

分析复杂度可知,n个结点,每个结点down一遍,复杂度就是 O(n * logn),较高!
但仔细一想,似乎叶子结点没有down的必要啊,因为它没有子结点了,而它们的父结点down时必定会将其考虑至其中。
因为完全二叉树的性质:总结点为n —> 叶子结点的个数约为n / 2个,所以从第一个叶子结点的父结点开始,那么对应代码如下:

for (i = n / 2; i >= 1; i--)	down(i);

再来分析复杂度会怎样呢,这里有个推导公式:

在这里插入图片描述

可以惊奇地发现,复杂度降了近一个数量级!


C++代码

#include <iostream>
#include <algorithm>
using namespace std;

#define size Size
const int N = 1e5 + 10;

/**
 * 小根堆
 */

int n, m, size;
int heap[N];

void down(int x){       //递归向下调整子结点
    int t = x;      //t:存三个中最小的结点
    if(2 * x <= size && heap[t] > heap[2 * x])      t = 2 * x;		//比较左结点
    if(2 * x + 1<= size && heap[t] > heap[2 * x + 1])      t = 2 * x + 1;	//比较右结点
    
    //如果t == x那么这个子堆往下的所有子堆都调整好,退出
    
    if(t != x){
        swap(heap[x], heap[t]);
        down(t);
    }
}

int main(){
    cin >> n >> m;
    size = n;
    for(int i = 1;i <= n;i ++)      cin >> heap[i];
    
    for(int i = n / 2; i ;i --)     down(i);        //建堆
    
    while(m --){
        cout << heap[1] << " ";
        heap[1] = heap[size --];    //删除根
        down(1);        //维护堆
    }
    
    return 0;
}

模拟堆

维护一个集合,初始时集合为空,支持如下几种操作:

I x,插入一个数 x;
PM,输出当前集合中的最小值;
DM,删除当前集合中的最小值(数据保证此时的最小值唯一);
D k,删除第 k 个插入的数;
C k x,修改第 k 个插入的数,将其变为 x;
现在要进行 N 次操作,对于所有第 2 个操作,输出当前集合的最小值。

输入格式
第一行包含整数 N。

接下来 N 行,每行包含一个操作指令,操作指令为 I x,PM,DM,D k 或 C k x 中的一种。

输出格式
对于每个输出指令 PM,输出一个结果,表示当前集合中的最小值。

每个结果占一行。

数据范围
1≤N≤105
−109≤x≤109
数据保证合法。

输入样例:

8
I -10
PM
I -10
D 1
C 2 8
I 6
PM
DM

输出样例:

-10
6

映射和反映射

由于此时出现了一个新概念 “堆中插入的第k个数”,而heap[x]仅仅能映射出x的结点值,而如何通过x找到它是第几个被插入的点以及又如何通过次序k找到第k个插入点的结点下标呢?

所以就引入了两个映射数组:

正映射:ph,第k个数中的k映射—>结点下标i,即ph[k] = i
反映射:hp,结点下标i —> k,即hp[i] = k

由于phhp是实时记录的,所以在结点交换时不再是单纯的swap(heap[a], heap[b])了。
而是要使对应的映射跟着结点下标a、b变动。
所以新的swap操作:

void heap_swap(int a, int b){
    //a, b是两个结点的下标
    swap(heap[a], heap[b]);
    swap(hp[a], hp[b]);
    swap(ph[hp[a]], ph[hp[b]]);
}

有关于映射—反映射和其它几样操作的详细说明还可以参照这篇Acwing的文章:
👉如何理解模拟堆中的heap_swap,hp[N], ph[N]?


C++代码

#include <iostream>
#include <algorithm>
#include<string.h>
#define size Size

using namespace std;

const int N = 1e5 + 10;

char op[3];
int n, x, k, cnt = 0, size = 0, heap[N];
int ph[N], hp[N];        //ph:正映射,第k个数中的k映射—>结点下标i;  hp:反映射,结点下标i —> k

void heap_swap(int a, int b){       //由于引入两个映射,结点交换时会跟着变,所以专门封装一系列的swap操作
    //a, b是两个结点的下标
    
    swap(heap[a], heap[b]);
    swap(hp[a], hp[b]);
    swap(ph[hp[a]], ph[hp[b]]);
}

void down(int x){       //思路一样直接copy堆排序
    int t = x; 
    if(2 * x <= size && heap[t] > heap[2 * x])      t = 2 * x;
    if(2 * x + 1<= size && heap[t] > heap[2 * x + 1])      t = 2 * x + 1;

    if(t != x){
        heap_swap(t, x);
        down(t);
    }
}

void up(int x){
    while(x / 2 && heap[x / 2] > heap[x]){           //父结点存在且大于该结点就交换
        heap_swap(x / 2, x);
        x >>= 1;
    }
}

int main(){
    cin >> n;
    int i;
    while(n --){
        scanf("%s", op);
        
        if(!strcmp(op, "I")){       //尾部插入
            cin >> x;       //此时cnt也即是堆的 “size”
            size ++;        //结点下标
            cnt ++;           //第k个数
            ph[cnt] = size, hp[size] = cnt;
            heap[ph[cnt]] = x;
            up(ph[cnt]);
        }
        
        else if(!strcmp(op, "PM"))      cout << heap[1] << endl;
        
        else if(!strcmp(op, "DM")){     //用尾部交换成根结点后size-- 代以删除
            heap_swap(1, size);
            size --;
            down(1);
        }
         
        else if(!strcmp(op, "D")){
            cin >> k;
            i = ph[k];      //k对应的结点下标
            heap_swap(i, size);
            size --;
            up(i), down(i);     //up和down只会执行其中一个
        }
        
        else{
            cin >> k >> x;
            i = ph[k];
            heap[i] = x;
            up(i), down(i);
        }
    }
    
    return 0;
}

相关文章:

  • java-php-python-ssm白樵校园物品交易系统计算机毕业设计
  • C语言详解《位段+联合体+枚举》
  • 2.ROS2笔记-ROS2命令行操作
  • 【Proteus】:入门学习
  • SpringBoot启动流程简析
  • 如何在网站上安装 WordPress
  • java毕业设计社区健康管理系统源码+lw文档+mybatis+系统+mysql数据库+调试
  • 17、Java——汽车租赁系统
  • SpringSecurity系列 - 15 SpringSecurity 记住我 RememberMe
  • 2022出海东南亚:越南电商市场现状及网红营销特点
  • 测试用例设计专栏
  • Intel汇编-间接寻址
  • 测试工程师面试不会做的2门功课,你是不是也是呢?
  • 365天深度学习训练营-第5周:运动鞋品牌识别
  • Centos 7 Kubernetes-1.23 Kubesphere v3.3 Docker Harbor Git 安装过程
  • (十五)java多线程之并发集合ArrayBlockingQueue
  • android 一些 utils
  • gitlab-ci配置详解(一)
  • input实现文字超出省略号功能
  • JavaScript设计模式之工厂模式
  • js ES6 求数组的交集,并集,还有差集
  • Js实现点击查看全文(类似今日头条、知乎日报效果)
  • REST架构的思考
  • SpringBoot几种定时任务的实现方式
  • text-decoration与color属性
  • vue.js框架原理浅析
  • 从setTimeout-setInterval看JS线程
  • 从输入URL到页面加载发生了什么
  • 道格拉斯-普克 抽稀算法 附javascript实现
  • 对超线程几个不同角度的解释
  • 基于Mobx的多页面小程序的全局共享状态管理实践
  • 排序算法学习笔记
  • 浅谈web中前端模板引擎的使用
  • 远离DoS攻击 Windows Server 2016发布DNS政策
  • 云栖大讲堂Java基础入门(三)- 阿里巴巴Java开发手册介绍
  • puppet连载22:define用法
  • 宾利慕尚创始人典藏版国内首秀,2025年前实现全系车型电动化 | 2019上海车展 ...
  • 没有任何编程基础可以直接学习python语言吗?学会后能够做什么? ...
  • # 日期待t_最值得等的SUV奥迪Q9:空间比MPV还大,或搭4.0T,香
  • #NOIP 2014# day.1 生活大爆炸版 石头剪刀布
  • ( )的作用是将计算机中的信息传送给用户,计算机应用基础 吉大15春学期《计算机应用基础》在线作业二及答案...
  • (1)Android开发优化---------UI优化
  • (done) 两个矩阵 “相似” 是什么意思?
  • (八)c52学习之旅-中断实验
  • (定时器/计数器)中断系统(详解与使用)
  • (二)WCF的Binding模型
  • (非本人原创)我们工作到底是为了什么?​——HP大中华区总裁孙振耀退休感言(r4笔记第60天)...
  • (已解决)什么是vue导航守卫
  • (原創) 如何解决make kernel时『clock skew detected』的warning? (OS) (Linux)
  • (转)负载均衡,回话保持,cookie
  • (转)原始图像数据和PDF中的图像数据
  • .cfg\.dat\.mak(持续补充)
  • .gitignore
  • .NET Framework 的 bug?try-catch-when 中如果 when 语句抛出异常,程序将彻底崩溃
  • .net 使用ajax控件后如何调用前端脚本