【数据结构】详解堆

一、堆的概念

堆(Heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵 完全二叉树的 数组对象。 堆是非线性数据结构,相当于一维数组,有两个直接后继。
如果有一个关键码的集合K = { k₀,k₁,k₂ ,k₃ ,…,kₙ₋₁  },把它的所有元素按完全二叉树的顺序存储方式存储,在一个一维数组中,并满足:Kᵢ  <= K₂ *ᵢ₊₁  且 Kᵢ  <= K₂ *ᵢ₊₂  (Kᵢ  >= K₂ *ᵢ₊ ₁ 且 Kᵢ  >= K₂ *ᵢ₊₂ ) i = 0,1,2…,则称为小堆 (或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

【大根堆和小根堆】:

根结点最大的堆叫做大根堆,树中所有父亲都大于或等于孩子。

根结点最小的堆叫做小根堆,树中所有父亲都小于或等于孩子。

共同特点:父亲 =(孩子-1)/2

大堆小堆有什么特点呢?

我们购物平台中,我想选择销量大的前k家,这个时候,我们不需要对所有的数据进行排序,只需要取出前k家最大的值就可以。而最值常常出现在0号位,我们就可以利用Topheap解决,大大减少了我们的时间复杂度;

特点:

  • 堆中某个节点的值总是不大于或不小于其父节点的值;
  • 堆总是一棵完全二叉树
  • 每层节点个数为2^(h-1)个

二、堆的创建

1、头文件的声明:

typedef int HPDataType;
typedef struct Heap
{HPDataType* a;int size;int capacity;
}Heap;void HeapInit(Heap* hp);
// 堆的销毁
void HeapDestory(Heap* hp);void swap(HPDataType* p1, HPDataType* p2);void AdjustUp(HPDataType* a, int child);
void AdjustDown(HPDataType* a, int n, int parent);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);

2、代码实现:

2.1堆的初始化与堆的摧毁

//堆的初始化
void HeapInit(Heap* hp) {assert(hp);hp->a = NULL;hp->capacity = hp->size = 0;
}//堆的摧毁
void HeapDestory(Heap* hp) {assert(hp);free(hp->a);hp->a = NULL;hp->capacity = hp->size = 0;
}

2.2堆的插入

下面给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的 向下调整算法 可以把它调整成一个小堆 。向下调整算法有一个前提:左右子树必须是一个堆( 包括大堆和小堆) ,才能调整。

具体步骤如下:

1.将新插入的元素放置在堆的最后一个位置(通常是数组的末尾)。

2.将该元素与其父节点进行比较。

3.若该元素大于(或小于,具体根据堆是最大堆还是最小堆而定)其父节点的值,则交换该元素和其父节点的位置。 

4.继续向上对比和交换,直到满足堆的性质或达到堆的根节点。

// 堆的插入
void HeapPush(Heap* hp, HPDataType x) {assert(hp);//与顺序表的开辟类似if (hp->size == hp->capacity){int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;//使用三目操作符开辟空间大小HPDataType* tmp = (HPDataType*)realloc(hp->a, newcapacity * sizeof(HPDataType));if (tmp == NULL){perror("realloc fail");return;}hp->a = tmp;hp->capacity = newcapacity;}hp->size++;hp->a[hp->size] = x;
//向上调整法,因为每次的插入是在数组末尾
//每次插入需要与父亲比较大小交换AdjustUp(hp->a, hp->size - 1);
}
2.2.1向上调整法 

//向上调整法,因为每次的插入是在数组末尾
//每次插入需要与父亲比较大小交换
    AdjustUp(hp->a, hp->size - 1);

我们每次插入末尾的位置,相当于孩子,我们需要找到该孩子的父亲与之比较大小,这个时候就要利用堆的特点:父亲 =(孩子-1)/2
 向上调整法:

void AdjustUp(HPDataType* 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;}}
}

 交换函数:

因为在堆的实现中我们会经常使用父子交换

void swap(HPDataType* p1, HPDataType* p2) {HPDataType tmp = *p1;*p1 = *p2;*p2 = tmp;//这里也可以回想前面学习//不使用第三个变量,利用换位与实现交换
}

2.4堆的删除

如果我们直接删除堆顶数据,将数组数据整体向前移动,这样会导致堆的乱序;

删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法

2.4.1向下调整法

void AdjustDown(HPDataType* a, int n, int parent) {//注:这里的parent为0,而数组a则是首尾交换的int child = parent * 2 + 1;//假设左孩子小while (child < n) {//while循环直到超出数组长度//左孩子大if (a[child] > a[child + 1]) {child++;}if (a[child]>a[parent]) {swap(&a[child], &a[parent]);parent = child;child = parent * 2 + 1;}}
}

这样我们只需要完成交换,传指就可以了

void HeapPop(Heap* hp) {assert(hp);assert(hp->size);swap(&hp->a[0], &hp->a[hp->size - 1]);hp->size--;AdjustDown(hp->a, hp->size, 0);
}

3、总结

升序:建大堆

降序:建小堆

(1)升序:建大堆

【思考】排升序,建小堆可以吗?-- 可以(但不推荐)。

首先对 n 个数建小堆,选出最小的数,接着对剩下的 n-1 个数建小堆,选出第二小的数,不断重复上述过程。

【时间复杂度】建 n 个数的堆时间复杂度是 O(N),所以上述操作时间复杂度为 O(N²),效率太低,关系变得杂乱,尤其是当数据量大的时候,效率就更低。同时堆的价值也没有被体现出来,这样不如用直接排序。

排升序,因为数字依次递增,需要找到最大的数字,得建大堆。

首先对 n 个数建大堆。将最大的数(堆顶)和最后一个数交换,把最大的数放到最后。前面 n-1 个数的堆结构没有被破坏(最后一个数不看作在堆里面的),根节点的左右子树依然是大堆,所以我们进行一次向下调整成大堆即可选出第 2 大的数,放到倒数第二个位置,然后重复上述步骤。

【时间复杂度】:建堆时间复杂度为 O(N),向下调整时间复杂度为 O(log₂N),这里我们最多进行N-2 次向下调整,所以堆排序时间复杂度为 O(N*log₂N),效率相较而言是很高的。

因为在堆的实现中我们会经常使用父子交换

void swap(HPDataType* p1, HPDataType* p2) {HPDataType tmp = *p1;*p1 = *p2;*p2 = tmp;//这里也可以回想前面学习//不使用第三个变量,利用换位与实现交换
}

我相信接下来对你来说简直轻而易举

// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);

4.1取堆顶数据

HPDataType HeapTop(Heap* hp) {assert(hp);assert(hp->size);return hp->a[0];
}

4.2 堆的数据个数

int HeapSize(Heap* hp) {assert(hp);assert(hp->size);return hp->size;
}

4.3堆的判空

int HeapEmpty(Heap* hp) {assert(hp);assert(hp->size>0);//为NULL,返回1,不为NULL,返回0;return hp->size == 0 ? 1 : 0;
}

5、堆的时间复杂度

5.1建堆的时间复杂度

5.1.1向下调整法建堆
// 堆的插入
void HeapPush(Heap* hp, HPDataType x) {assert(hp);//与顺序表的开辟类似if (hp->size == hp->capacity){int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;//使用三目操作符开辟空间大小HPDataType* tmp = (HPDataType*)realloc(hp->a, newcapacity * sizeof(HPDataType));if (tmp == NULL){perror("realloc fail");return;}hp->a = tmp;hp->capacity = newcapacity;}hp->size++;hp->a[hp->size] = x;
//向下调整法,因为每次的插入是在数组末尾
//每次插入需要与父亲比较大小交换AdjustUp(hp->a, hp->size - 1);
}

  因此:建堆的时间复杂度为O(N)

等比数列求和公式: 
建堆要从倒数第一个非叶子节点开始调整,也即是从倒数第二层开始调,可得出时间复杂度公式:T ( n ) = ∑ ( 每 层 节 点 数 ∗ ( 堆 的 高 度 − 当 前 层 数 ) ) 
建堆的时间复杂度为O(N)。(向下调整算法)

为何使用向下调整建堆而不使用向上调整建堆

5.1.2向上调整法建堆

可以看出结点数多的层, 调整次数也多, 结点数少的层, 调整次数少, 时间复杂度为O(N*logN), 所以一般建堆都采用向下调整建堆法. 

6、TOPK问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大

比如:专业前十、世界500强、销量最高的前十、富豪榜等等

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能

数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

1. 用数据集合中前K个元素来建堆

前k个最大的元素,则建小堆

前k个最小的元素,则建大堆

2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素

将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

 我们常常会想到冒泡排序( O(N^2) )
对于少量的数据是可以拿捏的,但面对100000的数据就显得有点吃力,而堆排序O(N*logN)则觉得轻而易举

 

7、堆排序  

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;}
}

哇呜!点个赞走吧!!!

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

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

相关文章

数据结构(双向链表)

链表的分类 链表的结构⾮常多样&#xff0c;以下情况组合起来就有8种&#xff08;2 x 2 x 2&#xff09;链表结构&#xff1a; 虽然有这么多的链表的结构&#xff0c;但是我们实际中最常⽤还是两种结构&#xff1a;单链表和双向带头循环链表 1.⽆头单向⾮循环链表&#xff1a…

第十课:telnet(远程登入)

如何远程管理网络设备&#xff1f; 只要保证PC和路由器的ip是互通的&#xff0c;那么PC就可以远程管理路由器&#xff08;用telnet技术管理&#xff09;。 我们搭建一个下面这样的简单的拓扑图进行介绍 首先我们点击云&#xff0c;把云打开&#xff0c;点击增加 我们绑定vmn…

【面试题】Redo log和Undo log

Redo log 介绍Redo log之前我们需要了解一下&#xff0c;mysql数据操作的流程&#xff1a; 上述就是数据操作的流程图&#xff0c;可以发现sql语句并不是直接操作的磁盘而是通过操作内存&#xff0c;然后进行内存到磁盘的一个同步。这里我们必须要了解一些区域&#xff1a; 缓…

华为HCIP Datacom H12-821 卷42

42.填空题 如图所示&#xff0c;MSTP网络中SW1为总根&#xff0c;请将以下交换机与IST域根和主桥配对。 参考答案&#xff1a;主桥1468 既是IST域根又是主桥468 既不是又不是就是25 解析&#xff1a; 主桥1468 既是IST域根又是主桥468 既不是又不是就是25 43.填空题 网络有…

[日进斗金系列]用码上飞解决企微开发维修管理系统的需求

前言&#xff1a; 今天跟大家唠唠如何用小money生 大money的方法&#xff0c;首先我们需要准备一个工具。 这个工具叫码上飞CodeFlying&#xff0c;它是目前国内首发的L4级自动化智能软件开发平台。 它可以在短时间内&#xff0c;与AI进行几轮对话就能开发出一个可以解决实际…

WEB前端06-BOM对象

BOM浏览器对象模型 浏览器对象模型&#xff1a;将浏览器的各个组成部分封装成对象。是用于描述浏览器中对象与对象之间层次关系的模型&#xff0c;提供了独立于页面内容、并能够与浏览器窗口进行交互的对象结构。 组成部分 Window&#xff1a;浏览器窗口对象 Navigator&…

区块链资料

Quantstamp - Public Security Assessments smart-contract-sanctuary-bsc/contracts/mainnet at master tintinweb/smart-contract-sanctuary-bsc GitHub https://github.com/slowmist/Cryptocurrency-Security-Audit-Guide/blob/main/README_CN.md sFuzz: 高效自适应的智…

Spring3(代理模式 Spring1案例补充 Aop 面试题)

目录 一、代理模式 介绍 意图 主要解决的问题 使用场景 实现方式 关键代码 应用实例 优点 缺点 使用建议 注意事项 结构 什么是代理模式&#xff1f; 为什么要用代理模式&#xff1f; 有哪几种代理模式&#xff1f; 1. 静态代理 实现 2. 基于接口的动态代理…

《昇思25天学习打卡营第20天|GAN图像生成》

生成对抗网络&#xff08;GAN&#xff09;是一种深度学习模型&#xff0c;用于生成逼真的图像。在手写数字识别的任务中&#xff0c;GAN 可以用来生成与真实手写数字相似的图像&#xff0c;以增强模型的训练数据集。GAN 主要由两个部分组成&#xff1a;生成器&#xff08;Gener…

Spring Data Jpa 原生SQL联表查询返回自定义DTO

Spring Data Jpa 原生SQL联表查询返回自定义DTO 方案一&#xff1a;返回Map 这个就不说了 方案二&#xff1a;实体定义成接口的形式 该方式最直观&#xff01;&#xff01;推荐&#xff01;&#xff01;&#xff01; 注意&#xff1a;XxxDto是interface接口&#xff0c;而…

WPF/C#:在WPF中如何实现依赖注入

前言 本文通过 WPF Gallery 这个项目学习依赖注入的相关概念与如何在WPF中进行依赖注入。 什么是依赖注入 依赖注入&#xff08;Dependency Injection&#xff0c;简称DI&#xff09;是一种设计模式&#xff0c;用于实现控制反转&#xff08;Inversion of Control&#xff0…

[Redis]典型应用——缓存

什么是缓存 缓存&#xff08;Cache&#xff09;是一种用于临时存储数据的机制&#xff0c;目的是提高数据访问速度和系统性能。 核心思路就是把一些常用的数据放到触手可及(访问速度更快)的地方&#xff0c;方便随时读取 缓存是一个相对的概念&#xff0c;比如说&#xff0c…

EE trade:强平和爆仓的区别

在金融交易市场中&#xff0c;杠杆交易的引入&#xff0c;让投资者可以用少量的资金撬动更大的头寸&#xff0c;获取更大的收益。然而&#xff0c;杠杆交易也带来了更大的风险&#xff0c;一旦市场波动&#xff0c;投资者可能会面临强平或爆仓的风险。了解强平和爆仓的区别&…

选择Maya进行3D动画制作与渲染的理由

如果你对3D动画充满热情并追求成为专业3D动画师的梦想&#xff0c;你一定听说过Maya——近年来3D动画的行业标准。Maya被3D艺术家广泛使用&#xff0c;你是否想知道为什么Maya总是他们的首选&#xff1f;下面一起来了解下。 一、什么是Maya&#xff1f; 由Autodesk开发的Maya是…

2024年土木建筑与结构工程国际会议(IACCASE 2024)

2024年土木建筑与结构工程国际会议 2024 International Conference on Civil and Structural Engineering 【1】会议简介 2024年土木建筑与结构工程国际会议旨在为全球土木建筑与结构工程领域的专家学者、研究人员及从业人员提供一个交流与合作的平台。会议聚焦该领域的最新研究…

影院选座系统小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;影院信息管理&#xff0c;电影类型管理&#xff0c;放映厅管理&#xff0c;电影信息管理&#xff0c;系统管理 微信端账号功能包括&#xff1a;系统首页&#xff0c;影院信息&…

学习小记-Nacos的服务注册与发现原理

服务注册&#xff1a; 当一个服务实例启动时&#xff0c;它会向 Nacos 服务器注册自己的信息&#xff0c;包括 IP 地址、端口号、元数据&#xff08;如服务版本、区域信息等&#xff09;。服务实例使用 Nacos API 发送注册请求&#xff0c;Nacos 服务器接收请求并存储服务实例信…

[iOS]浅析isa指针

[iOS]浅析isa指针 文章目录 [iOS]浅析isa指针isa指针isa的结构isa的初始化注意事项 上一篇留的悬念不止分类的实现 还有isa指针到底是什么 它是怎么工作的 class方法又是怎么运作的 class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags 这里面的class又是何方…

python的tkinter、socket库开发tcp的客户端和服务端

一、tcp通讯流程和开发步骤 1、tcp客户端和服务端通讯流程图 套接字是通讯的利器&#xff0c;连接时要经过三次握手建立连接&#xff0c;断开连接要经过四次挥手断开连接。 2、客户端开发流程 1&#xff09;创建客户端套接字 2&#xff09;和服务端器端套接字建立连接 3&#x…

Linux·基本指令(下)

1. mv 指令 (move) 语法&#xff1a;mv[选项] 源文件或目录 目标文件或目录 功能&#xff1a;将源文件或目录剪贴到一个新位置&#xff0c;或给源文件或目录改名但不会改变其内容 常用选项&#xff1a; -f &#xff1a;force 强制&#xff0c;如果目标文件已经存在&#xff0c;…