数据结构——堆(C语言实现)

news2024/12/22 16:08:56

文章目录

  • 什么是堆
  • 堆的实现
    • 堆的结构定义
    • 堆的初始化接口
    • 堆的销毁接口
    • 堆的插入数据接口
    • 向上调整建堆接口
    • 判断堆是否为空
    • 堆的删除数据接口
    • 向下调整建堆接口
    • 获取堆顶数据
    • 获取堆的有效数据个数
    • 完整实现代码
    • 小结
  • 堆排序
    • 堆排序的实现
  • 关于建堆和堆排序时间复杂度的分析
    • 向下调整建堆
    • 向上调整建堆
    • 堆排序
    • 小结
  • TOPK问题的介绍

什么是堆

堆是一种特殊的数据结构,它是一棵完全二叉树,同时满足堆属性,即父节点的值总是大于或小于其子节点的值。如果父节点的值总是大于子节点的值,那么我们称之为大根堆;反之,如果父节点的值总是小于子节点的值,那么我们称之为小根堆。在堆中,根节点的值最大(大根堆)或最小(小根堆),因此它也被称为堆顶。堆经常用于排序、topK问题等场景。
在这里插入图片描述

堆的实现

本篇文章使用c语言实现并以头文件和源文件分离的形式,也会逐步介绍各个接口的实现思路以及提供参考代码。

堆的结构定义

堆的结构定义其实是一个特殊的顺序表,这点和栈类似。所以,需要用一个指针指向动态开辟的内存,需要一个指向当前下标位置的变量,以及需要一个记录当前动态内存的容量。
在这里插入图片描述

堆的初始化接口

堆初始化接口实现思路如下,首先,改变一个堆,我们需要传它的地址。所以参数部分需要写成Hp*。在接口的一开始先判断指针的合法性。然后开辟动态内存,判断一下动态内存的有效性。最后,初始化结构体成员即可。
在这里插入图片描述

堆的销毁接口

释放动态申请的空间,free后及时置空的好习惯是我们应该要养成的。最后将size和capacity置零即可。
在这里插入图片描述

堆的插入数据接口

堆的插入接口实现思路如下,assert判断指针有效性,这是一个好的编程习惯,建议大家平时也养成这种习惯。首先判断容量是否满了,满了的话就扩容。然后直接下面的插入数据逻辑,其实就是类似于顺序表。直接将数据插入size下标的位置,++size即可。最后调用向上调整建堆接口,使堆的结构不变。
在这里插入图片描述

向上调整建堆接口

首先要根据孩子的下标位置,推断出父节点的下标位置。然后开始向上调整,向上调整的过程是一个循环的过程,循环的迭代条件就是当child大于根节点下标就继续支持循环。当子节点小于父节点循环终止。若父节点小于子节点那么进行对应下标的数据交换,然后迭代子节点下标和父节点下标即可。
在这里插入图片描述

在这里插入图片描述

判断堆是否为空

判断堆是否为空接口思路相对简单,类似于顺序表的判空思路,当下一个可以插入数据的下标为0时,表示为空堆。
在这里插入图片描述

堆的删除数据接口

要删除堆的数据,是删除堆的堆顶数据还是堆底数据呢?答案是删除堆顶的数据,因为删除堆底的数据没有什么价值。而删除堆顶可以产生一些价值,如排序或者对一些前排名K个的数据进行收集等。举个例子,当我们在购物app想选一个电脑,我们可以按照销量进行排序这也是堆应用的一个场景。回到正题,删除堆顶的实现思路如下,我们让堆顶数据和最后一个数据交换,然后让size–,达到删除堆顶数据的效果,并且大大提升了效率。最后向下调整建堆即可。
在这里插入图片描述

在这里插入图片描述

向下调整建堆接口

向下调整建堆实现思路如下,首先,向下调整的过程是一个循环,它的终止条件为parent > size。循环体内部就是向下调整的核心思路,parent跟左右孩子较大的(较小的)比,本文以实现大堆为例。这里要介绍一个比较重要的概念,由于堆的底层使用顺序表存储,所以同一个父亲的左孩子右孩子是相邻存储的。即左孩子下标+1就是右孩子下标。让父亲和左右孩子较大的那一个比较,如果父亲小于孩子就交换位置,然后迭代。注意:向下调整的条件是左右子树必须是堆。
在这里插入图片描述

获取堆顶数据

其实,就是访问顺序表的第一个元素。但是,这样提供一个接口是非常符合接口的一致性以及对于代码的可读性有极大的提升的。
在这里插入图片描述

获取堆的有效数据个数

由于我们的size是从0开始,所以直接return size即可。
在这里插入图片描述

完整实现代码

//Heap.h文件
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

//默认起始容量
#define DefaultCapacity 4

//存储的数据类型
typedef int HpDataType;

typedef struct Heap
{
	HpDataType* data;
	int size;//可以插入数据的下标
	int capacity;//容量
}Hp;


//初始化
void HpInit(Hp* pHp);

//堆的销毁
void HpDestroy(Hp* pHp);

//插入数据
void HpPush(Hp* pHp, HpDataType x);

//向上调整建堆
void AdjustUp(HpDataType* data, int child);

//判断是否为空
bool HpEmpty(Hp* pHp);

//删除数据
void HpPop(Hp* pHp);

//向下调整建堆
void AdjustDown(HpDataType* data,int size, int parent);

// 取堆顶的数据
HpDataType HpTop(Hp* pHp);

// 堆的数据个数
int HpSize(Hp* pHp);
// Heap.c文件
#include"Heap.h"

//初始化
void HpInit(Hp* pHp) 
{
	//判断合法性
	assert(pHp);

	//开辟动态空间
	HpDataType* tmp = (HpDataType*)malloc(sizeof(HpDataType) * DefaultCapacity);
	if (tmp == NULL)//判断合法性
	{
		perror("malloc fail");
		return;
	}

	//初始化
	pHp->data = tmp;
	pHp->size = 0;
	pHp->capacity = DefaultCapacity;
}

//堆的销毁
void HpDestroy(Hp* pHp)
{
	//判断合法性
	assert(pHp);

	//释放内存和清理
	free(pHp->data);
	pHp->data = NULL;
	pHp->size = pHp->capacity = 0;

}


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

//向上调整建堆
void AdjustUp(HpDataType* data, int child)
{
	//判断指针有效性
	assert(data);
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		//向上调整呢
		if (data[child] > data[parent])
		{
			Swap(&data[child], &data[parent]);
		}
		else
		{
			break;
		}	
		//迭代
		child = parent;
		parent = (child - 1) / 2;
	}

}

//插入数据
void HpPush(Hp* pHp, HpDataType x)
{
	//判断指针有效性
	assert(pHp);

	//判断容量是否满了
	if (pHp->size == pHp->capacity)
	{
		HpDataType* tmp = (HpDataType*)realloc(pHp->data,sizeof(HpDataType) * pHp->capacity * 2);
		if (tmp == NULL)//判断空间合法性
		{
			perror("malloc fail");
			return;
		}
		//扩容后
		pHp->data = tmp;
		pHp->capacity *= 2;
	}

	//数据入堆
	pHp->data[pHp->size] = x;
	pHp->size++;

	//向上调整建堆
	AdjustUp(pHp->data, pHp->size - 1);

}
void AdjustDown(HpDataType* data, int size, int parent)
{
	//断言检查
	assert(data);

	int child = parent * 2 + 1;

	while (child < size)
	{
		//求出左右孩子较大的那个下标
		if (child + 1 < size && data[child + 1] > data[child])
		{
			child++;
		}
		//父亲比孩子小就交换位置
		if (data[child] > data[parent])
		{
			//交换
			Swap(&data[child], &data[parent]);
			//迭代
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}

}

void HpPop(Hp* pHp)
{
	//断言检查
	assert(pHp);

	//删除数据
	Swap(&pHp->data[0], &pHp->data[pHp->size-1]);
	pHp->size--;

	//向下调整建堆
	AdjustDown(pHp->data,pHp->size-1,0);

}

//判断是否为空
bool HpEmpty(Hp* pHp)
{
	assert(pHp);
	
	return pHp->size == 0;
}

// 取堆顶的数据
HpDataType HpTop(Hp* pHp)
{
	assert(pHp);

	return pHp->data[0];
}

// 堆的数据个数
int HpSize(Hp* pHp)
{
	assert(pHp);

	return pHp->size;
}

小结

操作堆这种数据结构就像吃老婆饼,你吃的是香甜的饼,至于是不是你老婆做的那就不一定了。但是,你在吃的时候想像成你老婆做的饼吃起来也别有一番风味。堆在逻辑结构上你操作的是一颗树,在底层的存储上又是一个顺序表。这就比较抽象的地方,需要考验我们画图以及走读代码调试的能力。

堆排序

堆排序其实就是堆这个数据结构的一种常见的使用方式。堆排序的核心思想就是利用堆删除的思想来进行排序操作。堆排序是一种时间复杂度O(N*logN)的不稳定排序。至于排序的稳定性的讲解在后面的博客会给大家介绍。

堆排序的实现

堆排序的实现思路如下,首先确定排序的顺序并将数据建成堆,升序建大堆,降序建小堆。建堆的话建议使用向下调整建堆。因为时间复杂度为O(logN),若使用向上调整建堆,那么时间复杂度为O(N*logN)。这样的时间复杂度对于找出堆顶数据的损耗太大,还不如直接遍历一遍(时间复杂度)。
在这里插入图片描述

接着是利用堆删除的思想进行排序。下面就以排升序为例。
在这里插入图片描述

//堆排序--排升序建大堆
void HeapSort(int* arr, int n)
{
	//向下建堆,效率更高
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(arr,n-1,i);
	}

	//排序
	//利用堆删除的思想进行排序
	int end = n - 1;
	while (end > 0)
	{
		//交换
		int tmp = arr[0];
		arr[0] = arr[end];
		arr[end] = tmp;
		//调整堆
		AdjustDown(arr, end-1, 0);
		end--;
	}
}

关于建堆和堆排序时间复杂度的分析

向下调整建堆

在前面的堆排序实现中,提到向下调整建堆的效率更高,这是因为向下调整建堆的时间复杂度是O(N)。下面我就带领大家简单分析一下向下调整建堆的时间复杂度。
在这里插入图片描述

向上调整建堆

向上调整建堆的时间复杂度为O(N*logN)。下面请看向上调整的时间复杂度问题。
在这里插入图片描述

堆排序

堆排序的时间复杂度为O(NlogN)。向下调整建堆的复杂度为O(N),这个上面已经分析。排序部分的话结合从第一个非叶子结点开始向下调整堆的话是O(NlogN)。
·

小结

上面介绍的建堆时间复杂度和堆排序复杂度,记个结论其实就可以了。当然,从实现的角度上也不难分析出向上调整建堆和向下调整建堆的大致效率上的差距。因为向下调整从第一个非叶子结点开始调整,最坏的情况下也要比向上调整少调整一半的结点。这在效率上已经胜出不少了。

TOPK问题的介绍

TOPK问题是指在一组数据中,找出前K个最大或最小的数据的问题,常见的解决方法包括堆排序、快速排序、归并排序等。该问题在数据分析、机器学习等领域中经常出现。当然有一种特殊场景下用堆进行TOK的筛选非常的妙。假设现在有100亿个整数,要求出前50个数,我们可以建一个小堆,只要遍历的数据比堆顶数据大就替代它进堆(向下调整),最终就可以得到最大的前五十个数。下面用一个样例简单感受一下。

void AdjustDownSH(HpDataType* data, int size, int parent)
{
	//断言检查
	assert(data);

	int child = parent * 2 + 1;

	while (child < size)
	{
		//求出左右孩子较大的那个下标
		if (child + 1 < size && data[child + 1] < data[child])
		{
			child++;
		}
		//父亲比孩子小就交换位置
		if (data[child] < data[parent])
		{
			//交换
			Swap(&data[child], &data[parent]);
			//迭代
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}

}

void PrintTopK(const char* file, int k)
{
	// 1. 建堆--用a中前k个元素建小堆
	int* topk = (int*)malloc(sizeof(int) * k);
	assert(topk);

	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		perror("fopen error");
		return;
	}

	// 读出前k个数据建小堆
	for (int i = 0; i < k; ++i)
	{
		fscanf(fout, "%d", &topk[i]);
	}

	for (int i = (k - 2) / 2; i >= 0; --i)
	{
		AdjustDownSH(topk, k, i);
	}

	// 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换
	int val = 0;
	int ret = fscanf(fout, "%d", &val);
	while (ret != EOF)
	{
		if (val > topk[0])
		{
			topk[0] = val;
			AdjustDownSH(topk, k, 0);
		}

		ret = fscanf(fout, "%d", &val);
	}

	for (int i = 0; i < k; i++)
	{
		printf("%d ", topk[i]);
	}
	printf("\n");

	free(topk);
	fclose(fout);
}

void CreateNDate()
{
	// 造数据
	int n = 10000;
	srand(time(0));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}

	for (size_t i = 0; i < n; ++i)
	{
		int x = rand() % 10000;
		fprintf(fin, "%d\n", x);
	}

	fclose(fin);
}

int main()
{
	CreateNDate();
	PrintTopK("data.txt", 10);

	return 0;
}

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

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

相关文章

STL之string

目录 string的基本实现一. string的基本架构二. 常用方法及实现1.初始化和清理a. 构造函数b. 析构函数c. swapd. 拷贝构造e. operator 2.成员访问a. operator[]b. sizec. c_str 3.迭代器iteratora. beginb. end 4.增删查改a. reserveb. resizec. insertd. push_backe. appendf.…

【连续介质力学】连续体运动学

简介 一个质点从t0出发&#xff0c;随着时间有不同的构形&#xff0c;运动的描述是运动学。 所以需要建立运动方程来表征连续体是如何演化及其性质&#xff08;例如位移、速度、加速度、质量密度、温度等&#xff09;如何随时间变化 初始构形 或者 参考构形&#xff1a; B …

Java反射与注解

文章目录 一、 注解1.简介2. 元注解3. 自定义注解 二、 反射1. 简介2. 理解Class类并获取Class实例3. 类的加载与初始化4. 类加载器ClassLoader5. 获取运行时类的完整结构6. 动态创建对象执行方法7. 反射操作泛型8. 反射操作注解 一、 注解 1.简介 Annotation是JDK5.0开始引入…

day52|动态规划13-子序列问题

子序列系列问题 300.最长递增子序列 什么是递增子序列&#xff1a; 元素之间可以不连续&#xff0c;但是需要保证他们所在位置是元素在数组中的原始位置。 dp数组dp[i]表示以nums[i]为结尾的最长递增子序列的长度。递归函数&#xff1a;dp[i] max(dp[j]1,dp[j])初始化条件&…

Linux常用命令——gpm命令

在线Linux命令查询工具 gpm 提供文字模式下的滑鼠事件处理 补充说明 gpm命令是Linux的虚拟控制台下的鼠标服务器&#xff0c;用于在虚拟控制台下实现鼠标复制和粘贴文本的功能。 语法 gpm(选项)选项 -a&#xff1a;设置加速值&#xff1b; -b&#xff1a;设置波特率&…

chatgpt赋能python:Python中如何取出字符串中的数字并赋予新的变量

Python 中如何取出字符串中的数字并赋予新的变量 在 Python 中&#xff0c;我们经常需要处理字符串&#xff0c;其中可能包含多种类型的数据。当我们需要获取字符串中的数字时&#xff0c;该怎样做呢&#xff1f;本文将介绍取出字符串中的数字的方法&#xff0c;并赋予新的变量…

chatgpt赋能python:Python利器之数字提取

Python 利器之数字提取 Python 是一门强大的编程语言&#xff0c;被广泛应用在数据分析、人工智能等领域。在很多数据分析的场景下&#xff0c;需要从文本中提取数字&#xff0c;本文将介绍如何使用 Python 快速提取文本中的所有数字。 使用正则表达式提取数字 正则表达式&a…

YOLOv7 模型融合

将两种训练模型的检测结果融合 首先需要两种模型&#xff0c;一个是yolov7原有的模型&#xff0c;另一个是你训练出来的模型 其中yolov7.pt是官方提供的模型&#xff0c;有80个类别 其中yolov7_3.7HRW.pt是我们自己训练的模型&#xff0c;有三个分类&#xff0c;举手、看书、写…

WPF开发txt阅读器6:用树形图管理书籍

txt阅读器系列&#xff1a; 需求分析和文件读写目录提取类&#x1f48e;列表控件与目录字体控件绑定书籍管理系统 TreeView控件 TreeView可以通过可折叠的节点来显示层次结构中的信息&#xff0c;是表达文件系统从属关系的不二选择&#xff0c;其最终效果如下 为了构建这个树…

UnityVR--组件6--Animation动画

目录 新建动画Animation Animation组件解释 应用举例1&#xff1a;制作动画片段 应用举例2&#xff1a;添加动画事件 Animator动画控制器 应用举例3&#xff1a;在Animator中设置动画片段间的跳转 本篇使用的API&#xff1a;Animation、Animator以及Animator类中的SetFlo…

【Python可视化大屏】「淄博烧烤」热评舆情分析大屏

文章目录 一、开发背景二、爬虫代码2.1 展示爬取结果2.3 导入MySQL数据库 三、可视化代码3.1 大标题3.2 词云图&#xff08;含&#xff1a;加载停用词&#xff09;3.3 玫瑰图&#xff08;含&#xff1a;snownlp情感分析&#xff09;3.4 柱形图-TOP10关键词3.5 折线图-讨论热度趋…

ViewOverlay-使用简单实践

ViewOverlay-使用简单实践 一、ViewOverlay能实现什么&#xff1f;二、基础用法2.1 一个简单案例2.2 overlay.add(drawable)/groupOverlay.add(view)之后&#xff0c;不显示问题解决2.2.1 add(Drawable)方法2.2.1 add(View)方法 一、ViewOverlay能实现什么&#xff1f; 在Andro…

北京软件外包开发流程

随着软件的规模越做越多&#xff0c;功能也越来越复杂&#xff0c;对项目管理和开发人员也提出了更高的要求。软件开发的流程通常包括需求分析、项目评估与计划、设计、编码、测试等多个环节&#xff0c;今天和大家分享这方面的知识&#xff0c;希望对大家有所帮助。 软件外包开…

关系代数表达式练习(针对难题)

教师关系T&#xff08;T#,TNAME,TITLE&#xff09;课程关系C(C#,CNAME,TNO)学生关系S(S#,SNAME,AGE,SEX)选课关系SC(S#,C#,SCORE) 检索至少选修了C2,C4两门课程的学生学号&#xff1a; 涉及减法相关&#xff1a; 检索不学C2课的学生姓名和年龄 涉及除法相关&#xff1a; 检索…

使用SQL语句创建存储过程

前言: 本篇文章是记录学校学习SQL server中知识,可用于复习资料. 目录 前言:一、存储过程的创建1、创建简单存储过程2、创建带参数的存储过程3、创建带输出参数的存储过程 二 、使用T一SQL语句管理和维护存储过程2.1 使用sp_helptext查看存储过程student_sc的定义脚本2.2 使用…

Redis安装与启动

概念 简介&#xff1a;Redis是基于内存的数据结构存储系统&#xff0c;它可以用作:数据库、缓存和消息中间件。特点&#xff1a;Redis是用C语言开发的一个开源的高性能健值对(key-value)数据库&#xff0c;官方提供的数据是每秒内查询次数十万加。它存储的value类型比较丰富&a…

问麻了…阿里一面索命27问,过了就60W+

前言 在40岁老架构师尼恩的&#xff08;50&#xff09;读者社区中&#xff0c;经常有小伙伴&#xff0c;需要面试阿里、 百度、头条、美团、京东等大厂。 下面是一个小伙伴成功拿到通过了阿里三次技术面试&#xff0c;小伙伴通过三个多小时技术拷问&#xff0c;最终拿到 offe…

git修改commit的注释内容

文章目录 1. 查看所有commit2. 修改最近一次commit注释3. 修改某一次commit注释 要修改 Git 中的 commit 注释内容&#xff0c;可以使用 git commit --amend 命令。具体步骤如下&#xff1a; 1. 查看所有commit 运行 git log --oneline 命令&#xff0c;查看需要修改的 commi…

202316读书笔记|《作家榜名著:花间集》——花半坼,雨初晴,满身香雾簇朝霞,娇艳轻盈香雪腻

202316读书笔记|《作家榜名著&#xff1a;花间集》——花半坼&#xff0c;雨初晴&#xff0c;满身香雾簇朝霞&#xff0c;娇艳轻盈香雪腻 《作家榜名著&#xff1a;花间集》作者赵崇祚 房开江。这里有绮丽的艳词&#xff0c;缱绻的缠绵&#xff0c;温婉绵延的思愁。或慵懒梳洗迟…

springboot+vue.js学生宿舍报修信息管理系统68ozj

本学生宿舍信息管理系统管理员&#xff0c;学生&#xff0c;维修人员&#xff0c;商家四个角色。管理员功能有个人中心&#xff0c;班级管理&#xff0c;学生管理&#xff0c;维修人员管理&#xff0c;商家管理&#xff0c;宿舍信息管理&#xff0c;宿舍安排管理&#xff0c;报…