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

news2024/10/5 2:11:09

作者主页

📚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,则节点总数为2^{h+1}+1,计算方式即为等比数列的前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. 在实际应用中,选择最大堆还是最小堆取决于问题的性质。

作者个人水平有限,文章难免出错,如有错误欢迎指正!


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

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

相关文章

【每日一题】找出数组的串联值

文章目录 Tag题目来源题目解读解题思路方法一&#xff1a;双指针 写在最后 Tag 【模拟】【双指针】【数组】【2023-10-12】 题目来源 2562. 找出数组的串联值 题目解读 串联值指的是将两个数字串联起来形成新的数字。现在要一次计算一个数组的地也给数和最后一个数的串联值&…

【java学习】类的成员之三:构造方法(即构造器)(25)

文章目录 1. 构造器(构造方法)基本概念2. 语法格式3. 构造器(构造方法)的种类4. 从代码中理解构造函数5. 练习题5.1. 题目15.2. 题目25.3. 题目3 1. 构造器(构造方法)基本概念 构造器的特征 (1) 它具有与类相同的名称 (2) 它不声明返回值类型。&#xff08;与声明为 void 不同&…

【Vue基础-数字大屏】图表自适应大小

一、需求描述 缩放页面&#xff0c;页面中的图标自适应缩放 二、关键代码 1、图表横向全屏 <style scoped>#myecharts{ width: 100%;height: 600px; border: 2px solid rgb(0, 255, 255);} </style> 2、监听&#xff0c;使图表自适应缩放 myChart.setOption(o…

kubernetes环境 搭建

1、准备2台机器 2、安装docker环境&#xff08;参考官网&#xff09; 1、 sudo apt-get update sudo apt-get install ca-certificates curl gnupg2、 sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dea…

代码随想录算法训练营第五十九天 |647. 回文子串、516.最长回文子序列、动态规划总结篇

一、647. 回文子串 题目链接/文章讲解&#xff1a;代码随想录 思考&#xff1a; 1.确定dp数组&#xff08;dp table&#xff09;以及下标的含义 如果本题定义dp[i] 为 下标i结尾的字符串有 dp[i]个回文串的话&#xff1a; 会发现很难找到递归关系&#xff0c;dp[i] 和 dp[i-1]…

ubuntu20.04 nerf Instant-ngp

Instant-ngp linux ubuntu 20.04 GPU RTX3050Ti Instant-ngp官方文档地址 https://github.com/NVlabs/instant-ngp 参考链接Instant-ngp linux部署及使用 - 简书 Ubuntu20.04复现instant-ngp&#xff0c;自建数据集&#xff0c;导出mesh_XINYU W的博客-CSDN博客 步骤 安装基…

Elasticsearch 分片内部原理—使文本可被搜索、动态更新索引

目录 一、使文本可被搜索 不变性 二、动态更新索引 删除和更新 一、使文本可被搜索 必须解决的第一个挑战是如何使文本可被搜索。 传统的数据库每个字段存储单个值&#xff0c;但这对全文检索并不够。文本字段中的每个单词需要被搜索&#xff0c;对数据库意味着需要单个字…

object property order

起因 使用 prosemirror 时&#xff0c;想要取 schema 实例设置的 nodes const mySchema new Schema({nodes: {doc: {},customBlock: {},text: {},},marks: {}, });在浏览器控制台查看 mySchema&#xff0c;发现两个地方有nodes&#xff1a; mySchema.nodes 是普通对象&…

某医疗机构:建立S-SDLC安全开发流程,保障医疗前沿科技应用高质量发展

某医疗机构是头部资本集团旗下专注大健康领域战略性投资与运营的实业公司&#xff0c;市场规模超300亿。该医疗机构已完成数字赋能&#xff0c;形成了标准化、专业化、数字化的疾病和健康管理体系&#xff0c;将进一步规划战略方向&#xff0c;为人工智能纳米技术、高温超导、生…

【分享】小红书采集图片下载到本地

前面我们把图片链接都存到了excel里&#xff0c;现在想要把图片都下载到本地文件夹 下面是实现的python代码 图片链接存在第三列单元格&#xff0c;并且是多个图片\n分割的&#xff0c;现在把他们全部下载到本地 import pandas as pd import requestsdf pd.read_excel(&quo…

关于scanf和printf的格式控制修饰符

关于scanf和printf的格式控制修饰符

input时间控件选择时禁用某个日期之前或之后

【版权所有&#xff0c;文章允许转载&#xff0c;但须以链接方式注明源地址&#xff0c;否则追究法律责任】【创作不易&#xff0c;点个赞就是对我最大的支持】 前言 仅作为学习笔记&#xff0c;供大家参考 总结的不错的话&#xff0c;记得点赞收藏关注哦&#xff01; 目录 …

应届生写简历不如AI?HR招人到底看什么

又双叒创新高&#xff01; 据悉&#xff0c;2024届高校毕业生人数预计有 1187 万人&#xff0c;比2023届将近多30万人&#xff01;相当于冰岛一个国家的人口啊&#xff01; 毋庸置疑&#xff0c;大学生的就业压力只增无减。也因此&#xff0c;2024届的应届生们为了博HR关注&…

2023年中国艺人管理行业发展历程及趋势分析:未来市场规模还会不断增加[图]

艺人管理行业是指一系列涉及艺人职业发展、形象管理、工作安排、合同管理、媒体宣传策划、财务管理等方面的专业服务和活动。它旨在协助艺人实现个人职业目标&#xff0c;提升艺人在娱乐行业中的知名度、影响力和经济效益&#xff0c;维持艺人形象的稳定和提高其工作的效率和质…

公众号营业执照注销被冻结了,怎么迁移?

公众号迁移后原来内容还在么&#xff1f;通过公众号迁移&#xff0c;可以实现这些目的&#xff1a;主体变更、开通留言功能、多号合并、订阅号升级为服务号、服务号转为订阅号。公众号迁移流程&#xff1a;①办理公证&#xff1b;②提交迁移申请&#xff1b;③第三方审核&#…

json库的基本使用

目录 1 将python变量转变为json变量 dumps() 2 将json变量转换为python变量 loads() 3 将键值对存储为json文件 dump() 4 读取json文件 前后端常用json进行信息的交互&#xff0c;不转json会有收不到的情况 我们先看一下转换成json的服务 发现该有的信息都有&#x…

实战指南:使用 kube-prometheus-stack 监控 K3s 集群

作者简介 王海龙&#xff0c;Rancher 中国社区技术经理&#xff0c;Linux Foundation APAC Evangelist&#xff0c;负责 Rancher 中国技术社区的维护和运营。拥有 9 年的云计算领域经验&#xff0c;经历了 OpenStack 到 Kubernetes 的技术变革&#xff0c;无论底层操作系统 Lin…

【JavaEE初阶】 死锁详解

文章目录 &#x1f38b;死锁的概念&#x1f333;死锁的三个典型情况&#x1f6a9;一个线程一把锁&#x1f6a9;两个线程两把锁&#x1f6a9;n个线程m把锁(哲学家就餐问题) &#x1f384;如何破除死锁&#x1f6a9;破坏循环等待 本文重点&#xff1a; 死锁咋回事 死锁的三个典型…

2023年中国石油催化裂化剂行业供需、竞争格局及市场规模分析[图]

催化裂化是石油炼制过程之一&#xff0c;是在热和催化剂的作用下使重质油发生裂化反应&#xff0c;转变为裂化气、汽油和柴油等的过程。中国原油加工量在这一阶段逐年提升&#xff0c;2022年国内原油加工量67589.7万吨。 2016-2022年中国原油加工量情况 资料来源&#xff1a;国…

learn编码器

目录 1、编码器的作用 2、编码器的结构图 3、代码实现如下 1、编码器的作用 编码器用于对输入进行指定的特征提取的过程&#xff0c;也称为编码&#xff0c;由 N 个编码器层堆叠而成 2、编码器的结构图 3、代码实现如下 import numpy as np from torch.autograd import Vari…