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

堆(超详解)

文章目录

  • 前言
  • 一、堆的概念及结构
  • 二、堆的实现
    • 2.1 堆的向下调整算法
    • 2.2 堆的向上调整算法
    • 2.3 初始化堆
    • 2.4 销毁堆
    • 2.5 打印堆
    • 2.6 堆的插入
    • 2.7 堆的删除
    • 2.8 获取堆顶的元素
    • 2.9 获取堆的数据个数
    • 2.10 堆的判空
    • 2.11 建堆
      • 2.11.1 向上调整建堆(逐个插入建堆:自顶向下)
      • 2.11.2 向下调整建堆(直接数组建堆:自底向上)
      • 2.11.3 向上调整vs向下调整


前言

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段
在这里插入图片描述

由于堆总是一颗完全二叉树,因此数组便成为了用来存储堆的最好方式。数组最大的优点就是支持随机访问,在数组中,只要我们知道其中一个结点的下标,便可以通过完全二叉树父结点和孩子结点的编号关系,快速定位到其父结点或孩子结点

一、堆的概念及结构

在这里插入图片描述
堆的性质:

  1. 堆中某个结点的值总是不大于或不小于其父结点的值
  2. 堆总是一颗完全二叉树

在这里插入图片描述
练习题:
1 .已知小根堆为8,15,10,21,34,16,12,删除关键字 8 之后需重建堆,在此过程中,关键字之间的比较次数是()
 A.1
 B.2
 C.3
 D.4
2 .一组记录排序码为(5 11 7 2 3 17),则利用堆排序方法建立的初始堆为
 A.(11 5 7 2 3 17)
 B.(11 5 7 2 17 3)
 C.(17 11 7 2 3 5)
 D.(17 11 7 5 3 2)
 E.(17 7 11 3 5 2)
 F.(17 7 11 3 2 5)

解析:
1.(答案:C)在删除小根堆的根节点8后,将最后一个元素12移动到根位置,堆变为:12, 15, 10, 21, 34, 16。随后进行向下调整以恢复堆性质
调整过程如下:
根节点12有两个子节点15和10。首先比较两个子节点15和10(第一次比较),找到较小值10。然后比较根节点12与较小子节点10(第二次比较),由于12 > 10,交换两者。堆变为:10, 15, 12, 21, 34, 16
现在节点12位于索引3位置,它只有一个左子节点16。比较节点12与子节点16(第三次比较),由于12 < 16,不需要交换,调整结束
在此过程中,关键字之间的比较次数共计3次

2.(答案:C)根据堆排序方法,建立初始堆(大顶堆)的过程如下:
给定记录排序码为 (5, 11, 7, 2, 3, 17),需要将其调整为大顶堆,即每个节点的值都大于或等于其子节点的值。
调整步骤:
①从最后一个非叶子节点(索引 2,值 7)开始调整。比较节点 7 与其子节点(索引 5,值 17),由于 7 < 17,交换两者,数组变为 (5, 11, 17, 2, 3, 7)。
②调整索引 1(值 11)。其子节点为索引 3(值 2)和索引 4(值 3),较大值为 3。比较 11 和 3,11 > 3,无需交换。
③调整索引 0(值 5)。其子节点为索引 1(值 11)和索引 2(值 17),较大值为 17。比较 5 和 17,5 < 17,交换两者,数组变为 (17, 11, 5, 2, 3, 7)。
④由于交换后索引 2(值 5)可能违反堆性质,调整索引 2。其子节点为索引 5(值 7),比较 5 和 7,5 < 7,交换两者,数组变为 (17, 11, 7, 2, 3, 5)。

二、堆的实现

2.1 堆的向下调整算法

现在我们给出一个数组,逻辑上看作一棵完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆

但是,使用向下调整算法需要满足一个前提:
 若想将其调整为小堆,那么根结点的左右子树必须都为小堆
 若想将其调整为大堆,那么根结点的左右子树必须都为大堆
在这里插入图片描述
向下调整算法的基本思想(以建小堆为例):

  1. 从根结点处开始,选出左右孩子中值较小的孩子
  2. 让小的孩子与其父亲进行比较
     ①若小的孩子比父亲还小,则该孩子与其父亲的位置进行交换。并将原来小的孩子的位置当成父亲继续向下进行调整,直到调整到叶子结点为止
     ②若小的孩子比父亲大,则不需处理了,调整完成,整个树已经是小堆了
//交换函数
void swap(HPDateType* p1, HPDateType* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}//向下调整(小堆)
void AdjustDown(HPDateType* a, int n, int parent)
{//child记录左右孩子中值较小的孩子的下标int child = parent * 2 + 1;//假设左孩子的值较小while (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.2 堆的向上调整算法

当我们在一个堆的末尾插入一个数据后,需要对堆进行调整,使其仍然是一个堆,这时需要用到堆的向上调整算法
在这里插入图片描述
向上调整算法的基本思想(以建小堆为例):

  1. 将目标结点与其父结点比较
  2. 若目标结点的值比其父结点的值小,则交换目标结点与其父结点的位置,并将原目标结点的父结点当作新的目标结点继续进行向上调整。若目标结点的值比其父结点的值大,则停止向上调整,此时该树已经是小堆了
//向上调整(小堆)
void AdjustUp(HPDateType* a, int child)
{int parent = (child - 1) / 2;//调整到根节点的位置截止while (child > 0){//孩子结点的值小于父结点的值if (a[child] < a[parent]){//将父结点与孩子结点进行交换swap(&a[child], &a[parent]);//继续向上进行调整child = parent;parent = (child - 1) / 2;}//已成堆else{break;}}
}

2.3 初始化堆

首先,必须创建一个堆类型,该类型中需包含堆的基本信息:存储数据的数组、堆中元素的个数以及当前堆的最大容量

typedef int HPDateType;//堆中存储数据的类型
typedef struct Heap
{HPDateType* a;//用于存储数据的数组int size;//记录堆中已有元素个数int capacity;//记录堆的容量
}HP;

然后我们需要一个初始化函数,对刚创建的堆进行初始化

//初始化
void HeapInit(HP* php)
{assert(php);php->a = NULL;php->size = 0;php->capacity = 0;
}

2.4 销毁堆

为了避免内存泄漏,使用完动态开辟的内存空间后都要及时释放该空间,所以,一个用于释放内存空间的函数是必不可少的

//销毁
void HeapDestroy(HP* php)
{assert(php);free(php->a);//释放动态开辟的数组php->a = NULL;//及时置空php->size = 0;//元素个数置0php->capacity = 0;//容量置0
}

2.5 打印堆

打印堆中的数据,按照堆的物理结构进行打印,即打印为一排连续的数字


//打印
void HeapPrint(HP* php)
{assert(php);//按照物理结构进行打印int i = 0;for (i = 0; i < php->size; i++){printf("%d ", php->a[i]);}printf("\n");

2.6 堆的插入

数据插入时是插入到数组的末尾,即树形结构的最后一层的最后一个结点,所以插入数据后我们需要运用堆的向上调整算法对堆进行调整,使其在插入数据后仍然保持堆的结构
在这里插入图片描述
先插入一个10到数组的尾上,再进行向上调整算法直到满足堆

//插入
void HeapPush(HP* php, HPDateType x)
{assert(php);if (php->size == php->capacity){HPDateType* tmp = (HPDateType*)realloc(php->a, 2 * php->capacity * sizeof(HPDateType));if (tmp == NULL){printf("realloc fail\n");exit(-1);}php->a = tmp;php->capacity *= 2;}php->a[php->size] = x;php->size++;//向上调整AdjustUp(php->a, php->size - 1);
}

2.7 堆的删除

堆的删除,删除的是堆顶的元素,但是这个删除过程可并不是直接删除堆顶的数据,而是先将堆顶的数据与最后一个结点的位置交换,然后再删除最后一个结点,再对堆进行一次向下调整

原因:我们若是直接删除堆顶的数据,那么原堆后面数据的父子关系就全部打乱了,需要全体重新建堆,时间复杂度为O(N)。若是用上述方法,那么只需要对堆进行一次向下调整即可,因为此时根结点的左右子树都是小堆,我们只需要在根结点处进行一次向下调整即可,时间复杂度为O(log(N))

//删除
void HeapPop(HP* php)
{assert(php);assert(!HeapEmpty(php));swap(&php->a[0], &php->a[php->size - 1]);//交换堆顶和最后一个结点的位置php->size--;//删除最后一个结点(也就是删除原来堆顶的元素)AdjustDown(php->a, php->size, 0);//向下调整
}

2.8 获取堆顶的元素

获取堆顶的数据,即返回数组下标为0的数据

//获得堆顶元素
HPDateType HeapTop(HP* php)
{assert(php);assert(!HeapEmpty(php));return php->a[0];
}

2.9 获取堆的数据个数

获取堆的数据个数,即返回堆结构体中的size变量

//获取堆中数据个数
int HeapSize(HP* php)
{assert(php);return php->size;
}

2.10 堆的判空

堆的判空,即判断堆结构体中的size变量是否为0

//判断是否为空
bool HeapEmpty(HP* php)
{assert(php);return php->size == 0;
}

2.11 建堆

2.11.1 向上调整建堆(逐个插入建堆:自顶向下)

核心思想:

  • 从空堆开始,逐个往堆的末尾插入元素
  • 每次插入后,通过向上调整(AdjustUp)维护堆性质
  • 类似于"边建边调整"的策略

在这里插入图片描述

2.11.2 向下调整建堆(直接数组建堆:自底向上)

核心思想:

  • 一次性将整个数组视为完全二叉树
  • 从最后一个非叶子节点开始,通过向下调整(AdjustDown)构建堆
  • 采用"先建后调整"的策略

在这里插入图片描述
在之前,我们说过使用向下调整算法有一个前提:左右子树必须是个堆。那么我们显然不能从前往后进行调整,因为堆顶的左右子树不是一个堆。为了保证调整某个元素时它的左右子树已经是堆了,我们应该从后往前逐元素进行向下调整

那么,第一个需要调整的元素是谁呢?叶子节点没有左右子树,故不需要进行调整,我们应该从第一个非叶子结点开始向下调整调整,即下标为(n-1-1)/2的元素(n为元素个数)。就这样不断调整到根结点即可完成堆的构建。下面是向下调整构建大堆的过程和代码:
在这里插入图片描述

//堆的创建,向下调整
void HeapDownCreate(HP* php, HPDateType* a, int n)
{assert(php);assert(a);HeapInit(php);//初始化堆//将数组的n个元素拷贝到堆中if (php->capacity < n)//容量不足先扩容{HPDateType* tmp = realloc(php->a, n * sizeof(HPDateType));if (tmp)php->a = tmp;elseexit(-1);php->capacity = n;}memcpy(php->a, a, sizeof(HPDateType) * n);php->size = n;//从后往前逐元素向下调整建堆//n-1最后一个叶子结点的下标//(n-1-1)/2最后一个叶子结点的父结点的下标,即第一个非叶子结点for (int i = (n - 1 - 1) / 2; i >= 0; i--){AdjustDown(php->a, n, i);}
}

2.11.3 向上调整vs向下调整

我们分析一下这两种建堆方法的时间复杂度

因为堆是完全二叉树,而满二叉树也是完全二叉树,因此为了简化运算,我们使用满二叉树来分析复杂度
在这里插入图片描述
在这里插入图片描述

我们明显可以看出“向下调整建堆”优于“向上调整建堆”,具体表现在向下调整随着层数增加每个结点的调整次数会递减,而向上调整正好相反。层数越高需要调整的结点越多,因此总体来看向下调整建堆的总调整次数会更少。我们后面的建堆均会使用向下调整来建堆

http://www.dtcms.com/a/474022.html

相关文章:

  • Java Redis “Sentinel(哨兵)与集群”面试清单(含超通俗生活案例与深度理解)
  • Eureka注册中心通用写法和配置
  • python内置函数map()解惑:将可迭代对象中的每个元素放入指定函数处理
  • 吕口*云蛇吞路的特效*程序系统方案
  • c 网站购物车怎么做.net 网站 源代码
  • 网站建设开发合同模板优秀的商城网站首页设计
  • 服务注册、服务发现、OpenFeign及其OKHttp连接池实现
  • 设计模式篇之 门面模式 Facade
  • 2026年COR SCI2区,自适应K-means和强化学习RL算法+有效疫苗分配问题,深度解析+性能实测,深度解析+性能实测
  • 广州黄浦区建设局网站网站免费模版代码
  • 寄存器技术深度解析:从硬件本质到工程实践
  • **发散创新:探索量化模型的设计与实现**一、引言随着大数据时代的到来,量化模型在金融、医疗、科研等领域的应用越来越广泛。本文将
  • windows查看端口使用情况,以及结束任务释放端口
  • 开源安全管理平台wazuh-与网络入侵检测系统集成增强威胁检测能力
  • 【004】生菜阅读平台
  • 南通网站建设兼职电商平台如何做推广
  • 守护集群与异步备库区别
  • UDP可靠性传输指南:从基础机制到KCP协议核心解析
  • SQL常用函数
  • 义乌建网站引流推广软件
  • Ansible Role修改IP地址与主机名
  • 贺Filcion五周岁:Chain Shop 10月17号正式上线
  • 部分Spark SQL编程要点
  • 【完整源码+数据集+部署教程】 飞机表面缺陷检测系统源码和数据集:改进yolo11-EfficientFormerV2
  • 工作做ppt课件的网站广州抖音seo
  • Java并发编程实战深度解析线程池ThreadPoolExecutor的设计原理与性能优化策略
  • 烟台建设公司网站兰州新区网站建设
  • OpenWrt之ipv6防火墙配置放行局域网设备的公网ipv6
  • 第一个爬虫程序:用 Requests+BeautifulSoup 抓取豆瓣电影 Top250
  • JavaScript 企业面试与学习难度拆解:从0到中高级的阶梯式路线图