堆/二叉堆详解[C/C++]

news2025/3/15 4:23:35

前言

堆是计算机科学中-类特殊的数据结构的统称。实现有很多,例如:大顶堆,小顶堆,斐波那契堆,左偏堆,斜堆等等。从子结点个数上可以分为二汊堆,N叉堆等等。本文将介绍的是二叉堆

二叉堆的概念

1、引例

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们小时候,基本都玩过或见过叠罗汉的恶作剧(如上图)。叠罗汉运动是把人堆在一起,而且为了保证稳定性,体重大身高高的人一般在下面,体重轻身高矮的人一般在上面,我们的二叉堆结构也是按照某种规则把元素堆成一个塔形结构。

2、定义

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

二叉堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆( 例如左上图所示) ;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆(例如右上图所示)。

3、性质

对于大(小)顶堆,它总是满足下列性质:
1)空树是一个大(小)顶堆;
2)大(小)顶堆中某个结点的关键字小(大)于等于其父结点的关键字;
3)大(小)顶堆是一棵完全二叉树。
4)根结点一定是大(小)顶堆中所有结点最大(小)者。

4、作用

二叉堆能够在O(1)的时间内,获得关键字最大(小)的元素。并且能够在O(logn)的时间内执行插入和删除。一般用来做优先队列的实现、堆排序算法等。

堆的存储结构

二叉堆是一颗完全二叉树,是一种树形结构,但是我们未必真的需要按照二叉树的存储方式去实现,类似于并查集用数组来模拟树形结构,我们的二叉堆类似的,把每个结点,按照层序映射到-一个顺序存储的数组中,然后利用每个结点在数组中的下标,来确定结点之间的关系。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

结点的编号

根节点

在国内的数据结构教材中,有的把根节点编号为0,有的编号为1,这里我们选择编号为0。

孩子结点

根据上图示例结合我们二叉树的性质,不难发现

根节点和左右孩子编号关系为:lchild = parent * 2 + 1 rchild = parent * 2 +2

同样的parent = (lchild - 1) / 2 = (rchild - 1) / 2(利用C/C++除法向下取整)

存储结构

对于堆,显然我们需要一个容器来存储我们的数据,由于堆的建造过程中我们难以避免大小比较,我们不妨增加一个比较的仿函数作为模板参数

存储结构如下

template <class T, class Container = vector<T>, class Compare = less<T>>
//T为存储数据类型   Container为存储数据的容器,默认为vector   Compare为两关键字进行比较的仿函数,可由用户自定义传入
class Heap{
    //...
private:
    Container _con;
    Compare _cmp;
};

堆的构造

向下调整算法

我们给定这样一个情形,有一颗根节点不满足堆规则但是其他任意子树都满足二叉堆规则的完全二叉树,我们进行怎样的调整可以使它成为堆?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如上图示例大顶堆,根节点35小于80和70,但是其他子树都符合我们大顶堆的规则,我们该如何调整呢?

需要从孩子中选择一个节点来替代我们的根节点,为了尽可能保证大顶堆的特性,我们选择孩子节点中最大的80和35进行交换,但是交换完之后根节点是符合规则了,交换后的35又小于40和50,这时除了35所在子树其他子树都满足大顶堆的特性,所以我们继续对35的子树进行调整,再次与最大孩子节点交换,我们发现这时得到了一个大顶堆。

我们向下调整算法调整的初始节点满足除了初始节点和子节点不满足堆规则,其他节点都满足。而我们每次调整会修正当前位置,同时最多增加一个不满足规则的位置,也就是说最多调整高度次,时间复杂度为O(log(N + 1)) = O(logN)

代码如下:

    void AdjustDown(int parent)
    {
        int child = parent * 2 + 1;
        int n = _con.size();
        while (child < n)
        {
            if (child + 1 < n && _cmp(_con[child], _con[child + 1]))
            {
                child++;
            }
            if (_cmp(_con[parent], _con[child]))
            {
                swap(_con[parent], _con[child]);
                parent = child;
                child = parent * 2 + 1;
            }
            else
                break;
        }
    }

向上调整算法

会了向下调整算法,自然就会向上调整算法了

同样的,我们向上调整算法调整的初始节点满足除了初始节点和父节点不满足堆规则,其他节点都满足。而我们每次调整会修正当前位置,同时最多增加一个不满足规则的位置,最多调整高度次,时间复杂度为O(log(N + 1)) = O(logN)

这里只给出大根堆示例就不详细说明了

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

代码如下:

    void AdjustUp(int child)
    {
        int parent = (child - 1) / 2;
        while (parent >= 0)
        {
            if (_cmp(_con[parent], _con[child]))
            {
                swap(_con[parent], _con[child]);
                child = parent;
                parent = (parent - 1) / 2;
            }
            else
                break;
        }
    }

堆的构建算法

基于AdjustDown的自底而上构建

我们实际中,大多数时候都不会像两种调整算法那么巧,只有一个位置不符合规则,那么对于任意元素集合,我们该如何来进行调整呢?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

比如上图,就是一种很不符合我们堆规则的情形,而我们只会两种调整算法的情形处理方式,但是我们可以把一个堆看成多个堆的组合

我们以向下调整算法为例我们按照下标顺序,从第一个非叶子节点开始从后往前执行向下调整算法,会出现什么情况呢?

为了更好理解,这里使用动画演示

在这里插入图片描述

对于我们的开始节点,也就是第一个非叶子节点20它显然符合我们向下调整算法的情形,Adjustdown后20节点所在子树修正完毕,然后向前继续该操作,修正节点80,然后修正节点70最后到根节点15的时候我们发现15此时也符合我们Adjustdown的情形,再次Adjustdown后,我们得到了一颗大根堆

现在我们清楚了,我们的向下调整堆构建算法就是从自下而上不断的修正,直到出现只有根节点不符合堆的规则,再次Adjustdown后我们就得到了堆

时间复杂的的计算我们后面详细解释

代码如下:

void BuildHeap()
{
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(i);
	}
}
基于AdjustUp的自顶而下构建

会了基于AdjustDown的自底而上构建,自然会基于AdjustUp的自顶而下构建辣。

这里直接给出代码:

void BuildHeap()
{
for (int i = 0; i <= (n - 1 - 1) / 2; i++)
	{
		AdjustUp(i);
	}
}
两种构建算法的时间复杂度分析

前面没有给出两种构建的时间复杂度分析,是为了专门在这里说明,从而进行两种方法的选择。

对于基于AdjustDown的自底而上构建

这里给出计算方法

总调整次数为每一层节点数量乘该层每个节点调整次数求和

假设高度为h,对于第i层,有2^i次方个节点,需要调整(h - i)次,根据高中数列求和的知识,不难完成F(h)的计算
F ( h ) = 2 h − 1 × 0 + 2 h − 2 × 1 + ⋯ + 2 0 × ( h − 1 ) ① 2 ∗ F ( h ) = 2 h × 0 + 2 h − 2 × 1 + ⋯ + 2 0 × ( h − 1 ) ② F ( h ) = 2 h − 1 + 2 h − 2 + ⋯ + 2 0 − h + 2 ② − ① = 2 h − 1 − h − 1 = N − log ⁡ 2 ( N + 1 ) ≈ N \begin{equation} \begin{split} F(h)&=2^{h-1}\times 0+2^{h-2}\times 1+\dots +2^{0} \times(h - 1) ①\\ 2*F(h)&=2^{h}\times 0+2^{h-2}\times 1+\dots +2^{0} \times(h - 1) ②\\ F(h)&=2^{h-1}+2^{h-2}+\dots +2^{0}-h+2②-①\\ &=2^{h-1}-h-1\\ &=N-\log_{2}{(N+1)} \\ &\approx N \end{split} \end{equation} F(h)2F(h)F(h)=2h1×0+2h2×1++20×(h1)=2h×0+2h2×1++20×(h1)=2h1+2h2++20h+2②=2h1h1=Nlog2(N+1)N

对于基于AdjustUp的自顶而下构建

F ( h ) = 2 1 − 1 × 0 + 2 2 − 1 × 1 + ⋯ + 2 h − 1 × ( h − 1 ) ① 2 ∗ F ( h ) = 2 1 × 0 + 2 2 × 1 + ⋯ + 2 h × ( h − 1 ) ② F ( h ) = 2 h × ( h − 1 ) − 2 h + 1 ② − ① = 2 h × ( h − 2 ) − h + 1 = ( N + 1 ) × ( log ⁡ 2 N − 1 ) − log ⁡ 2 ( N + 1 ) + 1 = N × log ⁡ 2 N − N ≈ N × log ⁡ 2 N \begin{equation} \begin{split} F(h)&=2^{1-1}\times 0+2^{2-1}\times 1+\dots +2^{h-1} \times(h-1) ①\\ 2*F(h)&=2^{1}\times 0+2^{2}\times 1+\dots +2^{h} \times(h-1) ②\\ F(h)&=2^{h}\times(h-1)-2^{h}+1②-①\\ &=2^{h}\times(h-2)-h+1\\ &=(N+1)\times(\log_{2}{N} - 1)-\log_{2}{(N+1)}+1 \\ &=N\times\log_{2}{N}-N\\ &\approx N\times\log_{2}{N} \end{split} \end{equation} F(h)2F(h)F(h)=211×0+221×1++2h1×(h1)=21×0+22×1++2h×(h1)=2h×(h1)2h+1②=2h×(h2)h+1=(N+1)×(log2N1)log2(N+1)+1=N×log2NNN×log2N

通过对比发现,自底而上构建为O(N),而自顶而下构建为O(NlogN),所以我们一般选择自底而上构建

堆的常用接口

实际上,我们如果不以序列初始化堆的话,是用不到堆的构建算法的,因为每次只涉及堆的一个元素的增删查改,下面介绍我们堆的常用接口的实现

堆的插入push

对于在堆里面新增元素,我们尾插,然后此时对新元素向上调整即可

    void push(const T &x)
    {
        _con.push_back(x);
        AdjustUp(_con.size() - 1);
    }

堆顶元素的删除pop

对于删除堆顶元素,我们把堆顶元素和序列是容器中最后一个元素交换然后尾删,但是此时堆顶可能非法,所以还要对堆顶向下调整

    void pop()
    {
        swap(_con[0], _con[_con.size() - 1]);
        _con.pop_back();
        AdjustDown(0);
    }

获取堆顶元素top

直接返回堆顶即可

    T top() const
    {
        return _con[0];
    }

堆是否为空empty

调用序列是容器的接口即可

    bool empty() const
    {
        return _con.empty();
    }

获取堆的大小size

同样调用序列式容器的接口

    bool size() const
    {
        return _con.size();
    }

堆的代码实现

template <class T, class Container = vector<T>, class Compare = less<T>>
class Heap
{
public:
    explicit Heap(const Compare &cmp) : _con(), _cmp(cmp) {}
    Heap() : _con() {}

    void push(const T &x)
    {
        _con.push_back(x);
        AdjustUp(_con.size() - 1);
    }
    void pop()
    {
        swap(_con[0], _con[_con.size() - 1]);
        _con.pop_back();
        AdjustDown(0);
    }
    T top() const
    {
        return _con[0];
    }
    bool empty() const
    {
        return _con.empty();
    }

    bool size() const
    {
        return _con.size();
    }

private:
    void AdjustDown(int parent)
    {
        int child = parent * 2 + 1;
        int n = _con.size();
        while (child < n)
        {
            if (child + 1 < n && _cmp(_con[child], _con[child + 1]))
            {
                child++;
            }
            if (_cmp(_con[parent], _con[child]))
            {
                swap(_con[parent], _con[child]);
                parent = child;
                child = parent * 2 + 1;
            }
            else
                break;
        }
    }
    void AdjustUp(int child)
    {
        int parent = (child - 1) / 2;
        while (parent >= 0)
        {
            if (_cmp(_con[parent], _con[child]))
            {
                swap(_con[parent], _con[child]);
                child = parent;
                parent = (parent - 1) / 2;
            }
            else
                break;
        }
    }

private:
    Container _con;
    Compare _cmp;
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1103796.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

网络安全常见问题隐患及其应对措施

随着数字化时代的到来&#xff0c;网络安全已经成为组织和个人面临的严重挑战之一。网络攻击日益普及&#xff0c;黑客和不法分子不断寻找机会侵入系统、窃取敏感信息、破坏服务和网络基础设施。在这种情况下&#xff0c;了解网络安全的常见问题隐患以及如何应对它们至关重要。…

Android 13 - Media框架(11)- MediaCodec(一)

MediaCodec 是 Android 平台上音视频编解码的标准接口&#xff0c;无论是使用软解还是硬解都要通过调用 MediaCodec来完成&#xff0c;是学习 Android 音视频不可跳过的重要部分。MediaCodec 部分的代码有几千行&#xff0c;光是头文件就有几百行&#xff0c;对于我这样的新手来…

OpenCV Series : TI - DSP - CCS

Code Composer Studio V5.5 https://www.ti.com/tool/download/CCSTUDIO https://www.ti.com/tool/download/CCSTUDIO/5.5.0.00077

vue中引入jquery解决跨域问题

1、vue 工程文件 package.json 中 引入 “dependencies”: { “jquery”:“^2.2.4” }, 2、控制台执行命令&#xff0c;当前工程文件夹下 cnpm install 3、修改的vue文件中 加入 import $ from ‘jquery’ 4、调用 ajax请求 $.ajax({url:http://192.168.0.10:9099/strutsJspA…

黑马JVM总结(三十六)

&#xff08;1&#xff09;CAS-概述 cas是配合volatile使用的技术 &#xff0c;对共享变量的安全性要使用synachonized加锁&#xff0c;但是CAS不加锁&#xff0c;它是使用where&#xff08;true&#xff09;的死循环&#xff0c;里面compareAndSwap尝试把结果赋值给共享变量&…

Leetcode 02.07 链表相交(链表)

Leetcode 02.07 链表相交&#xff08;链表&#xff09; 解法1 尾部对齐解法2&#xff1a;太厉害了&#xff0c;数学归纳推导的方法 很巧妙&#xff0c;这就是将链表的尾端对齐后再一起遍历&#xff0c;这样能满足题目的要求。因为相交之后两个链表到结束的所有节点都一样了&…

nginx正向代理、反向代理、负载均衡(重中之重)

nginx中有两种代理方式&#xff1a; 七层代理&#xff08;http协议&#xff09; 四层代理&#xff08;基于tcp或udp的流量转发&#xff09; 一、七层代理 原理&#xff1a;客户端请求代理服务器&#xff0c;由代理服务器转发客户端的http请求&#xff0c;转发到内部的服务器…

手写一个PrattParser基本运算解析器2: PrattParser概述

点击查看 基于Swift的PrattParser项目 解析器概述 由于编译原理内容太过于枯燥, 所以当时我就在想能不能写一个编译过程, 这时候就在B站上看到了熊爷的技术去魅篇 - PrattParser解析器. 解析器主要的工作是把一系列的标记转换为树的表示形式. 例如线性代码 a 1 1 * 3 的转换…

React 路由学习总结 react-router-dom6+react-router-dom5

开题 单页面应用和多页面应用 SPA&#xff1a;单页面应用程序&#xff0c;整个应用中只有一个页面(index.html) MPA&#xff1a;多页面应用程序&#xff0c;整个应用中有很多页面(*.html) react路由 现在的前端应用大多都是SPA单页面应用程序&#xff0c;也就是一个HTML页面的…

Compose Material3 新增垂直分隔符(VerticalDivider)解析与疑惑

前言 谷歌在 7 月 28 日发布了 Compose Material3 1.2.0-alpha04 版本&#xff0c;在该版本新增&#xff08;修改&#xff09;了两个组件&#xff0c;垂直分隔符和分段按钮&#xff1a; Experimental Segmented Button API. Dividers now have a parameter to control orienta…

TwinCAT3 ADS与C++通讯

文章目录 一 ADS简介1.1 ADS通讯定义1.2 ADS通讯实现 二 上位机程序编写&#xff08;Visual Studio 2019&#xff09;2.1 启动VS2019,新建MFC项目2.2 添加ADS通讯链接库2.3 在程序中引入头文件 一 ADS简介 1.1 ADS通讯定义 ADS&#xff08;Advanced Design System&#xff09…

从0开始编写BP,自适应学习率的BP神经网络,不使用MATLAB工具箱,纯手写matlab代码,以BP分类为例...

与上篇文章不同&#xff0c;仔细读了上篇文章的小伙伴应该知道&#xff0c;BP神经网络是有一个学习率的&#xff0c;而这个学习率很大程度上决定着神经网络的效果。这里采用自适应学习率&#xff0c;实现纯手写BP神经网络。 编程时&#xff0c;激活函数选择Sigmoid函数&#xf…

【计算机毕设选题推荐】网络在线考试系统SpringBoot+SSM+Vue

前言&#xff1a;我是IT源码社&#xff0c;从事计算机开发行业数年&#xff0c;专注Java领域&#xff0c;专业提供程序设计开发、源码分享、技术指导讲解、定制和毕业设计服务 项目名 网络在线考试系统 技术栈 SpringBootSSMVueMySQLMaven 文章目录 一、网络在线考试系统-环境…

SLAM从入门到精通(dwa算法)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 要说搜路算法&#xff0c;这个大家都比较好理解。毕竟从一个地点走到另外一个地点&#xff0c;这个都是直觉上可以感受到的事情。但是这条道路上机…

行情分析——加密货币市场大盘走势(10.17)

大饼昨日在受到假消息美国证券交易委员会&#xff08;SEC&#xff09;通过大饼ETF后迅速上涨&#xff0c;一度上涨到30000&#xff0c;而很快回落到28000附近。从MACD日线来看&#xff0c;现在完全进入多头趋势&#xff0c;同时大饼再次进入蓝色上涨趋势线&#xff0c;目前按照…

李宏毅机器学习笔记-半监督学习

半监督学习&#xff0c;一般应用于少量带标签的数据&#xff08;数量R&#xff09;和大量未带标签数据的场景&#xff08;数量U&#xff09;&#xff0c;一般来说&#xff0c;U>>R。 半监督学习一般可以分为2种情况&#xff0c;一种是transductive learning&#xff0c;…

使用秋云 ucharts echarts 高性能跨全端图表组件 流程

1. 2. // 引入 import qiunDataCharts from ../../uni_modules/qiun-data-charts/components/qiun-data-charts/qiun-data-charts.vue // 注册 components:{qiunDataCharts },// 页面中使用 <qiun-data-charts type"line" :opts"opts" :chartData"…

04 MIT线性代数-矩阵的LU分解 Factorization into A=LU

目的: 从矩阵的角度理解高斯消元法, 完成LU分解得到ALU 1.矩阵乘积的逆矩阵 Inverse of a product 2.矩阵乘积的转置 Transpose of a product 3.转置矩阵的逆矩阵 Inverse of a transpose 4.矩阵的LU分解 U为上三角阵(Upper triangular matrix), L为下三角阵(Lower triangular…

pycharm远程连接miniconda完整过程,以及遇到的问题解决

问题1&#xff1a;no-zero exit code(126) env: ‘/home/user2/miniconda3/envs/ihan/bin/python3’: Too many levels of symbolic links Python interpreter process exited with a non-zero exit code 126 因为选择的新建导致太多软连接&#xff0c;先在服务器上建好虚拟环…

【微信小程序】数字化会议OA系统之首页搭建(附源码)

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是Java方文山&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;推荐给大家我的专栏《微信小程序开发实战》。&#x1f3af;&#x1f3a…