数据结构--堆的深度解析

目录

引言

一、基本概念

1.1堆的概念

1.2堆的存储结构

1.3堆的特点

二、 堆的基本操作

2.1初始化

2.2创建堆

2.3插入元素 

2.4删除元素 

2.5堆化操作

2.6堆的判空

2.7获取堆顶元素

三、堆的常见应用

1. 优先队列

2. 堆排序

3. Top-k 问题

4. 图论中的应用

四、Top-k问题精讲 

方法一:遍历选择

方法二:排序

 方法三:最小堆法

总结


引言

堆(Heap)是一种非常重要的非线性数据结构,广泛应用于各种算法和问题解决中,如优先队列、堆排序、Top-k 问题等。本文将深入解析堆的定义、类型、操作、应用及其优缺点。

在上篇博客我们简单介绍了堆,并用堆实现了二叉树的顺序存储,这里我们趁热打铁深入解析堆这种结构。

一、基本概念

1.1堆的概念

堆是一种特殊的完全二叉树,具有以下性质:

  • 完全二叉树:堆是一个完全二叉树,意味着树的每一层都被完全填满,除了最后一层可能没有填满,但所有节点都集中在左侧。
  • 堆性质:堆有两种类型:
    ‧ 小顶堆(min heap):任意节点的值 ≤ 其子节点的值。
    ‧ 大顶堆(max heap):任意节点的值 ≥ 其子节点的值。

1.2堆的存储结构

堆通常使用数组进行存储,这种方式的优点是可以直接通过数组下标计算父节点和子节点的位置:

数组索引映射:

对于节点 i:

父节点:parent(i) = (i - 1) / 2(使用整数除法)
左子节点:left(i) = 2 * i + 1
右子节点:right(i) = 2 * i + 2

//定义堆的结构--数组
typedef int DataType;
typedef struct Heap//小堆为例
{DataType *arr;int size;int capacity;
}HP;

存储示例
假设堆的数组表示为 {9,8,6,6,7,5,2,1,4,3,6,2}:
7的索引是4,其父节点为8(索引1),左子节点为3(索引9),右子节点6(索引10)。

这种方式有效利用了内存,避免了指针的开销。

1.3堆的特点

堆作为完全二叉树的一个特例,具有以下特性。
‧ 最底层节点靠左填充,其他层的节点都被填满。
‧ 我们将二叉树的根节点称为“堆顶”,将底层最靠右的节点称为“堆底”。
‧ 对于大顶堆(小顶堆),堆顶元素(根节点)的值是最大(最小)的。

二、 堆的基本操作

2.1初始化

//初始化
void HPInit(HP* p)
{assert(p);p->arr = NULL;p->size = p->capacity = 0;
}

2.2创建堆

创建堆通常有两种方式:

1.逐步插入:
从一个空堆开始,逐个插入元素。每插入一个新元素后,使用“上浮”操作来维持堆的性质。

for (int i = 0; i < n; i++)
{AdjustUp(arr, i);//向上调整算法,2.5中右有详细说明
}

2.堆化(Heapify):
给定一个无序数组,可以通过一次性构建堆来提高效率。这通常采用自底向上的方法,(先假定这组数据是一个“堆结构”)从最后一个非叶节点开始,逐个进行“下沉”操作,使之调整为堆(真正具有堆的特性)。

堆化算法步骤:
对于索引 i 从 (n-1-1)/2 到 0 进行下沉操作,n 是数组的长度。

for (int i =(n-1-1)/2 ; i >= 0; i--)//n-1为数组最后一个数据的下标,(n-1-1)/2为其父节点
{AdjustDown(arr, i, n);//向下调整算法,2.5中右有详细说明
}

2.3插入元素 

在堆中插入元素的步骤如下:

1.添加元素:
将新元素添加到堆的末尾(数组的最后一个位置)。

2.上浮操作:
比较新元素与其父节点的值,如果新元素大于父节点(在最大堆中)或小于父节点(在最小堆中),则交换两者,并继续上浮直到堆性质得以恢复。

时间复杂度:O(log n)

代码实现:

//插入
void HPPush(HP* p, DataType x)
{assert(p);if (p->size == p->capacity)//判断空间是否足够{//扩容int NewCap = p->capacity == 0 ? 4 : 2 * p->capacity;DataType* tmp = (DataType*)realloc(p->arr, NewCap * sizeof(DataType));if (tmp == NULL) {perror("realloc fail!");exit(1);}p->arr = tmp;p->capacity = NewCap;} p->arr[p->size] = x;AdjustUp(p->arr, p->size);++p->size;
}

2.4删除元素 

到通常,删除操作是从堆中移除根节点(最大或最小值),其步骤如下:
1.替换根节点:
将根节点(堆顶)替换为最后一个元素,然后从数组中删除该元素。
2.下沉操作:
从根节点开始,依次将该节点与其子节点进行比较,将其值与较大的子节点(在最大堆中)或较小的子节点(在最小堆中)进行交换,直堆的性质恢复。

时间复杂度:O(log n)

 代码实现:

//删除,出堆顶数据
void HPPop(HP* p)
{assert(p && p->size);Swap(&p->arr[0], &p->arr[p->size - 1]);--p->size;AdjustDown(p->arr, 0, p->size);
}

2.5堆化操作

堆化操作是保持堆性质的关键,主要包括:
1.向上调整(AdjustUp):
用于插入操作。当新插入的元素大于(或小于)其父节点时,通过交换将其上移,直至堆性质满足。

//堆的向上调整
void AdjustUp(DataType* arr, int child)
{int parent = (child - 1) / 2;//父节点while(child>0)//注意child可能会被调整到根节点,这时候就不能再调整了{//建小堆  <  //建大堆  >if (arr[child] < arr[parent])//如果条件语句不成立,就说明堆已经成型{Swap(&arr[child], &arr[parent]);child = parent;parent = (child - 1) / 2;//循环以上步骤}else{break;}}
}
计算向上调整算法建堆时间复杂度
因为堆是完全⼆叉树,⽽满⼆叉树也是完全⼆叉树,此处为了简化使⽤满⼆叉树来证明(时间复杂度本来看的就是近似值,多⼏个结点不影响最终结果)

分析:
第1层, 2^0 个结点,需要向上移动0层
第2层, 2^1 个结点,需要向上移动1层
第3层, 2^2 个结点,需要向上移动2层
第4层, 2^3 个结点,需要向上移动3层
......
第h层, 2^(h-1) 个结点,需要向上移动h-1层
则需要移动结点总的移动步数为:每层结点个数 * 向上调整次数(第⼀层调整次数为0)

由此可知,

向上调整算法建堆时间复杂度为:O(n ∗ log2 n)

2.向下调整(AdjustDown):
用于删除操作和堆化。根节点(或其他节点)与其子节点进行比较,将其值与较大的子节点(或较小的子节点)进行交换,直至堆性质满足。

//堆的向下调整
void AdjustDown(DataType* arr, int parent, int n)
{int child = parent * 2 + 1;//左孩子while (child < n) {//小堆:寻找左右孩子最小的那个//大堆:寻找左右孩子最大的那个if (child + 1 < n && arr[child] > arr[child + 1]){child++;}//这里和向上调整就一样了,比较,交换,循环//建小堆  <   //建大堆  >if (arr[child] < arr[parent]){Swap(&arr[child], &arr[parent]);parent = child;child = parent * 2 + 1;}else{break;}}
}
计算向下调整算法建堆时间复杂度

分析:
第1层, 2^0 个结点,需要向下移动h-1层
第2层, 2^1 个结点,需要向下移动h-2层
第3层, 2^2 个结点,需要向下移动h-3层
第4层, 2^3 个结点,需要向下移动h-4层
......
第h-1层, 2^(h-2) 个结点,需要向下移动1层

由此可知,

向下调整算法建堆时间复杂度为:O(n)

2.6堆的判空

堆的大小为0,堆为空,反之则非空。

//判空
bool HPEmpty(HP* p)
{assert(p);return p->size == 0;
}

2.7获取堆顶元素

获取堆的根节点(最大或最小值)非常简单,只需返回数组的第一个元素。

//返回堆顶元素
DataType HPTop(HP* p)
{assert(p && p->size);return p->arr[0];
}

三、堆的常见应用

1. 优先队列

优先队列是一种数据结构,允许根据优先级处理元素。堆可以高效地实现优先队列,支持在 O(logn) 时间内插入和删除元素,适用于任务调度和图算法(如 Dijkstra 算法)。

2. 堆排序

堆排序是一种基于堆的排序算法,时间复杂度为 O(nlogn)。它通过构建最大堆并逐步提取最大元素来实现排序,适合需要原地排序的场景。

3. Top-k 问题

Top-k 问题涉及从数据集中找出前 k 个最大或最小的元素。使用最小堆可以在 O(nlogk) 的时间复杂度内有效解决该问题,常用于数据分析和排行榜。

4. 图论中的应用

在图算法中,堆常用于 Dijkstra 和 Prim 算法,通过优先队列高效管理节点和边,从而加速最短路径和最小生成树的计算。

下面我们来详细讲一下 Top-k问题

四、Top-k问题精讲 

方法一:遍历选择

我们可以进行 𝑘 轮遍历,分别在每轮中提取第 1 2 𝑘 大的元素,时间复杂度为 𝑂(𝑛𝑘)
此方法只适用于 𝑘 ≪ 𝑛 的情况,因为当 𝑘 𝑛 比较接近时,其时间复杂度趋向于 𝑂(𝑛^ 2 ) ,非常耗时。 (当 𝑘 = 𝑛 时,我们可以得到完整的有序序列,此时等价于“选择排序”算法。 )

方法二:排序

我们可以先对数组 nums 进行排序,再返回最右边的 𝑘 个元素,时间复杂度取决于排序所使用的算法,最快为 𝑂(𝑛 log 𝑛)
显然,该方法“超额”完成任务了,因为我们只需找出最大的 𝑘 个元素即可,而不需要排序其他元素

 方法三:最小堆法

1. 初始化一个小顶堆,其堆顶元素最小。
2. 先将数组的前 𝑘 个元素依次入堆。
3. 从第 𝑘 + 1 个元素开始,若当前元素大于堆顶元素,则将堆顶元素出堆,并将当前元素入堆。
4. 遍历完成后,堆中保存的就是最大的 𝑘 个元素。

 代码实现:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>typedef int DataType;// 堆的结构
typedef struct Heap
{DataType* arr;int size;int capacity;
} HP;// 交换两个元素
void Swap(DataType* x, DataType* y)
{DataType tmp = *x;*x = *y;*y = tmp;
}
// 向下调整堆
void AdjustDown(DataType* arr, int parent, int n)
{int child = parent * 2 + 1; // 左孩子while (child < n) {if (child + 1 < n && arr[child] > arr[child + 1]){child++;}if (arr[child] < arr[parent]) {Swap(&arr[child], &arr[parent]);parent = child;child = parent * 2 + 1;}else{break;}}
}// 建小堆
void BuildMinHeap(int* arr, int k)
{for (int i = (k-1-1) / 2; i >= 0; i--){AdjustDown(arr, i, k);}
}// 查找Top-k元素
void FindTopK(DataType* arr, int n, int k) {if (k <= 0 || k > n){printf("k的值非法\n");return;}//开辟所需小堆的内存int* minheap = (int*)malloc(sizeof(int) * k);if (minheap == NULL){perror("malloc error");return;}// 将前k个元素放入小堆for (int i = 0; i < k; i++){minheap[i] = arr[i];}BuildMinHeap(minheap, k);// 遍历剩余元素,更新小堆for (int i = k; i < n; i++){if (arr[i] > minheap[0]){minheap[0] = arr[i]; // 替换堆顶AdjustDown(minheap, 0, k); // 重新调整堆}}// 输出Top-k元素printf("最大的前%d个元素为:\n", k);for (int i = 0; i < k; i++){printf("%d ", minheap[i]);}printf("\n");// 释放内存free(minheap);
}int main() {DataType array[] = {1,3,5,7,9,2,4,6,8,10};int n = sizeof(array) / sizeof(array[0]);int k = 3; // 查找前3个最大的元素FindTopK(array, n, k);return 0;
}
总共执行了 𝑛 轮入堆和出堆,堆的最大长度为 𝑘 ,因此时间复杂度为 𝑂(𝑛 log 𝑘) 。该方法的效率很高,当 𝑘 较小时,时间复杂度趋向 𝑂(𝑛) ;当 𝑘 较大时,时间复杂度不会超过 𝑂(𝑛 log 𝑛)
另外,该方法适用于动态数据流的使用场景。在不断加入数据时,我们可以持续维护堆内的元素,从而实现最大的 𝑘 个元素的动态更新。

总结

堆是一种强大的数据结构,使用堆排序解决 Top-k 问题是一种高效且简洁的方法。通过维护一个最小堆,我们能够在遍历数据的同时有效地找出前 k 个最大元素。这种方法在实际应用中非常有用,特别是在处理大规模数据时。如果你对堆或其他相关数据结构有进一步的兴趣,欢迎留言讨论!

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=3avhxavk524g0 

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

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

相关文章

rom定制系列------小米5x_miui12安卓11定制固件界面预览 小米5x第三方固件

&#x1f49d;&#x1f49d;&#x1f49d;此固件来源于客户卡刷固件定制。客户需要修改为线刷。并且修改账号锁功能。 可以让客户使用官方平台批量进行刷写。方便操作。 定制机型以及功能预览&#x1f49d;&#x1f49d;&#x1f49d; 小米5x版本miui12.5.8安卓11固件。此机型…

MySQL 连接

使用MySQL二进制方式连接 使用MySQL二进制方式进入到MySQL命令提示符下来连接MySQL数据库。 实例 以下是从命令行中连接MySQL服务器的简单实例&#xff1a; [roothost]# mysql -u root -p Enter password:******在登录成功后会出现 mysql> 命令提示窗口&#xff0c;你可以在…

Java 文件拷贝

1.小文件拷贝 实例代码&#xff1a; 上面程序运行的图示&#xff1a; 弊端&#xff1a;一次读一个字节&#xff0c;效率太慢。所以需要一次读取多个字节。 2.大文件拷贝 结果&#xff1a;

UE5运行时动态加载场景角色动画任意搭配-全流程代码(四)

UE5运行时动态加载场景、角色、角色动画、相机动画任意搭配,Android、iOS也可以跑,横竖屏兼容,手机竖屏: 1、场景切换UWorld处理 在通过OpenLevel进行场景切换的时候,UWorld会发生变化,需要我们获取正确的UWorld。 1、在GameInstance监听Level加载 void UMyGameInsta…

数据结构——复杂度

目录 数据结构前言 数据结构 算法 算法效率 时间复杂度 大O的渐进表示法 示例1 示例2 示例3 示例4 示例5 示例6 示例7 空间复杂度 示例1 示例2 示例3 示例4 常见复杂度对比 旋转数组 优化1 优化2 这一篇文章我们就开始数据结构知识的学习&#xff01; 数据…

stm32单片机个人学习笔记10(TIM编码器接口)

前言 本篇文章属于stm32单片机&#xff08;以下简称单片机&#xff09;的学习笔记&#xff0c;来源于B站教学视频。下面是这位up主的视频链接。本文为个人学习笔记&#xff0c;只能做参考&#xff0c;细节方面建议观看视频&#xff0c;肯定受益匪浅。 STM32入门教程-2023版 细…

Jetpack-Room

Room是Android Jetpack中的一个组件&#xff0c;它提供了一个抽象层&#xff0c;帮助开发者在本地数据库&#xff08;如SQLite&#xff09;上进行持久化数据存储。Room通过简化数据库操作&#xff0c;使得数据管理变得更加容易和高效。 Room重要的概念 实体&#xff08;Entit…

Python即时获取上证指数信息并发送邮件到指定邮箱

最近股市振荡&#xff0c;股民会时不时查看一下上证指数信息&#xff0c;但是繁忙的工作却时不是让人忘记了&#xff0c;错过了投资的最佳时机&#xff0c;这时就可以通过Python来爬取网站上的上证指数&#xff0c;并发送到指定的邮箱&#xff0c;这样就不用上指定网页也以获取…

网络断开导致SSH会话和服务端任务终止的原因及使用screen详解

在进行深度学习任务时&#xff0c;常常在本地通过ssh连接远程服务器进行炼丹任务。当我在pycharm上远程连接到服务器上进行训练时&#xff0c;由于网络不稳定使得SSH断开连接&#xff0c;我原以为服务器不会受影响。通过nvidia-smi命令发现GPU占用为0&#xff0c;这才发现任务终…

稀土阻燃协效剂 - 阻燃

金士镧稀土阻燃协效剂的技术衬垫&#xff1a; 金士镧KingCela 稀土阻燃协效剂KCL-FR-06xx系列产品&#xff0c;凭借独特的稀土4f电子层结构, 可协效磷氮阻燃剂&#xff0c;卤系阻燃剂阻燃剂在高分子材料&#xff08;橡胶&#xff0c;塑料&#xff0c;涂层&#xff0c;胶黏剂&a…

『网络游戏』服务器启动逻辑【16】

新建Visual Studio工程命名为NetGameServer 重命名为ServerStart.cs 创建脚本&#xff1a; 编写脚本&#xff1a;ServerRoot.cs 编写脚本&#xff1a;ServerStart.cs 新建文件夹 调整脚本位置 新建文件夹 新建文件夹网络服务 创建脚本&#xff1a;NetSvc.cs 编写脚本&#xff1…

使用 KVM 在 Xubuntu 上创建 Windows 10 虚拟机

目录 前言说明注意准备 iso官网思博主(嘻嘻)拖动到虚拟机里面启动 virt-manager创建虚拟机选择本地安装介质选择 iso配置 内存 和 CPU选择 创建的虚拟机 保存的位置启动虚拟机看到熟悉的 Win10界面前言 XUbuntu安装OpenSSH远程连接服务器 远程连接之MobaXterm安装使用 虚拟化技…

DS线性表之队列的讲解和实现(5)

文章目录 前言一、队列的概念及结构二、队列的实现队列节点和队列初始化销毁判断是否为空入队列出队列获取队头队尾数据获取队列元素个数 三、实际使用效果总结 前言 队列实现源代码   队列是我们遇到的第二个实用数据结构&#xff0c;栈和队列地位等同 一、队列的概念及结构…

一篇python常见的Pandas的数据功能的使用

当我们学习了如何使用 Pandas 进行数据的导入与导出,这为我们后续的数据处理打下了基础.此次我们将重点讨论数据选择与过滤.通过掌握这一部分的知识,你将能够轻松地从复杂的数据集中提取出所需的信息.接下来,我们将通过一些实例来逐步了解这些操作. 这里插播一条粉丝福利&#…

三、AOP

文章目录 1. AOP&#xff08;概念&#xff09;2. AOP&#xff08;底层原理&#xff09;2.1 AOP 底层使用动态代理2.2 AOP&#xff08;JDK动态代理&#xff09; 3. AOP&#xff08;术语&#xff09;3.1 连接点3.2 切入点3.3 通知&#xff08;增强&#xff09;3.4 切面 4. AOP操作…

Element中el-table组件设置max-height右侧出现空白列的解决方法

之前就出现过这个情况&#xff0c;没理过&#xff0c;因为不影响啥除了不美观...但今天看着实在是难受&#xff0c;怎么都不顺眼(可能是我自己烦躁--) 试了很多网上的方法&#xff0c;都不得行&#xff0c;后面发现了这篇文章&#xff0c;解决了! 感谢&#xff01; Element中t…

SpringBoot人事系统:企业人才资源的智能管家

6系统测试 6.1概念和意义 测试的定义&#xff1a;程序测试是为了发现错误而执行程序的过程。测试(Testing)的任务与目的可以描述为&#xff1a; 目的&#xff1a;发现程序的错误&#xff1b; 任务&#xff1a;通过在计算机上执行程序&#xff0c;暴露程序中潜在的错误。 另一个…

fastadmin 多商户模式下侧边栏跳转路径BUG

记录&#xff1a;仅作自己项目记录&#xff0c;在一个域名下部署多套项目时&#xff0c;若是多商户模式项目会出现跳转路径问题。 修改 \manystore\library\Auth.php 文件的 getSidebar 方法 // 1 改为&#xff1a; $v[url] isset($v[url]) && $v[url] ? $v[url] :…

关于WPF(Windows Presentation Foundation)中Grid控件

本文将从Grid控件的基础概念开始&#xff0c;逐步深入探讨其特性、用法、实例代码&#xff0c;以及最佳实践。 1. WPF和布局简介 WPF是一种用于构建Windows桌面应用程序的UI框架&#xff0c;它通过XAML&#xff08;Extensible Application Markup Language&#xff09;使开…

PDF编辑不求人!4款高效工具,内容修改从此变得简单又快捷

咱们现在生活在一个数字时代&#xff0c;PDF文件可不就是工作、学习还有日常生活中经常要用的东西嘛。但遇到那些需要改动的PDF文件&#xff0c;是不是就觉得有点头疼啊&#xff1f; 因为传统的PDF文件真的不好编辑&#xff0c;这确实挺烦人的。不过呢&#xff0c;我今天要给你…