【数据结构】 -- 堆 (堆排序)(TOP-K问题)

news2024/11/24 8:02:52

引入

要学习堆,首先要先简单的了解一下二叉树,二叉树是一种常见的树形数据结构,每个节点最多有两个子节点,通常称为左子节点和右子节点。它具有以下特点:

  1. 根节点(Root):树的顶部节点,没有父节点。
  2. 子节点(Children):每个节点最多有两个子节点,分别称为左子节点和右子节点。
  3. 叶子节点(Leaf):没有子节点的节点称为叶子节点。
  4. 父节点(Parent):每个节点都有一个父节点,除了根节点。
  5. 深度(Depth):从根节点到某个节点的唯一路径的长度,根节点的深度为0。
  6. 高度(Height):从某个节点到它的最远叶子节点的路径长度,叶子节点的高度为0。
  7. 遍历(Traversal):遍历二叉树是指按照一定顺序访问树中的每个节点,常见的遍历方式包括前序遍历、中序遍历和后序遍历。

二叉树的应用非常广泛,在后面我会详细介绍。

满二叉树:除了叶子结点外,每个结点都有两个子结点

一个深度为k的满二叉树有2的k次方减一个节点。

完全二叉树:除了最底层可能不是满的外,其它每一层从左到右都是满的。

满二叉树是完全二叉树的子集,满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树。 

堆就是一种完全二叉树。

二叉树的储存

逻辑结构和物理结构

逻辑结构和物理结构是计算机科学中两个重要的概念,它们描述了数据在计算机中的不同组织方式。

  1. 逻辑结构:

    • 逻辑结构是指数据元素之间的相互关系和操作规则。它关注的是数据之间的逻辑关联,而不考虑数据在计算机内部的存储方式。
    • 常见的逻辑结构包括线性结构、树形结构和图形结构。
    • 线性结构中的数据元素之间是一对一的关系,例如线性表、栈、队列等。
    • 树形结构中的数据元素之间存在一对多的关系,例如二叉树、B树等。
    • 图形结构中的数据元素之间是多对多的关系,例如图、网络等。
  2. 物理结构:

    • 物理结构描述了数据在计算机内部存储的方式和组织形式,也称为存储结构。
    • 物理结构与计算机的存储器相关,它包括了数据元素在内存中的存储位置和存储方式。
    • 常见的物理结构包括顺序存储结构和链式存储结构。
    • 顺序存储结构是将数据元素连续地存储在内存中的一块连续的存储空间中,例如数组。
    • 链式存储结构是通过指针将数据元素存储在内存中的不同位置,并通过指针将它们串联起来,例如链表。

逻辑结构关注数据之间的逻辑关系和操作规则,而物理结构关注数据在计算机内部的实际存储方式和组织形式。

二叉树的储存

二叉树有多种存储方式,常见的包括顺序存储和链式存储。

  1. 顺序存储: 顺序存储通常使用数组来表示二叉树。假设树的根节点存储在数组下标为0的位置,则对于任意一个下标为i的节点:

    • 其左子节点的下标为2i + 1
    • 其右子节点的下标为2i + 2 例如,如果要存储二叉树的节点值为[1, 2, 3, 4, 5, 6, 7]的完全二叉树,可以使用数组[1, 2, 3, 4, 5, 6, 7]进行存储。
  2. 链式存储: 链式存储则是通过节点之间的引用来表示二叉树的结构,每个节点包含数据域和左右子节点指针域。

链式储存我们放在后边更新,在这里我们先学习顺序储存。

顺序储存

顺序储存用数组来储存,顺序存储一般只适合用来存储完全二叉树(堆),用顺序储存再存储非完全的二叉树会存在空间浪费

 堆的实现

头文件:

#define _CRT_SECURE_NO_WARNINGS 1

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>

typedef int HPDatatype;

typedef struct Heap
{
	HPDatatype * a;
	int size;
	int capacity;

}HP;

//初始化
void HPInit(HP* php);

//插入数据
void HPPush(HP* php, HPDatatype x);

//交换
void Swap(HPDatatype* a,HPDatatype * b);

//销毁
void HPDestroy(HP* php);

//向上调整
void AdjustUp(HPDatatype* a, int child);

//向下调整
void AdjustDown(HPDatatype* a,int n, int parent);


//删除顶部数据
void HPPop(HP* php);

//返回顶部数据
HPDatatype* HPTop(HP* php);

//判空
bool HPEmpty(HP* php);

实现文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"

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

}

//插入数据
void HPPush(HP* php, HPDatatype x)
{
	assert(php);
	//判断空间够不够
	if (php->capacity == php->size)
	{
		int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
		HPDatatype* tmp = (HPDatatype* )realloc(php->a,newcapacity * sizeof(HPDatatype));
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}
		php->capacity = newcapacity;
		php->a = tmp;
	}
	php->a[php->size] = x;
	php->size++;
	
	AdjustUp(php->a, php->size - 1);
}

//交换
void Swap(HPDatatype* a, HPDatatype* b)
{
	HPDatatype cmp = *a;
	*a = *b;
	*b = cmp;
}

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

//向上调整
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 AdjustDown(HPDatatype* a, int n, int parent)
{
	int child = 2 * parent + 1;//先假设左边的小

	while (child < n)
	{
		if (child + 1 < n && a[child + 1] < a[child])//规避chlid + 1 越界的风险
		{
			child++;
		}
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}

}


//删除顶部数据
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);
}

//返回顶部数据
HPDatatype* HPTop(HP* php)
{
	assert(php);
	assert(php->size > 0);

	return php->a[0];
}

//判空
bool HPEmpty(HP* php)
{
	assert(php);

	return php->size == 0;
}

TOP-K问题

一般来说,堆分为两类

  1. 大堆(Max Heap):在最大堆中,每个节点的值都大于或等于其子节点的值。换句话说,堆顶部的元素是整个堆中的最大值。最大堆常用于实现优先队列,其中具有最高优先级的元素始终位于堆顶。

  2. 小堆(Min Heap):在最小堆中,每个节点的值都小于或等于其子节点的值。因此,堆顶部的元素是整个堆中的最小值。最小堆也常用于优先队列,其中具有最低优先级的元素位于堆顶。

简单来说大堆中,同一个分支中大的在上;小堆中,同一分支小的在上。

在这里以小堆为例:

向上调整算法

往堆中插入一个数据时,先将插入的数据放到堆的最后一个节点,然后利用向上调整算法依次调整。

图示:

只要子节点不越界循环一直进行,当字节点不小于父节点时跳出if()语句进入else,跳出循环。

//向上调整
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;
		}
	}
}

求一堆数据(储存在小堆中)中最最小的前几个数据:将数据插入堆中,小堆的堆顶中储存的就是堆中最小的数据,把堆顶的数据取下来,再将堆顶的数据释放;用向上调整算法调整堆,再依次取堆顶,重复。

//TOP-K
void HPtest02()
{
	int a[] = { 5,6,1,4,2,8 };
	HP s;
	HPInit(&s);
	for (size_t i = 0; i < sizeof(a) / sizeof(int); i++)
	{
		HPPush(&s, a[i]);
	}
	int k = 0;
	scanf("%d", &k);
	while (k--)
	{
		printf("%d ", HPTop(&s));
		HPPop(&s);
	}
	HPDestroy(&s);
}


int main()
{
	HPtest02();

	return 0;
}

 演示:

在TOP-K问题中,我们会发现,输出的数据是按顺序拍好的,那么我们可不可以在此基础上进行排序呢。 把数据储存到堆中之后,再依次拿出来。

//排序
void HPtest03()
{
	int a[] = { 5,6,1,4,2,8 };
	HP s;
	HPInit(&s);
	for (size_t i = 0; i < sizeof(a) / sizeof(int); i++)
	{
		HPPush(&s, a[i]);
	}
	int i = 0;
	while (!HPEmpty(&s))
	{
		a[i++] = HPTop(&s);
		HPPop(&s);
	}
	HPDestroy(&s);
}
int main()
{
	
	HPtest03();
	
	return 0;
}

这样我们就可以对数据进行排序。

这个算法的时间复杂度非常低 。 一个有k个节点的对的深度为log(k),一条分支最多交换log (k) - 1次,所以

算法的时间复杂度为log N。 但是这并不能称作真正的排序,因为它在原数组的基础上开辟了新的空间。

堆排序

建堆算法

//堆排序
void HeapSort(int* a, int n)
{
	//建堆
	for (int i = 1; i < n; i++)
	{
		AdjustUp(a, i);
	}
}

void Heaptset()
{
	int a[] = { 5,6,8,4,1,2,3 };
	HeapSort(a, 7);
}
int main()
{
	//HPtest01();
	/*HPtest02();*/
	//HPtest03();
	Heaptset();
	return 0;
}

排序

在惯性思维中,要排降序应该会建大堆,排升序会建小堆。但这样会导致一个问题(以建排降序 为建小堆为例)

小堆的堆顶为这组数据中最小的数,我们将它取出,作为排序的第一个数

取出堆顶后,找出第二小的数据, 但是此时的堆各个节点已经不满足之前的大小关系了,4之前是6和5的父节点,比6和5大,但是与2为兄弟节点,兄弟节点之间的大小关系原来并不清楚,无法直接找出第二大的数据(可以重新把剩下的数据建堆,但是没必要,时间成本大)。在堆排序中不能让第一个数据直接拿出去,这样会改变节点之间的父子关系,不能确定大小关系,无法找出需要的节点。

接下来以排降序排降序为例演示过程。

//堆排序
void HeapSort(int* a, int n)
{
	//建堆
	for (int i = 1; i < n; i++)
	{
		AdjustUp(a, i);
	}

	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

void Heaptset()
{
	int a[] = { 5,6,8,4,1,2,3 };
	HeapSort(a, 7);
}
int main()
{
	//HPtest01();
	/*HPtest02();*/
	//HPtest03();
	Heaptset();
	return 0;
}

调试:

向下调整算法的时间复杂度为log N,堆排序在最坏的情况下N个数据要排N次,所以堆排序的时间复杂度为N log N。可以极大的提高程序的效率。

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

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

相关文章

为什么Kubernetes(K8S)弃用Docker:深度解析与未来展望

为什么Kubernetes弃用Docker&#xff1a;深度解析与未来展望 &#x1f680; 为什么Kubernetes弃用Docker&#xff1a;深度解析与未来展望摘要引言正文内容&#xff08;详细介绍&#xff09;什么是 Kubernetes&#xff1f;什么是 Docker&#xff1f;Kubernetes 和 Docker 的关系…

iOS 17.5中的一个漏洞

i0S 17.5中的一个漏洞 iOS 17.5中的一个漏洞会使已刚除的照片重新出现&#xff0c;并目此问题似乎会影响甚至已擦除并出售给他人的 iPhone 和 iPad. 在2023年9月&#xff0c;一位Reddit用户根据Apple的指南擦除了他的iPad&#xff0c;并将其卖给了一位朋友。然而&#xff0c;这…

Spring @Transactional 事务注解

一、spring 事务注解 1、实现层(方法上加) import org.springframework.transaction.annotation.Transactional;Transactional(rollbackFor Exception.class)public JsonResult getRtransactional() {// 手动标记事务回滚TransactionAspectSupport.currentTransactionStatus…

Python第二语言(二、Python语言基础)

目录 1. python中常用的6中数据类型 2. Print语句&#xff08;输出程序&#xff09; 3. 字面量 4. 注释 5. 变量 6. 数据类型 7. type()语句&#xff1a;查询数据类型 8. 数据类型转换语句 9. 标识符 10. 算术运算符 11. 赋值运算符 && 复合赋值运算符 12. …

Polar Web【困难】上传

Polar Web【困难】上传 Contents Polar Web【困难】上传探索&思路&效果进入环境绕过过程Webshell连接 EXPPayload 总结 探索&思路&效果 本题的主题可见为文件上传&#xff0c;详情在破解的过程中逐步发掘&#xff1a; 进入环境&#xff0c;为一个文件上传功界面…

零基础入门篇①⑦ Python可变序列类型--集合

Python从入门到精通系列专栏面向零基础以及需要进阶的读者倾心打造,9.9元订阅即可享受付费专栏权益,一个专栏带你吃透Python,专栏分为零基础入门篇、模块篇、网络爬虫篇、Web开发篇、办公自动化篇、数据分析篇…学习不断,持续更新,火热订阅中🔥专栏限时一个月(5.8~6.8)重…

【UML用户指南】-12-对高级结构建模-接口、类型和角色

目录 1、名称 2、操作 3、关系 4、理解接口 5、常用建模技术 5.1、对系统中的接缝建模 5.2、对静态类型和动态类型建模 5.2.1、对静态类型建模 5.2.2、对动态类型建模 使接口易于理解和易于访问 接口在关于一个抽象做什么的描述与关于这个抽象如何做的实现之间定义了…

数仓建模中的一些问题

​​​在数仓建设的过程中&#xff0c;由于未能完全按照规范操作&#xff0c; 从而导致数据仓库建设比较混乱&#xff0c;常见有以下问题&#xff1a; 数仓常见问题 ● 数仓分层不清晰&#xff1a;数仓的分层没有明确的逻辑&#xff0c;难以管理和维护。 ● 数据域划分不明确…

ipynb转markdown的简单方法

在线转换 推荐在线转换&#xff0c;拖进去后下载就行&#xff0c;简单易操作。 Convert Jupyter notebook to GitHub-Flavored Markdown for free on AlldocsThe free text converter for all your documents.https://alldocs.app/convert-jupyter-notebook-to-markdown vsc…

RHEL8/Centos8 install for PXE

PXE介绍 PXE&#xff08;Preboot Execution Environment&#xff09;是预引导执行环境的缩写。它是由Intel设计的&#xff0c;允许客户端计算机通过网络从服务器上加载操作系统镜像。PXE通常用于大规模部署操作系统&#xff0c;例如在企业或学校环境中。 PXE工作流程如下&…

Windows安装运行elasticsearch服务

官方下载地址&#xff1a;Download Elasticsearch | Elastic 我在linux上执行的下载命令&#xff1a;wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.5.3-linux-x86_64.tar.gz Elasticsearch&#xff08;简称ES&#xff09;是一款基于Apache Lu…

Functional ALV系列 (10) - 将填充FieldCatalog封装成函数

在前面的博文中&#xff0c;已经讲了封装的思路和实现&#xff0c;主要是利用 cl_salv_data_descr>read_structdescr () 方法来实现。在这里&#xff0c;贴出代码方便大家参考。 编写获取内表组件的通用方法 form frm_get_fields using pt_data type any tablechanging…

OpenFeign远程接口调用使用公共模块出现的错误

今天在使用openfeign和sentinel实现fallback服务降级时遇到找不到类型的异常 检查代码发现没有错误&#xff0c;EnableFeignClients也在启动类上标注了 错误信息&#xff1a;A component required a bean of type com.zxc.cloud.apis.PayFeignSentinelApi that could not be f…

Effective Java 2 遇到多个构造器参数时要考虑使用构建器

第2个经验法则&#xff1a;用遇到多个构造器参数时要考虑使用构建器&#xff08;consider a builder when faced with many constructor parameters&#xff09; 上一条讨论了静态工厂相对于构造器来说有五大优势。但静态工厂和构造器有个共同的局限性:它 们都不能很好地扩展到…

SpringBoot——整合WebSocket长连接

目录 WebSocket 项目总结 新建一个SpringBoot项目 pom.xml WebSocketConfig配置类 TestWebSocketEndpoint服务端点类 socket.html客户端 IndexController控制器 SpringbootWebsocketApplication启动类 测试客户端和服务端如何使用WebSocket进行连接和通信 WebSocket S…

LayUI的暗淡:错误的押宝了前后端不分离

LayUI是一个不错的中后台UI框架&#xff0c;贝格前端工场用的CMS就是基于layUI的&#xff0c;可以说简单轻便。除此之外&#xff0c;贝格前端工场很少接到客户要求升级LayUI界面&#xff0c;或者采用LayUI框架的。 一、LayUI官网的谢幕&#xff0c;吹起了前后端不分离模式没落…

Java Set系列集合的使用规则和场景(HashSet,LinkedHashSet,TreeSet)

Set集合 package SetDemo;import java.util.HashSet; import java.util.Iterator; import java.util.Set;public class SetDemo {public static void main(String[] args) {/*Set集合的特点&#xff1a;1.Set系列集合的特点&#xff1a;Set集合是一个存储元素不能重复的集合方…

【数据结构(邓俊辉)学习笔记】图03——拓扑排序

文章目录 0. 概述1. 零入度算法1. 1 拓扑排序1. 2 算法 2. 零出度算法2.1 算法2.2 实现2.3. 复杂度 0. 概述 学习下拓扑排序 1. 零入度算法 1. 1 拓扑排序 首先理解下拓扑排序 其实老师经常干这事&#xff0c;如编讲义&#xff0c;将已经知道的知识点串起来变成讲课序列。那…

AIGC作答《2024年高考作文|新课标I卷》能拿多少分?

AIGC作答《2024年高考作文&#xff5c;新课标I卷》能拿多少分&#xff1f; 一、前言二、题目三、作答 一、前言 如火如荼的2024年高考圆满落幕&#xff0c;在如此Happy的时刻&#xff0c;AIGC技术正以其前所未有的热度席卷全球。它不仅改变了我们获取信息的方式&#xff0c;也…

RJ45 PCB布线

RJ45底盘接地和数字地通过一个1M欧姆的电阻和一个0.1uF的去耦电容隔离。其底盘接地和数字地的间距&#xff0c;必须比60mil宽。如图11及图12所示。 图11 典型变压器集成单RJ45的机箱/数字地平面 图12 典型RJ45和变压器分开的机箱/数字地平面https://www.bilibili.com/read/…