数据结构 堆——详细动画图解,形象理解

这篇具有很好参考价值的文章主要介绍了数据结构 堆——详细动画图解,形象理解。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构

作者主页

📚lovewold少个r博客主页

​➡️栈和队列博客传送门

🌳参天大树充满生命力,其根深叶茂,分枝扶疏,为我们展示了数据分治的生动形态


目录

🌳 树

树的常见概念

📒树的表示

二叉树

一棵二叉树是结点的一个有限集合,该集合:

📲二叉树的基本类型

🌲满二叉树(完美二叉树)

🌳完全二叉树

🌴 二叉树的性质

💾二叉树的存储方式

顺序存储

链式存储

📌 堆的定义

🔧堆的常用操作

堆的初始化

堆的构建

堆的向上调整的堆化算法

堆的向下调整的堆化算法

​编辑

时间复杂化分析

堆的插入

堆的删除

获取堆顶元素

获取堆有效元素个数

堆的判空

堆的销毁

完整代码

🚀堆的应用

🅰️Top-K问题

堆实现逻辑

🅱️堆排序

✒️总结


前言

        树木随处可见,对于树的形容,我们总是以枝繁叶茂,树木丛生等来形容它。一颗树的主干能长出许多的分支,每一颗分支上又可以有更多分支。而这和我们要学的新结构抽象成的逻辑结构极为相似。

        前面我们学习的数据结构大多数都是一对一的关系,前面接触的单链表,栈等无外乎是对于数据的存储和查找。可是现实中还存在着一对多的关系,也就需要研究这种一对多的数据关系——树(Tree)。同时树中有特殊的完全二叉树,还有特殊的完全二叉树——堆。


🌳 树

树是一种非线性的数据结构,代表这祖先和后代之间的派生关系,树是一种由n(n>=0)个节点组成的集合,其中:

  1. 有且仅有一个节点被指定为根节点;
  2. 其余节点被分为m(m>=0)个互不相交的子集,每个子集本身也是一棵树,称为根的子树。

树在我们的计算机中主要应用为文件的管理,这里我们来用Linux更加直观的展示树在文件管理中的应用。

我们进入Linux系统的根目录,这就好比是文件管理的的主干。

数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构

在这个根目录下我们还可以看见许多的文件以及文件夹,而我们知道的是文件夹中还可以套文件夹和文件

数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构

我们利用tree指令就可以看见整个树形结构的文件系统,观察<tmp>,我们就能看见文件就像一颗树一样,分支纵横,枝繁叶茂,而tmp也只为根的一个分支。

数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构

比如在根目录的树形展示中,通过记数,我们发现拥有17008个文件夹和115225个文件。而文件却多而不乱,通过访问各个分支就能访问到想要的文件,这就是树的魅力所在。

数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构

树的常见概念

  • 【根节点 root】:位于二叉树顶层的节点,没有父节点。
  • 【叶子节点 leaf】:没有子节点的节点,左右节点指向都为空。
  • 【非终端节点或分支节点】:度不为0的节点。
  • 【双亲节点或父节点】:若一个节点含有子节点,则称这个节点为子节点的父节点。
  • 【孩子节点或子节点】:一个节点含有的子树的根节点为该节点的子节点。
  • 【兄弟节点】:具有相同父节点的节点称为兄弟节点。
  • 【堂兄弟节点】:双亲在同一层的节点互为堂兄弟节点。
  • 【节点的祖先】:从根节点到该节点经过的所有分支节点可称之为该节点的祖先。
  • 【边 edge】:连接两个节点的线段,即可以抽象为指针。
  • 【节点的度】:一个节点含有的子树的个数称为该节点的度。二叉树的度叶子节点为零,取值为0,1,2。
  • 【树的度】:一棵树最大的节点的度即为树的度。
  • 【节点的层次 level】:从顶至底部递增,根节点所在层为1。
  • 【节点高度 height】:高度通常表述为根节点到叶子节点的层距离。
  • 【二叉树高度 height】:根节点到叶子节点的层距离。
  • 【深度 depth】:到根节点经过的层数。
  • 【森林 Forest】:多棵互不相交的树的集合。对每个节点而言,其子树的集合即为森林。(但是一般不这么去理解,针对与后面的递归逻辑,我们只简单抽象为左右子树即可)。

📒树的表示

        树结构相对与以前的数据结构要更加复杂,既要保存节点的值域,也要保存节点和节点之间的关系。树在实际中有多种表示方式如:双亲表示法,孩子表示法,孩子双亲表示法以及孩子兄弟表示法。这里我们简单的用孩子兄弟表示法。即我们预想并不能考虑到树的根节点到底有多少分支,但是我们可以不去考虑根节点去链接所有孩子节点。我们通过根管老大,老大管老二的链接方式,让父节点链接左孩子,让左孩子去链接他的兄弟节点。

数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构

​​逻辑上这一棵树的概念如图所示,在代码上我们通过孩子兄弟表示法进行连接。

typedef int DataType;
struct Node
{
 struct Node* firstChild1; // 第一个孩子结点
 struct Node* pNextBrother; // 指向其下一个兄弟结点
 DataType data; // 结点中的数据域
};

数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构

二叉树

        二叉树是一种特殊的树,在日常操作和解决问题的过程中我们更常使用二叉树。什么是二叉树呢,顾名思义,即每一颗节点有两个分支。“一分二支”即作为二叉树操作中主要思想。与链表类似,二叉树的基本单元为节点,每一个节点包含值,左节点,右节点。每一个根节点都通过指针分别指向他的左右节点,在二叉树中,除叶子节点外,其他所有节点都包含子节点和非空子树。数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构

数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构

一棵二叉树是结点的一个有限集合,该集合:

  1. 或者为空
  2.  由一个根节点加上两棵别称为左子树和右子树的二叉树组成

从上图可以看出:

  1.  二叉树不存在度大于2的结点
  2.  二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

📲二叉树的基本类型

🌲满二叉树(完美二叉树)

满二叉树所有层的节点都被完全填满的二叉树。

  • 在满二叉树中,叶节点的度为零,其余所有节点的度为2;
  • 若树的高度为h,则节点总数为数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构,计算方式即为等比数列的前h项合,呈现标准的指数关系。

数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构

🌳完全二叉树

完美二叉树的除了最底层未被填满,其余层都被填满,且叶子节点是从左往右的填充。

数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构

🌴 二叉树的性质

  1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有 2^(i-1) 个结点。
  2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是 2^h-1
  3. 对任何一棵二叉树, 如果度为0其叶结点个数为 n0,度为2的分支结点个数为 n2,则有 n0=n2+1
  4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h= log_2(n+1)。 (ps: log_2 是以2为底,n+1为对数)
  5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:
    • 若i>0,i位置节点的双亲序号:(i-1)/2;若i=0,i为根节点编号,无双亲节点
    • 若2i+1<n,左孩子序号:2i+1;否则无左孩子
    • 若2i+2<n,右孩子序号:2i+2;否则无右孩子

💾二叉树的存储方式

        关于数据结构的存储方式,我们一般使用两种方式,即顺序存储和链式存储,二叉树我们同样使用这两种方式。这时候不免要考虑了,链式存储能理解,顺序存储又怎么能实现这个逻辑呢。首先需要清楚的是,顺序存储即数组更适合连续存储,这就比较适合对完全二叉树的存储。

顺序存储

        我们通过对一颗完美二叉树建立节点索引,按照层序遍历的方式进行存储,就会发现孩子节点和双亲节点可以通过映射公式建立逻辑联系。

  • 即通过任意一个孩子节点的索引值可以通过 (n-1)/2(向下去整)找到其唯一的双亲节点索引。
  • 通过一个双亲节点的索引值 2*n+1 ;可以找到唯一的左孩子节点索引,再通过左孩子节点+1即可访问右孩子节点。

数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构

        非完全二叉树由于后续节点之间不联系,存在空的情况,因此不太适合用数组去存储。数组对于完全二叉树的最优体现就是对堆的实现。

链式存储

        二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是 链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所,在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面到高阶数据结构如红黑树等会用到三叉链,因此不做过多铺垫。

        不是完全二叉树的篇章么,怎么又跳到了堆。别急,堆是基于完全二叉树的升华体现,要了解树不妨先看看堆。

数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构

📌 堆的定义

堆是一种特殊的完全二叉树,它可以用数组来实现,数组中的每个元素对应一个结点。

  • 堆有两种类型:大堆和小堆。
    • 大堆中,每个结点的值都大于等于它的两个子结点的值;
    • 小堆中,每个结点的值都小于等于它的两个子结点的值。
  • 堆中根结点的值是堆中最大或最小的值,可以用来实现优先队列或堆排序等算法。
  • 堆中某个结点的编号为i,那么它的父结点的编号为(i-1)/2,它的左子结点的编号为2i+1,它的右子结点的编号为2i+2。
  • 堆中某个结点所在的层数为logi+1向上取整,其中log在我们程序界是以2为底的对数。例如,编号为7的结点所在的层数为log8=3。

数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构

堆的存储上并不是有序的,但在在每一棵子树都存在根节点相对于左右子树为最大值(大堆),最小值(小堆)。

🔧堆的常用操作

//堆的初始化
void HeapInit(Heap* hp);
// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);

堆的初始化

堆由于用数组来进行存储,父子节点采用索引进行定位。

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}Heap;
void HeapInit(Heap* hp)
{
	assert(hp);
	hp->a = NULL;
	hp->capacity = hp->size = 0;
}
  • 通过任意一个孩子节点的索引值可以通过 (n-1)/2(向下去整)找到其唯一的双亲节点索引。
  • 通过一个双亲节点的索引值 2*n+1 ;可以找到唯一的左孩子节点索引,再通过左孩子节点+1即可访问右孩子节点。

堆的构建

        堆的构建简单来讲就是插入数据进堆,由于插入的val并不能保证大小堆的根节点和左右子节点大小关系,因此需要修复从插入节点到根节点路径上的每一节点。这个进行堆调节的一个过程也叫堆化。一般来讲建堆操作通常是对一个已经存在的数组进行堆化,通过建堆利用堆的大小堆根节点的最大和最小进行排序等操作。因此在入堆过程中我们通常有两种堆化方式,那么这两种方式的思维逻辑和时间复杂度怎么样呢?

堆的向上调整的堆化算法

·        在建堆的过程中首先创建一个空堆,然后遍历列表对每个元素依次入堆操作,这种方式就是在构建堆的时候,新元素进入堆末,再通过对其祖先路径上元素进行比较,如果子节点元素小于父节点,则交换(构建大堆相反),进行"从底到顶"的堆化.

数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构

void AdjustDown(HPDataType* a, int n, int parent)
{
	assert(a);
	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 = child * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

堆分为大堆小堆,这里就用小堆代码来展示。

堆的向下调整的堆化算法

        由上至下的调整操作方式和由下至上的操作相反,构建堆的时候,我们先将列表按照层序遍历的方式连接起来即可。后续,我们需要调节根节点的值与其两个子节点的值进行比较,根据大堆小堆的根节点与子节点来确定二者比较关系。然后循环执行此操作,知道越过叶子节点或遇到无需交换的节点时结束。这里主要的思考方式就是找父比子。

要理解的是,构建堆需要倒序遍历堆,依次对每一个非叶子节点都需要执行向下调整的堆化。

数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构
void AdjustUp(HPDataType* a, int child)
{
	assert(a);
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (parent - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

这里是以图示的大堆向下调整算法代码

时间复杂化分析

        在构建堆的过程中,通过这两种不同的算法,我们有不同的建堆方式。

向上调整建堆:

        首先创建空堆,遍历待插入的列表,依次将每一个元素入堆,即先将元素添加到堆末,然后对该元素向上调整。元素数量为n,每个元素入堆的时间复杂度为logn,因此建堆整体时间在nlogn。这种建堆方式,是上层先达到大小堆关系,因此是从上至下的创建的堆。

数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构

向下调整建堆:

        和向上不同的是先将所有元素原封不动入堆,然后按照倒序层次遍历堆,依次对每一个非叶子节点进行向下调整,当调整到叶子节点的时候调整完毕。这种建堆方式是下层先有序,然后依次向上遍历建堆,因此是从下至上创建的堆。

        这里理解起来有点麻烦,为什么向下调整是倒序建立的堆呢。答案其实很简单,要对一个根节点进行如图所示的调整,得先保证左右子树是一定的大小堆,然后才能向下调整,否则调整是没有意义的。而要保证每一个根节点的左右子树是堆,只需要从倒数第二层即叶节点上一层开始向下调整,保证左右子树和根节点的大小关系。往上遍历的过程中,自然一直能保证左右子树是大堆或者小堆。

数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构

这种方式的时间复杂度是多少呢,假设完全二叉树的节点数量为n,则节点数量为(n+1)/2(向下整除),因此需要堆化的数量为(n-1)/2。从顶至底部堆化的过程中,每个节点最多堆化到叶子节点,因此最大高度为二叉树高度logn,所有时间复杂度为nlogn么?

我们来点更加精确的计算。数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构

一个节点从底到上的一个堆化中,最大需要去调整的次数为该节点到叶子节点的距离,而该距离为节点高度,因此我们可以得到公式 节点数量*节点高度。

叶子节点不用去调整,因此只需要计算到高度1即可。

Tn = 2⁰ · h + 2¹ · (h - 1) + 2² · (h - 2) + ...... + 2ʰ⁻² · 2 + 2ʰ⁻¹ · 1 + 2ʰ · 0

把首尾两个元素简化,记为①式:

①: Tn = h + 2¹ · (h - 1) + 2² · (h - 2) + ...... + 2ʰ⁻² · 2 + 2ʰ⁻¹

对①等于号左右两边乘以2,记为②式:

②: 2Tn = 2¹ · h + 2² · (h - 1) + 2³ · (h - 2) + ...... + 2ʰ⁻¹ · 2 + 2ʰ

那么用②式减去①式,其中②式的操作数右移一位使指数相同的部分对齐,错位相减法。

得到

Tn = n - log₂(n + 1)约等于n

向下调整的时间复杂度仅O(n),非常高效,因此这里我们建堆过程中采用向下调整的方法。

void HeapCreate(Heap* hp, HPDataType* a, int n)//对已有数组可以直接构建堆
{
	assert(hp);
	assert(a);
	
	hp->a = (HPDataType*)malloc(sizeof(HPDataType) * n);//开辟空间,如果已有数组,之间开辟等大空间
	if (hp->a == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	hp->capacity = n;
	hp->size = n;
	memcpy(hp->a, a, sizeof(HPDataType) * n);

	for (int i = 0; i < n; i++)
	{
		AdjustUp(hp->a, i);
	}
}

堆的插入

堆插入新元素,即先入堆末,在对此进行向上调整即可。

数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构

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, sizeof(HPDataType) * newcapacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}
		hp->a = tmp;
		hp->capacity = newcapacity;
	}
	hp->a[hp->size] = x;
	hp->size++;
	AdjustUp(hp->a, hp->size - 1);
}

堆的删除

        删除元素我们要考虑的是怎么去删除比较有意义,不论是大堆小堆,我们能直接获取就是堆顶元素,而且堆的性质能保证这个元素为堆最大或者最小。因此很显然直接删堆首元素嘛。但是当我们删除堆顶元素后,怎么样能保证他继续是一个堆呢。

        这个时候我们先将堆首和堆末替换,扶小弟上位,后面对小弟进行向下调整即可。

数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构

void HeapPop(Heap* hp)
{
	assert(hp);
	assert(hp->size > 0);

	Swap(&hp->a[0], &hp->a[hp->size - 1]);
	hp->size--;
	AdjustDown(hp->a, hp->size, 0);
}

获取堆顶元素

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

获取堆有效元素个数

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

堆的判空

int HeapEmpty(Heap* hp)
{
	assert(hp);
	return hp->size == 0;
}

堆的销毁

void HeapDestory(Heap* hp)
{
	assert(hp);
	free(hp->a);
	hp->a = NULL;
	hp->capacity = hp->size = 0;
}

完整代码

//头文件Heap.h
#pragma once
#include<stdlib.h>
#include<stdbool.h>
#include<stdio.h>
#include<assert.h>
#include<malloc.h>
#include<string.h>

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}Heap;


//自上而下调整
void AdjustUp(HPDataType* a, int child);
//自下而上调整
void AdjustDown(HPDataType* a, int n, int parent);
//堆的初始化
void HeapInit(Heap* hp);
// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);
//交换
void Swap(HPDataType* a, HPDataType* b);
//打印
void HeapPrint(Heap* hp);


//实现源文件 Heap.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Heap.h"

void HeapInit(Heap* hp)
{
	assert(hp);
	hp->a = NULL;
	hp->capacity = hp->size = 0;
}
void HeapCreate(Heap* hp, HPDataType* a, int n)//对已有数组可以直接构建堆
{
	assert(hp);
	assert(a);
	
	hp->a = (HPDataType*)malloc(sizeof(HPDataType) * n);//开辟空间,如果已有数组,之间开辟等大空间
	if (hp->a == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	hp->capacity = n;
	hp->size = n;
	memcpy(hp->a, a, sizeof(HPDataType) * n);

	for (int i = 0; i < n; i++)
	{
		AdjustUp(hp->a, i);
	}
}
void Swap(HPDataType* a, HPDataType* b)
{
	HPDataType tmp = *a;
	*a = *b;
	*b = tmp;
}
void AdjustUp(HPDataType* a, int child)
{
	assert(a);
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (parent - 1) / 2;
		}
		else
		{
			break;
		}
	}
}
void AdjustDown(HPDataType* a, int n, int parent)
{
	assert(a);
	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 = child * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
void HeapDestory(Heap* hp)
{
	assert(hp);
	free(hp->a);
	hp->a = NULL;
	hp->capacity = hp->size = 0;
}
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, sizeof(HPDataType) * newcapacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}
		hp->a = tmp;
		hp->capacity = newcapacity;
	}
	hp->a[hp->size] = x;
	hp->size++;
	AdjustUp(hp->a, hp->size - 1);
}
void HeapPop(Heap* hp)
{
	assert(hp);
	assert(hp->size > 0);

	Swap(&hp->a[0], &hp->a[hp->size - 1]);
	hp->size--;
	AdjustDown(hp->a, hp->size, 0);
}
HPDataType HeapTop(Heap* hp)
{
	assert(hp);
	assert(hp->size > 0);
	return hp->a[0];
}
int HeapSize(Heap* hp)
{
	assert(hp);
	return hp->size;
}
int HeapEmpty(Heap* hp)
{
	assert(hp);
	return hp->size == 0;
}

void HeapPrint(Heap* hp)
{
	assert(hp);

	for (size_t i = 0; i < hp->size; i++)
	{
		printf("%d ", hp->a[i]);
	}
	printf("\n");
}

🚀堆的应用

🅰️Top-K问题

        在日常生活中,我们能看见各种各样的榜单,或者是你微信运动的步数,亦或者是美团的餐馆评分前五名,又或者是自己游戏中的排行榜。很多情况下,这些排行榜并不会展示所有的数据,这样的数据反而没有任何参考意义。很多情况下我们总是盯这Top-100,Top-10。因为这些根据某种评分后决定的数据更有含金量,是大多数人的选择或者想知道的信息。

数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构

在实现中其实就可以利用堆完成这样子的Top-K问题。

堆实现逻辑

  1. 首先建立一个小堆,其堆顶元素最小。
  2. 再将数组的前K个元素入堆。
  3. 从第K+1个元素开始,若当前元素大于堆顶元素,堆顶元素出堆。当前元素入堆并调整。
  4. 遍历整个数组后,堆中保存的就是最大k个元素。

        整个建堆的过程时间复杂度就很低,相比于多次排序找最值,时间复杂度在k值最小时间复杂度O(n);k较大时,时间复杂度也不会超过O(nlogn)。这种方式在开辟空间也只用开辟K个节点的空间,空间复杂度也很低。

        这种方式适用于动态的数据流变换,在不断加入数据时,堆内元素始终只需要维护其K个,时刻可以保证K个元素及时更新。

        这里我们通过文件操作进行写入随机值,并且利用这些随机值解决Top-K问题来演示。

void PrintTopK(const char* filename, int k)
{
	// 1. 建堆--用a中前k个元素建堆
	FILE* fout = fopen(filename, "r");
	if (fout == NULL)
	{
		perror("fopen fail");
		return;
	}

	int* minheap = (int*)malloc(sizeof(int) * k);
	if (minheap == NULL)
	{
		perror("malloc fail");
		return;
	}

	for (int i = 0; i < k; i++)
	{
		fscanf(fout, "%d", &minheap[i]);
	}

	// 前k个数建小堆
	for (int i = (k - 2) / 2; i >= 0; --i)
	{
		AdjustDown(minheap, k, i);
	}


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


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

	free(minheap);
	fclose(fout);
}

// fprintf  fscanf

void CreateNDate()
{
	// 造数据
	int n = 10000000;//造了一个千万级别的数据
	srand(time(0));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");//打开文件
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}

	for (int i = 0; i < n; ++i)
	{
		int x = (rand() + i) % 10000000;//随机值
		fprintf(fin, "%d\n", x);//写入文件
	}

	fclose(fin);//关闭文件
}

int main()
{
	CreateNDate();
	PrintTopK("data.txt", 100);//Top-100

	return 0;
}

数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构

可以看见这个数据量还是相当大的,要是用排序等去实现得跑到天昏地暗。我们改变了数据的几个值,观察他能否找出来,可以看见8133211323这个数据能很快被找出来。

数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构

🅱️堆排序

数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构

        堆排序一个指定的数列,首先我们要确定的是排升序还是降序。升序构建大堆,降序构建小堆。

        但是我们还要思考的是对于一个已有的数列,堆是需要额外开辟一块空间进行打印操作么。很显然,我们需要将一个数组转变为一个有序数组,我们可以直接用这个数组本身作为一个堆,然后对数组依次入堆并向下调整。升降序只需要大小堆构建控制即可。

void HeapSort(int* a, int n)
{
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

数据结构 堆——详细动画图解,形象理解,数据结构与算法,数据结构


✒️总结

😄堆(Heap)是二叉树和数组的一种抽象数据结构

堆的基本概念:

  1. 堆是一种树状数据结构,通常是一个完全二叉树。
  2. 堆分为两种主要类型:最大堆(Max Heap)和最小堆(Min Heap),具体取决于根节点的值与其子节点的关系。
  3. 在最大堆中,父节点的值大于或等于子节点的值,最大值位于根节点。
  4. 在最小堆中,父节点的值小于或等于子节点的值,最小值位于根节点。

堆常用的应用:

  1. 堆排序:堆排序是一种高效的排序算法,通过使用堆数据结构,可以将数组以O(n log n)的时间复杂度进行原地排序。
  2. 堆可以用于Top-K算法问题。

堆的难点和理解难点:

  1. 堆的插入和删除操作需要维护堆的性质,这涉及到向下调整和向上调整操作。
  2. 确保堆的性质在插入或删除元素后仍然得到维护,需要深刻理解堆的特性。
  3. 堆排序算法的实现相对复杂,需要理解堆的建立和维护。
  4. 在实际应用中,选择最大堆还是最小堆取决于问题的性质。

作者个人水平有限,文章难免出错,如有错误欢迎指正!文章来源地址https://www.toymoban.com/news/detail-714539.html


到了这里,关于数据结构 堆——详细动画图解,形象理解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 数据结构 —— 双向链表(超详细图解 & 接口函数实现)

    数据结构 —— 顺序表 数据结构 —— 单链表 数据结构 —— 双向链表 数据结构 —— 队列 数据结构 —— 栈 数据结构 —— 堆 数据结构 —— 二叉树 数据结构 —— 八大排序 数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元

    2024年02月11日
    浏览(41)
  • 【图解数据结构】顺序表实战指南:手把手教你详细实现(超详细解析)

    🌈个人主页: 聆风吟 🔥系列专栏: 图解数据结构、算法模板 🔖少年有梦不应止于心动,更要付诸行动。 线性表(linear list):线性表是一种数据结构,由n个具有相同数据类型的元素构成一个有限序列。 线性表可以用数组、链表、栈等方式实现,常见的线性表有数组、链

    2024年01月22日
    浏览(59)
  • 【算法与数据结构】归并排序的代码实现(详细图解)以及master公式的讲解

    目录 1、归并排序  1.1、算法描述  1.2、图解说明 2、代码实现  3、master公式 3.1、公式以及结论 3.2、适用于某些特殊的递归 3.3、计算归并排序的时间复杂度 归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用 递归 或者说是 分治法 (Divide and Conquer)的一个非

    2024年02月08日
    浏览(58)
  • 数据结构与算法 —— 最短路径Dijkstra算法(迪杰斯特拉)详细图解以及python实现

    目录 前言 1. 介绍 2. 加权图 2.1 概念 3. 最短路径 -- Dijkstra 算法 3.1 历史 3.2 Dijkstra 算法的基本思路 3.3 Dijkstra 算法图解 4.  python中dijkstra算法的实现 5. 总结  前两章我们讲到了关于图的基本知识,和广度/深度优先搜索。 本章,我们将介绍 加权图 和 最短路径 的相关知识。 最

    2024年02月12日
    浏览(51)
  • 【茶话数据结构】查找最短路径——Dijkstra算法详解(保姆式详细图解,步步紧逼,保你学会)

     💯 博客内容:【茶话数据结构】查找最短路径——Dijkstra算法详解 😀 作  者:陈大大陈 🦉所属专栏:数据结构笔记 🚀 个人简介:一个正在努力学技术的准前端,专注基础和实战分享 ,欢迎私信! 💖 欢迎大家:这里是CSDN,我总结知识和写笔记的地方,喜欢的话请三

    2024年02月08日
    浏览(43)
  • 数据结构中 p->next的详细理解

    1.原因 p-next 理解有误,大多是对 c 语言中的结构体的理解有误,建议看完本文章,去自行复习一下。 2.理解 在结构体中 由数据域、指针域组成 3.实例 在数据结构中  线性表的插入(头插法或者尾插法)中通常使用的交换语句 第一段代码的意思是 :p  指针指向的节点的指针

    2024年02月12日
    浏览(22)
  • 数据结构刷题篇 之 【力扣二叉树基础OJ】详细讲解(含每道题链接及递归图解)

    有没有一起拼用银行卡的,取钱的时候我用,存钱的时候你用 难度等级:⭐ 直达链接:相同的树 难度等级:⭐ 直达链接:单值二叉树 难度等级:⭐⭐ 直达链接:对称二叉树 难度等级:⭐⭐⭐ 直达链接:二叉树的前序遍历 难度等级:⭐⭐⭐⭐ 直达链接:另一颗子树 注:

    2024年04月16日
    浏览(78)
  • LSTM从入门到精通(形象的图解,详细的代码和注释,完美的数学推导过程)

    先附上这篇文章的一个思维导图 按照八股文来说:RNN实际上就是一个带有 记忆 的时间序列的预测模型 RNN的 细胞结构图 如下: softmax激活函数只是我举的一个例子,实际上得到yt也可以通过其他的激活函数得到 其中at-1代表t-1时刻隐藏状态,at代表经过Xt这一t时刻的输入之后

    2024年02月10日
    浏览(46)
  • 【C++&数据结构】超详细一文带小白轻松全面理解 [ 二叉搜索树 ]—— [从零实现&逐过程分析&代码演示&简练易懂](23)

    前言 大家好吖,欢迎来到 YY 滴数据结构系列 ,热烈欢迎! 本章主要内容面向接触过C++的老铁 主要内容含: 欢迎订阅 YY 滴C++专栏!更多干货持续更新!以下是传送门! YY的《C++》专栏 YY的《C++11》专栏 YY的《Linux》专栏 YY的《数据结构》专栏 YY的《C语言基础》专栏 YY的《初

    2024年02月05日
    浏览(112)
  • 【数据结构】图解八大排序(下)

    在上一篇文章中,我们已经学习了五种排序算法,还没看过的小伙伴可以去看一下:传送门 今天要讲的是八大排序中剩下的三种,这三种排序算法用的是非常多的,需要好好掌握。 下面介绍的排序算法均以排升序为例。 快排的思想 是分治,就是选定一个基准值,使这个值的

    2024年02月17日
    浏览(45)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包