【数据结构】详解堆的基本结构及其实现

文章目录

  • 前言
  • 1.堆的相关概念
    • 1.1堆的概念
    • 1.2堆的分类
      • 1.2.1小根堆
      • 1.2.2大根堆
    • 1.3堆的特点
    • 堆的实用场景
  • 2.堆的实现
    • 2.1初始化
    • 2.2插入
    • 2.3堆的向上调整
    • 2.4删除
    • 2.5堆的向下调整
    • 2.6判空
    • 2.7获取堆顶元素
    • 2.8销毁
  • 3.堆排序
    • 3.1实现
    • 3.2堆排序的时间复杂度问题

前言

在上一篇文章中,我们已经了解了树和二叉树的概念,而下面我们要学习的堆,在二叉树中非常重要;

如果的二叉树还不太了解的,大家可以参考作者的上一篇文章
详解二叉树

1.堆的相关概念

1.1堆的概念

现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事
一个是数据结构,一个是操作系统中管理内存的一块区域分段

1.2堆的分类

对于堆我们可以分成两种:
大堆和小堆;

1.2.1小根堆

1.小堆
在小堆中,堆中的某个节点的值总是不小于其父节点的值,换句话说就是父节点的值永远不大于其子节点,而如果有两个子节点时,子节点之间不存在大小限制关系;即指在逻辑上的二叉树结构中,根结点<=子结点,总是最大的,并且在堆的每一个局部都是如此。例如{1,2,3}可以看作为小根堆,而{1,3,2}亦可以看作为小根堆。小根堆的根结点在整个堆中是最小的元素。

1.2.2大根堆

2.大堆
在大堆中,堆中的某个节点的值总是不大于其父节点的值,换句话说就是父节点的值永远不小于其子节点,而如果有两个子节点时,子节点之间不存在大小限制关系;即指在逻辑上的二叉树结构中,根结点>=子结点,总是最大的,并且在堆的每一个局部都是如此。例如{3,1,2}可以看作为大根堆,而{3,2,1}亦可以看作为大根堆。大根堆的根结点在整个堆中是最大的元素。
详情请看下图:
在这里插入图片描述

1.3堆的特点

对于二叉树来说,我们用堆来实现,那为什么不用数组来实现呢?
因为对于普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树(也就是堆)更适合使用顺序结构存储。
并且堆还具有以下的特点,让它更加高效和适用:
1.维护有序性
最大堆:每个节点的值都大于或等于其子节点的值,堆顶元素始终是最大值。
最小堆:每个节点的值都小于或等于其子节点的值,堆顶元素始终是最小值。
这种特性使得堆在需要频繁查找最大或最小元素的场景(如优先队列)中极为高效,无需遍历整个数组即可快速获得。
2.动态调整
堆支持插入和删除元素的同时能够高效地(通常是O(log n)时间复杂度重新调整结构以维持其特性,这一点在使用数组时难以直接高效实现。
3.内存利用率
实际实现时,堆可以采用数组来存储,虽然逻辑上是树状结构,但实际上占用的是连续内存空间,因此内存使用相对高效
4.排序应用
堆可以作为实现堆排序的基础,这是一种不稳定的排序算法,其优势在于能够提供较好的最坏情况和平均时间复杂度(O(n log n)),并且不需要像快速排序那样依赖于数据的初始分布。

堆的实用场景

1、我们可以利用堆的性质来找出一个序列中最大/小的元素,尽管通过遍历来解决这一问题可能更好。
2、堆排序,堆排序即利用堆的思想来进行排序,总共分为两个步骤:

1.建堆
升序:建大堆
降序:建小堆
2.利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。

3、建立优先级队列,根据上述的小结可知,若利用堆来建立优先级队列,可以快速的获取到队列中优先级最高/低的任务。
4、n个元素中排列出前k大的元素问题,对于此类问题,可以建立一个小根堆,依次读入n个元素并调整,并在堆的规模达到k+1时,剔除掉第1个元素,剩下k个较大元素,保持堆的规模不超过k,一直循环即可得到最终的结果。

2.堆的实现

实现堆,首先要知道堆在结构体中的结构是怎样的;
//堆的结构
typedef int HeapTypeData;
typedef struct heap
{HeapTypeData* a;int size;int capacity;
}HP;
还有我们要实现的一些接口:
//初始化
void HPInit(HP* php);
//插入
void HPPush(HP* php, HeapTypeData x);
//删除
void HPPop(HP* php); 
//判空
bool HPEmpty(HP* php);
//获取堆顶
HeapTypeData HPTop(HP* php);
//销毁
void HPDestory(HP* php);
//向上调整
void AdjustUp(HeapTypeData* a, int n, int parent);
//向下调整
void AdjustDown(HeapTypeData* a, int child);
//交换
void Swap(HeapTypeData* p1, HeapTypeData* p2);

下面我们就来一一实现;

2.1初始化

初始化的过程和顺序表的相似
void HPInit(HP* php)
{assert(php);php->a = NULL;php->size = php->capacity = 0;
}

2.2插入

1.,由于我们是用堆实现的二叉树的顺序结构,在存储结构上还是数组,所以当我们插入时要进行扩容;
2.当我们在尾端插入一个元素时,堆所满足的大根堆或小根堆的条件可能会被违背,所以我们还要再创建一个调整函数AdjustUp将数据进行调整,那么既然要调整,就还需要一个
交换函数swap将数据进行交换

void HPPush(HP* php, HPDataType x)
{assert(php);if (php->size == php->capacity){int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;HPDataType* tmp = (HPDataType*)realloc(php->a, newcapacity * sizeof(HPDataType));if (tmp == NULL){perror("realloc fail");return;}php->a = tmp;php->capacity = newcapacity;}php->a[php->size] = x;php->size++;AdjustUp(php->a, php->size - 1);
}

//交换

void Swap(HPDataType* p1, HPDataType* p2)
{HPDataType tmp = *p1;*p1 = *p2;*p2 = tmp;
}

2.3堆的向上调整

1.计算父节点位置:首先创建孩子节点下标的索引child(将最后一个叶子节点数据也就是我们插入的数据的下标)计算出其父节点的索引parent,公式为(child - 1) / 2;
2.循环比较并交换:进入一个循环,在循环中不断比较当前孩子节点和其父节点的值。如果孩子节点的值小于父节点的值(对于x小根堆而言),则交换这两个节点的值。这是因为小根堆要求父节点的值不大于子节点的值;
3.更新索引并继续:在交换之后,原来的子节点变成了新的父节点,因此需要更新child为parent,同时基于新的child计算新的parent,继续进行比较和可能的交换,直到孩子节点不再小于其父节点或者到达了堆的根部(即child <= 0时);
4.退出循环:一旦发现孩子节点不小于其父节点,或者已经没有父节点可比较(即到达了树的根),循环结束,此时堆的性质已经得到恢复;

注意:1.这里我们创建的child,parent,所指向的都是节点的下标
2.这里我们使用小根堆来进行示范,当调整大根堆时只需要将循环中if语句的判断符号改为大于号就可以了;
void AdjustUp(HPDataType* a, int child)
{// 初始条件// 中间过程// 结束条件int parent = (child - 1) / 2;//while (parent >= 0)错误while (child > 0){if (a[child] < a[parent]){Swap(&a[child], &a[parent]);child = parent;parent = (child - 1) / 2;}else{break;}}
}

2.4删除

删除我们也要考虑堆的性质问题——是否成立
所以这里也需要判断,并进行调整,而删除时由于删除的时堆顶的数据,所以我们要有一个向下调整的函数来调整整个堆;那么我们删除的逻辑就是:
1.将堆顶的元素和堆尾的元素进行交换,这样删除时更加简单,只需要将size–就可以了;
2.我们再把交换到堆顶的元素,进行调整,使之满足堆的成立条件;这里我们还是用小根堆进行示范;

void HPPop(HP* php)
{assert(php);assert(php->size > 0);Swap(&php->a[0], &php->a[php->size-1]);php->size--;AdjustDown(php->a, php->size, 0);
}

2.5堆的向下调整

1.初始化子节点位置:首先创建出计算出当前父节点的左孩子的索引child,公式为parent * 2 + 1。
2.循环比较并交换:进入循环,只要child的值小于n,表示还有子节点可以比较。

… 1.选择较小的子节点:如果右孩子存在(即child + 1 < n)且右孩子的值小于左孩子的值,则将child更新为右孩子的索引,因为我们要找到两个子节点中更小的那个。
… 2.比较并交换:如果找到的子节点child的值小于其父节点parent的值,说明违反了最小堆的性质,这时需要交换它们的值,并将当前的child位置作为新的父节点位置继续向下比较。
… 3.更新位置:交换后,原child位置已成为新的父节点位置,因此更新parent = child,并基于新的父节点重新计算其左孩子的索引child = parent * 2 + 1,继续循环。
… 4.退出条件:如果子节点不小于父节点,或者已经没有更多的子节点可比较(即child >= n),则跳出循环。

3.结束:循环结束后,堆的性质得到了恢复,即以parent为根的子树满足最小堆的定义。

void AdjustDown(HPDataType* a, int n, int parent)
{// 先假设左孩子小int child = parent * 2 + 1;while (child < n)  // child >= n说明孩子不存在,调整到叶子了{// 找出小的那个孩子if (child + 1 < n && a[child + 1] < a[child]){++child;}if (a[child] < a[parent]){Swap(&a[child], &a[parent]);parent = child;child = parent * 2 + 1;}else{break;}}
}

2.6判空

bool HPEmpty(HP* php)
{assert(php);return php->size == 0;
}

2.7获取堆顶元素

HPDataType HPTop(HP* php)
{assert(php);assert(php->size > 0);return php->a[0];
}

2.8销毁

销毁时要注意一一销毁,并且把变量置为零;

void HPDestroy(HP* php)
{assert(php);free(php->a);php->a = NULL;php->size = php->capacity = 0;
}

3.堆排序

3.1实现

堆排序首先要建堆,建堆时:
1.升序,建大堆;
2.降序,建小堆;
而对于这两种建堆方式,我们选择用第二种,因为我们从时间复杂度的角度去对比时会发现:
1.建大堆的时间复杂度为O(N*logN);
2.建小堆的则为O(N);
所以我们选择第二种;

1.建堆:
我们从最后一个非叶节点开始逆序遍历至根节点的方式来构建最小堆。这样做的目的是直接通过向上调整函数递归调整每个子树为最小堆,最终整个数组构成一个最小堆。计算最后一个非叶节点的索引公式为(sz-1-1)/2,然后从这个索引开始,逐步向前执行向上调整操作。
(sz-1-1)/2:sz-1->最后一个元素,(sz-1-1)/2找到父节点
2.排序:
首先,将堆顶元素(数组中的最小值)与数组末尾元素交换,确保当前最小值位于正确的位置(即数组末尾)。
然后,因为堆顶元素(现在是数组的末尾元素)已经正确排序,所以缩小堆的有效大小(end -= 1),并在剩下的元素中再次调用向上调整函数调整剩余元素为最小堆。这个过程重复,直到整个数组都被正确排序。

void HeapSort(int* a, int n)
{// 降序,建小堆// 升序,建大堆// 向上调整建堆 O(N*logN)/*for (int i = 1; i < n; i++){AdjustUp(a, i);}*/// 向下调整建堆 O(N)for (int i = (n-1-1)/2; i >= 0; i--){AdjustDown(a, n, i);}// O(N*logN)int end = n - 1;while (end > 0){Swap(&a[0], &a[end]);AdjustDown(a, end, 0);--end;}
}

3.2堆排序的时间复杂度问题

我们通过两张图来理解:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

最后得出结果为O(N*logN);

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

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

相关文章

TMS320F280049 ECAP模块--总览(0)

ECAP 特性&#xff1a; 4个32bit的事件时间戳寄存器&#xff1b; 4个连续时间戳捕获事件的边沿极性可选上升沿、下降沿 4个事件中每个都能触发中断 4个事件都能做单词触发 可以连续捕获4个事件 绝对的捕获时间戳 差异模式捕获 不使用捕获模式时&#xff0c;可以配置输出…

Python 图书馆管理系统 有GUI界面 【含Python源码 MX_031期】

使用python3&#xff0c;PyQt5&#xff0c;Sqlite3数据库搭建 主要功能&#xff1a; 用户注册、登录、修改密码、用户管理存储图书信息、采购增加和淘汰删除功能、租借功能实现图书采购、淘汰、租借功能。实现查询图书信息、采购和淘汰、库存、和租借情况实现统计图书的采购、…

JavaScript 基础 - 对象

对象 对象是一种无序的数据集合&#xff0c;可以详细的描述描述某个事物。 注意数组是有序的数据集合。它由属性和方法两部分构成。 语法 声明一个对象类型的变量与之前声明一个数值或字符串类型的变量没有本质上的区别。 <script>let 对象名 {属性名&#xff1a;属性值…

accelerate笔记:实验跟踪

Accelerate支持七种集成的跟踪器&#xff1a; TensorBoardWandBCometMLAimMLFlowClearMLDVCLive要使用这些跟踪器&#xff0c;可以通过在 Accelerator 类的 log_with 参数中传入所选类型来实现 from accelerate import Accelerator from accelerate.utils import LoggerTypeac…

高通开发系列 - ubuntu中的docker安装debian镜像

By: fulinux E-mail: fulinux@sina.com Blog: https://blog.csdn.net/fulinus 喜欢的盆友欢迎点赞和订阅! 你的喜欢就是我写作的动力! 返回:专栏总目录 目录 概述当前状态Ubuntu中安装dockerDebian镜像Debian容器中操作更改Debian源安装应用程序

28 _ WebComponent:像搭积木一样构建Web应用

在上一篇文章中我们从技术演变的角度介绍了PWA&#xff0c;这是一套集合了多种技术的理念&#xff0c;让浏览器渐进式适应设备端。今天我们要站在开发者和项目角度来聊聊WebComponent&#xff0c;同样它也是一套技术的组合&#xff0c;能提供给开发者组件化开发的能力。 那什么…

python 各种画图(2D 3D)-1 _matplotlib 官方网站笔记

背景 需利用python进行3D可视化处理&#xff0c;用于分析python得到的数据的正确性。 知识学习 python高阶3D绘图---pyvista模块&#xff0c;mayavi模块&#xff0c;pyopengl模块&#xff0c;MoviePy模块基础使用-CSDN博客 python用于3D绘图的模块比较多&#xff0c;pyvist…

目标2亿欧元!四年两次募资,全球最早专注于量子投资的Quantonation再次加码

Quantonation Ventures 是全球第一家专注于深度物理和量子技术的早期风险投资公司。4月10日&#xff0c;该公司宣布其第二只专门用于量子技术的早期基金 Quantonation II 首次募资完成&#xff0c;目前已募资 7000 万欧元&#xff0c;而目标为 2 亿欧元。 首次募资就募到了将…

《QT从基础到进阶·四十一》无法解析的外部符号及生成事件加入QT打包命令报错问题

其他无法解析的外部符号&#xff1a; 无法解析的外部符号 "public: virtual struct QMetaObject const * __cdecl ML_AddinManger::metaObject(void)const "… 无法解析的外部符号 “public: virtual void * __cdecl ML_AddinManger::qt_metacast(char const *)” (?…

toefl listening_托福听力

x.1 课程介绍 x.1.1 课程介绍 考试介绍 注意事项如下&#xff0c; x.1.2 分数设定和方法论 x.2.1 细节题解法 x.2.2 对话主旨题解法 听力对话不要扣分&#xff1b; 内容主旨题&#xff0c;以what开头&#xff1b; 目的主旨题&#xff0c;以why开头&#xff1b; 目的主旨题…

SpringCloud中注册中心Nacos的下载与使用步骤

1.前言 Nacos&#xff08;Dynamic Naming and Configuration Service&#xff09;是阿里巴巴开源的一款服务发现和配置管理工具。它可以帮助用户自动化地进行服务注册、发现和配置管理&#xff0c;是面向微服务架构的一个重要组成部分。 2.下载 链接&#xff1a;https://pan.b…

奶茶店、女装店、餐饮店是高危创业方向,原因如下:

关注卢松松&#xff0c;会经常给你分享一些我的经验和观点。 现在的俊男靓女们&#xff0c;心中都有一个执念&#xff1a; (1)想证明自己了&#xff0c;开个奶茶去…… (2)想多赚点钱了&#xff0c;加盟餐饮店去…… (3)工作不顺心了&#xff0c;搞个女装店去…… 但凡抱着…

回溯--字母迷宫

1.题目描述 字母迷宫游戏初始界面记作 m x n 二维字符串数组 grid&#xff0c;请判断玩家是否能在 grid 中找到目标单词 target。 注意&#xff1a;寻找单词时 必须 按照字母顺序&#xff0c;通过水平或垂直方向相邻的单元格内的字母构成&#xff0c;同时&#xff0c;同一个单…

Windows系统下DOS命令

Windows系统下DOS命令 1. 与文件操作相关1.1 mkdir&#xff0c;md命令1.2 rmdir、rd命令1.3 dir命令1.4 start命令1.5 echo命令1.6 type命令1.7 copy命令1.8 move命令1.9 copy和move的区别1.10 del命令1.11 rename命令1.12 attrib命令1.13 fsutil命令1.14 assoc命令 2. 与网络相…

数据持久化第六课-ASP.NET运行机制

数据持久化第六课-ASP.NET运行机制 一.预习笔记 1.动态网页的工作机制通常分为以下几个阶段&#xff1a; 1&#xff09;使用动态Web开发技术编写Web应用程序&#xff0c;并部署到Web服务器。 2&#xff09;客户端通过在浏览器中输入地址&#xff0c;请求动态页面。 3&#…

机器学习之数学基础(六)~时间复杂度和空间复杂度

目录 算法背景 background 1. 时间复杂度 Time Complexity 1.1 时间复杂度分类 1.1.1 O(1) 常数阶 1.1.2 O(n) 线性阶 1.1.3 O(n^2) 平方阶 1.1.4 O(logn) 对数阶 1.1.5 O(nlogn) 线性对数阶 1.1.6 O(2^n) 指数阶 1.1.7 O(n!) 阶乘阶 1.1.8 时间复杂度分类 1.2 时…

03-07Java自动化之JAVA基础之循环

JAVA基础之循环 一、for循环 1.1for循环的含义 for&#xff08;初始化语句;条件判断;条件控制或–&#xff09;{ ​ //代码语句 } 1、首先执行初始话语句&#xff0c;给变量一个起始的值 2、条件判断进行判断&#xff0c;为true&#xff0c;执行循环体中的代码语句 ​ …

3DGS语义分割之LangSplat

LangSplat是CVPR2024的paper. 实现3DGS的语义分割&#xff08;可文本检索语义&#xff09; github: https://github.com/minghanqin/LangSplat?tabreadme-ov-file 主要思想是在3DGS中加入了CLIP的降维语义特征&#xff0c;可用文本检索目标&#xff0c;实现分割。 配置环境&…

网线水晶头为什么要按标准线序打

网线接水晶头为什么要按照线序接&#xff1f; 减少串扰和增强信号质量&#xff1a; 双绞线的设计是为了减少信号间的串扰&#xff08; Crosstalk&#xff09;&#xff0c;每一对线芯在传输过程中通过相互扭绞抵消外部电磁干扰。按照标准线序接线能够确保每一对线芯之间的信号传…

Ubuntu server 24 (Linux) 安装部署smartdns 搭建智能DNS服务器

SmartDNS是推荐本地运行的DNS服务器&#xff0c;SmartDNS接受本地客户端的DNS查询请求&#xff0c;从多个上游DNS服务器获取DNS查询结果&#xff0c;并将访问速度最快的结果返回给客户端&#xff0c;提高网络访问速度和准确性。 支持指定域名IP地址&#xff0c;达到禁止过滤的效…