【堆】数据结构堆的实现(万字详解)

news2024/11/27 14:51:47

前言:
在上一期中我们讲到了树以及二叉树的基本的概念,有了之前的认识,今天我们将来具体实现一种二叉树的存储结构“堆”!!!

目录

  • 1.二叉树顺序结构介绍
  • 2.堆的概念及结构
  • 3.调整算法
    • 3.1向上调整算法
      • 3.1.1算法思想
      • 3.1.2算法代码
      • 3.1.3算法图解分析
      • 3.1.4算法复杂度分析
    • 3.2向下调整算法(重点)
      • 3.2.1算法思想(建小堆)
      • 3.2.2算法代码
      • 3.2.3算法图解分析
      • 3.2.4算法复杂度分析
    • 3.3算法对比
  • 4.堆的实现
    • 4.1 数据类型重定义
    • 4.2堆结构的定义
    • 4.3堆的常见操作(重点)
    • 4.4接口功能实现
      • 4.4.1堆的初始化
      • 4.4.2堆的销毁
      • 4.4.3堆元素的打印
      • 4.4.4堆元素交换
      • 4.4.5堆的插入
      • 4.4.6堆的删除
      • 4.4.7构建堆(及建堆时间复杂度)
      • 4.4.8取堆顶的数据
      • 4.4.9堆的数据个数
      • 4.4.10堆的判空
  • 5.堆的运用
    • 5.1 堆排序
    • 5.2TOP-K问题
  • 6.选择题讲解
  • 7.总结

1.二叉树顺序结构介绍

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

在这里插入图片描述

2.堆的概念及结构

如果有一个关键码的集合K = { k0, k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中。并满足以下条件:
在这里插入图片描述

则称为小堆(或大堆)。
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆

堆的性质:

1.堆中某个节点的值总是不大于或不小于其父节点的值;
2.堆总是一棵完全二叉树。

接下来对上面提到的最大堆和最小堆进行理解:

最小堆要求,对于任意一个父结点来说,其子结点的值都大于这个父节点;
同理,最大堆就是说,其子节点的值都小于这个父节点,具体如下:

在这里插入图片描述

1.对于上图的树形结构,其实并不是它在内存中真正的样子,这只是我们为了更好地理解而想象出来的;它在内存中真正的样子应该是像一种数组的存储方式。

2.而且从上图我们还可以得出一个规律,就是这个堆中的各个结点之间它是存在一种关系的,在上篇博客中我们已经讲了树和二叉树的基本概念讲到了父亲结点和孩子结点。那么我们就可以用下标来表示一下父亲结点和孩子结点直接的关系,具体如下:

lchild = parent * 2 + 1 左孩子
rchild = parent * 2 + 2 右孩子

从而我们还可以得出父结点的下标表示为:
parent = (child - 1) / 2

注意:

堆的根节点中存放的是最大或者最小元素,但是其他节点的排序顺序是未知的。例如,在一个最大堆中,最大的那一个元素总是位于 index 0的位置,但是最小的元素则未必是最后一个元素。唯一能够保证的是最小的元素是一个叶节点,但是不确定是哪一个。

3.调整算法

3.1向上调整算法

3.1.1算法思想

使用向上调整算法的思路是将要插入的数据放在数组尾部,进行向上调整。

但我们需要向堆中插入数据元素,使其仍然是一个堆时,就要使用到向上调整算法进行相关的调整。因为向堆中插入数据是将数据插入到数据尾部,此时可能就会出现不满足小堆(大堆)定义的情况,因此,需要堆其进行调整,向上调整算法和向下调整算法思路类似,此处以小堆为例:在调整的过程中,我们是从数组中最后一个元素的位置开始的,不断的向上进行与父亲结点存储的值进行对比,如果出现孩子的值比父亲的值大,那么就需要交换此时的父亲和孩子的值,一直交换到child到达根节点为止,当然也可能出现比较好的情况,就是在交换的半途中就出现了满足堆的性质,那么此时就不再需要继续向上进行调整了。

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

解析:

1.首先我们需要知道传过来的孩子结点的下标;

void AdjustUp(HPDataType* a, int child)

2.当我们知道了这个孩子结点的下标之后,我们就会想着去寻找其父亲结点的下标值,因为是要和它的父亲进行相应的比较;

int parent = (child - 1) / 2;

3.紧接着我们需要对孩子结点进行一个判断的操作,向上调整孩子结点就会被赋值到它祖先的结点处,这时我们的父亲结点就会发生越界行为,因此这里的判断条件为【child > 0】;

	while (child > 0)

4.孩子结点正确判断之后,就可以去进行相应的比较,这里的判断条件则是孩子比它父亲来的大,如果满足循环条件,那么就将它们俩进行一个交换即可(如果是小堆则只需改变这里的判断符号),若a[chaild]>=a[parent]则说明越界满足小堆,直接break;

if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}

3.1.3算法图解分析

我们以下图为例,通过在尾结点插入元素10来进行相关的调整:
在这里插入图片描述

3.1.4算法复杂度分析

为了保证每次调整后,已经调整过的部分不被打乱,我们需要从第二层开始调整,最坏的情况就是每个节点都向上调整了
h2-1次(h2表示以该节点所在的层数),设向上调整的总次数为关于h的函数F(h),则F(h)表示如下:

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

3.2向下调整算法(重点)

3.2.1算法思想(建小堆)

向下调整算法-前提:
若想将其调整为小堆,那么根结点的左右子树必须都为小堆;
若想将其调整为大堆,那么根结点的左右子树必须都为大堆。

向下调整算法的核心思想:选出左右孩子中小的哪一个,跟父亲交换,小的往上浮,大的往下沉,如果要建大堆则相反:

1.从根结点处开始,选出左右孩子中值较小的孩子。
2.让小的孩子与其父亲进行比较。若小的孩子比父亲还小,则该孩子与其父亲的位置进行交换。并将原来小的孩子的位置当成父亲继续向下进行调整,直到调整到叶子结点为止。
3.若小的孩子比父亲大,则不需处理了,调整完成,整个树已经是小堆了。

3.2.2算法代码

void AdjustDown(HPDataType* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		// 确认child指向大的那个孩子
		if (child+1 < n && a[child+1] < a[child])
		{
			++child;
		}

		// 1、孩子大于父亲,交换,继续向下调整
		// 2、孩子小于父亲,则调整结束
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

解析:
1.对于向下调整算法,我们开始传入的就不是孩子结点的下标了。因为调整的是堆顶数据,也就是根节点的下标,而对于根节点来说是没有父亲的,所以传入的就是父亲下标:

void AdjustDown(HPDataType* a, int n, int parent)

2.对比向上调整算法是知道孩子求父亲,这里的话就是知道父亲求孩子了,但是大家可能会有一些疑问就是,孩子结点不是分为左孩子和右孩子吗,不是有两个吗,为什么我只写了一个【child】,我们在这里就假设最大的孩子结点为左孩子,在后面在进行相关判断

int child = parent * 2 + 1;
{
	++child;
}

3.接下来就进行相关的判断操作,因为我们上面假设左孩子最大,在这里就判断右孩子是否合法的同时再去验证我们的假设是否成立。接下去将左右孩子的值进行一个比较,若是右孩子来的大就将【child++】,左孩子变成右孩子。

if (child+1 < n && a[child+1] < a[child])

4.紧接着和向上调整法类似的操作

        // 1、孩子大于父亲,交换,继续向下调整
		// 2、孩子小于父亲,则调整结束
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}

3.2.3算法图解分析

现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆

int array[] = {27,15,19,18,28,34,65,49,25,37};

在这里插入图片描述
解析:
对于向下调整算法,最主要的一个前提就是根节点的左右子树都要是大堆或者都要是小堆,就根结点不满足,才可以去进行一个向下调整,此时就需要使用到这个【向下调整算法】,我这个是小堆的调整,大堆的话刚好相反。

原理:找出当前结点的两个孩子结点中较小的那一个换上来,将这个【27】换下去,交换了之后我们发现此时还不构成小堆,因此我们还需要再根据上述方法进行调整,然后直到这个【27】的孩子结点到达【n - 1】就不作交换了,因为【n - 1】就相当于是位于数组下标的最后一个值

3.2.4算法复杂度分析

在这里插入图片描述
在这里插入图片描述

3.3算法对比

向下调整算法时间复杂度:O(N)
向上调整算法时间复杂度:O(N*logN)

我们发现对于向下调整算法,层数越高的结点,需调整的次数越少,而层数越高结点越多,也就是大多数结点只需要调整少次;
而向上调整算法恰恰相反,大多数结点需要调整多数次,而基于满二叉树结构的特点,我们发现最后一层节点数占总结点数的几乎一半,对于最后一层的调整次数对整体影响很大,因此我们需要让最后一层调整少次,对比我们得出,选择向下调整算法更优。

4.堆的实现

4.1 数据类型重定义

跟我们之前学习的数据结构一样,我们为了方便存储各种数据类型,我们会先对堆存储的数据类型进行重定义,具体如下:

typedef int HPDataType;

4.2堆结构的定义

通过上文的了解我们知道堆本质就是一棵完全二叉树,我们是采用顺序存储的方式来实现堆的,在前面我们已经学习过顺序表的实现,所以这里堆的实现我们可以类比顺序表的实现。我们可以考虑将堆中树的结点存储的值,按照一定的顺序存储在一个数组中,那么数组当然就是采用动态开辟的数组比较方便进行随时的扩容,其实现可以参考顺序表的实现,因此我们可以得出所需要的为:

堆的结构中需要包含一个数据域,就是动态数组;
需要一个能够随时记录堆中存储的有效数据个数的变量;
需要一个可以随时记录堆中数组的容量的变量

代码展示为:

typedef struct Heap
{
	HPDataType* a; // 数据域
	int size;      // 堆中有效数据个数
	int capacity;  // 堆的容量
}HP;               //重定义

4.3堆的常见操作(重点)

堆的主要操作是插入和删除最小(最大)元素(元素值本身为优先级值,小元素享有高优先级)。在插入或者删除操作之后,我们必须保持该实现应有的性质:

  1. 完全二叉树
  2. 每个节点值都小于或等于它的子节点。
// 堆的构建
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 HeapPrint(HP* php);
//初始化堆
void HeapInit(HP* php);

4.4接口功能实现

4.4.1堆的初始化

第一个功能接口就是实现堆的初始化操作,这个代码比较简单,跟我们之前的顺序表的初始化思路差不多,我们直接看代码:

void HeapInit(HP* php)
{
	assert(php);
	php->a = NULL;
	php->size = php->capacity = 0;
}

注意:
这里初始化时空间的开辟是任我们选择的,当然你也可以在初始化这一块就把堆的数据存放空间给开出来,我们可以选择这里开空间,后面用realloc修改空间大小(realloc接收的指针为NULL时,他的作用和malloc是一样的,所以这里开不开辟空间都是可以的)

4.4.2堆的销毁

销毁函数中本质是需要对堆中向系统申请的空间进行释放,防止出现内存泄露,释放完之后最好也是需要对该指针进行置空操作。这里的销毁操作跟我们之前写的顺序表等操作大差不差,基本思想都是相同的,具体如下:

void HeapDestroy(HP* php)
{
	assert(php);
	free(php->a);
	php->a = NULL;
	php->size = php->capacity = 0;
}

4.4.3堆元素的打印

打印操作就是将堆中的各个元素打印出来,没什么可说的,具体代码如下:

void HeapPrint(HP* php)
{
	assert(php);
	for (int i = 0; i < php->size; ++i)
	{
		printf("%d ", php->a[i]);
	}
	printf("\n");
}

4.4.4堆元素交换

很明显就是字面意思,交换堆中数据元素的位置,在接下来的插入和删除操作中需要用到这个功能函数,具体如下:

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

4.4.5堆的插入

1. 有了上述的操作后我们就会想到往堆中插入元素,跟我们之前的顺序表等数据结构类似。对于数组,在插入数据之前我们还是需要先检查空间是否满,如果满了,那么首先需要考虑扩容问题,扩容的基本操作和顺序表中基本保持一致。

2. 堆进行插入操作时,我们在插入数据的时候,只是将数据插入到堆中数组的最后一个位置,那么这个时候,插入数据之后,堆中的数据之间可能仍然满足堆的性质,也可能不满足堆的性质,所以,在插入数据之后我们需要对堆中的数据做进一步的调整,以保证插入数据之后,数组中的数据之间仍然可以保持堆的性质。

3. 因此进行插入操作,我们这里默认建的是大堆(因此除了有扩容之外,在底部还有一个【向上调整算法】,我们在插入新的元素后始终要保持原先的堆是一个大堆,所以要去进行一个向上调整)。首先将新插入的节点放在完全二叉树最后的位置,再和父节点比较。如果new节点比父节点小,那么交换两者。交换之后,继续和新的父节点比较……直到new节点不比父节点小,或者new节点成为根节点,到此就插入操作就结束。

void HeapPush(HP* php, HPDataType x)
{
	assert(php);

	// 扩容
	if (php->size == php->capacity)
	{
		int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType)*newCapacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}

		php->a = tmp;
		php->capacity = newCapacity;
	}

	php->a[php->size] = x;
	php->size++;

	AdjustUp(php->a, php->size-1);
}

图解:
我们通过具体的例子来加深我们的理解:
在这里插入图片描述
解答:

先将30插入到数组的尾结点处(即最后一个孩子之处),插入之后如果我们可以发现30比16大,则说明堆的性质不满足,我们就要进行相应的调整,即沿着双亲结点往上进行进行调整,依次类推,即可得到最后的结果。

4.4.6堆的删除

跟之前学习的数据结构类似,有插入操作就有相应的删除操作!!!

删除操作只能删除根节点,即堆顶的元素,根节点删除后,我们会有两个子树,我们需要基于它们重构堆。但是这个删除过程可并不是直接删除堆顶的数据,原因如下:

我们若是直接删除堆顶的数据,那么原堆后面数据的父子关系就全部打乱了,需要全体重新建堆,时间复杂度为O ( N ) 。若是用上述方法,那么只需要对堆进行一次向下调整即可,因为此时根结点的左右子树都是小堆,我们只需要在根结点处进行一次向下调整即可,时间复杂度为O ( log ⁡ ( N ) )

思路:

首先交换数组第一个数据和最后一个数据的值,然后删除最后一个数据,这个过程中的操作就是让数组中有效数据个数减一即可。然后再进行向下调整。

void HeapPop(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);
}

图解:
在这里插入图片描述
解答:
上述堆的插入我们以大堆为例,在这里我们以小堆为例(就是希望大家两种都会,我们一种举一个例子,另一种对照),具体步奏:

1.开始时我们要删除的是为【10】的元素,因此我们第一步将10堆中的最后一个元素进行交换;
2.交换之后紧接着我们删除最后一个元素;
3.当最后一个元素删除之后我们在看此时的堆是否满足条件,这里我们就不难发现此时并不满足我们小根堆的条件(即双亲结点小于孩子结点)。而这里的【28】却是大于【15】和【19】,因此我们需要向下调整达到最后满足条件即止(这里留给大家练练手看是否学会了相关方法)。

4.4.7构建堆(及建堆时间复杂度)

对于堆的构建我们有两种办法进行操作:

1.在上面我们已经写过了插入函数和初始化函数的功能实现,因此第一种方法就是利用上述两种函数进行对堆的构建,具体如下:

// 堆的构建
void HeapCreate(HP* php, HPDataType* a, int n)
{
	assert(php);
	HeapInit(php);	
	for (int i = 0; i < n; ++i)
	{
		HeapPush(php, a[i]);
	}
}

注意:

这种方法存在一个很明显的弊端,就是每当我们插入一个元素我们就需要进行相关的调整已达到相应的要求,这就会带来极大的不便。而我们下一种方法就会克服这个弊端!!

2.首先我们为这个堆存放数据的地方单独开辟出一块空间,然后将数组中的内容拷贝过来,当把这些数据都拿过来之后,我们整体性地做一个向下调整即可。具体如下:

void HeapCreate(HP* php, HPDataType* a, int n)
{
	assert(php);
	php->a = (HPDataType*)malloc(php->a, sizeof(HPDataType)*n);
	if (php->a == NULL)
	{
		perror("realloc fail");
		exit(-1);
	}
	memcpy(php->a, a, sizeof(HPDataType)*n);
	php->size = php->capacity = n;

	// 建堆算法
	for (int i = (n-1-1)/2; i>=0; --i)
	{
		AdjustDown(a, n, i);
	}
}

有了上述的两种方法之后,那么我们就会想到底该怎么构建堆呢?我将通过具体的例子来大家说明该如何构建,具体过程以下图为例:
在这里插入图片描述

解答:

1.第一步根据我们给出的一个乱序的完全二叉树,首先找到完全二叉树中最后一个不是叶子节点中的节点,也就是图中的【21】的位置 (最后一个节点的位置 - 1) / 2.就是最后一个不为叶子节点的位置);
2.找到这个节点后向下查找(这里以小根堆为例),找到子节点中最小的节点,然后判断与自身的关系,若满足小根堆条件(parent < child),则继续,若不满足则交换节点的值,这里将【16】与 【21】交换;
3. 交换之后,若parent为叶子结点(panent->leftchild越界)则退出,否则继续向下查找,直到parent为叶子结点或子树满足小根堆.这里【21】已经是叶子节点,所以退出向下查找过程.之后找到倒数第二个不为叶子节点的节点,依上述步骤向下查找,直到将所有不为叶子结点的节点都向下查找一遍,这里继续将【3】向下查找,【3】的子树满足小根堆,退出向下查找;
4. 之后继续向前遍历,找到节点【12】向下查找,在子节点中找到【3】 和 【16】中较小的3,不满足小根堆的性质,交换【12】 和 【3】;
5. 交换完成之后【12】此处的节点依旧不是叶子结点,继续向下查找,找到叶子节点中较小的【7】,仍不满足小根堆的性质,继续交换;
6. 到最后【12】 成为叶子结点,退出向下查找,由于根节点已经向下查找完毕,所以堆的构建也就完成了.此时一个乱序的完全二叉树就已经变成了一个小根堆.

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果):
在这里插入图片描述

4.4.8取堆顶的数据

在之前我们也学过去首元素的操作,即堆顶的数据就是数组的首元素,因此直接返回堆顶的数据即可,代码如下:

HPDataType HeapTop(HP* php)
{
	assert(php);
	assert(php->size > 0);

	return php->a[0];
}

4.4.9堆的数据个数

学到这里大家就会发现,这里的许多操作跟我们之前写的顺序表啊等数据结构都有着类似的地方,原理都是一样的,在这里我就不多说了,直接看代码:

int HeapSize(HP* php)
{
	assert(php);

	return php->size;
}

4.4.10堆的判空

代码如下:

// 堆的判空
bool HeapEmpty(HP* php)
{
	assert(php);

	return php->size == 0;
}

5.堆的运用

5.1 堆排序

堆排序即利用堆的思想来进行排序,总共分为两个步骤:

  1. 建堆
    升序:建大堆
    降序:建小堆
  2. 利用堆删除思想来进行排序
    建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
    在这里插入图片描述
    解析:

1.此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整。【9下沉之后,9变成了叶子节点,因此不会对子叶产生影响】
2.找到第二个非叶节点4 【3/2 - 1 = 0】,由于[4,9,8]中【9】元素最大,【4】和【9】交换。【4下沉之后,变动了的子树必须重新调整】
3.这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换【4】和【6】
4.此时,我们就将一个无需序列构造成了一个大顶堆

最后简单总结下堆排序的基本思路:

a.将无需序列构建成一个堆,根据升序降序需求选择大堆根或小根堆;
b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。

代码实现如下:

// O(N*logN)
void HeapSort(int* a, int n)
{
	// 向上调整建堆 -- 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;
	}
}

总结:

堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n1),log2(n-2)…1]逐步递减,近似为nlogn。所以堆排序时间复杂度一般认为就是O(nlogn)。

5.2TOP-K问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

  1. 用数据集合中前K个元素来建堆
    前k个最大的元素,则建小堆
    前k个最小的元素,则建大堆
  2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
    将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素

代码如下:

void TestHeap5()
{
	// 造数据
	int n, k;
	printf("请输入n和k:>");
	scanf("%d%d", &n, &k);
	srand(time(0));
	FILE* fin = fopen("data.txt", "w");
	if (fin == NULL)
	{
		perror("fopen fail");
		return;
	}

	int randK = k;
	for (size_t i = 0; i < n; ++i)
	{
		int val = rand() % 100000;
		fprintf(fin, "%d\n", val);
	}

	fclose(fin);
	
	// 找topk
	FILE* fout = fopen("data.txt", "r");
	if (fout == NULL)
	{
		perror("fopen fail");
		return;
	}

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

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

	// 建小堆
	for (int i = (k - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(minHeap, k, i);
	}

	int val = 0;
	while (fscanf(fout, "%d", &val) != EOF)
	{
		if (val > minHeap[0])
		{
			minHeap[0] = val;
			AdjustDown(minHeap, k, 0);
		}
	}

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

	fclose(fout);
}

复杂度分析:

首先需要对K个元素进行建堆,时间复杂度为O(k),然后要遍历数组,最坏的情况是,每个元素都与堆顶比较并排序,需要堆化n次
所以是O(nlog(k)),因此总复杂度是O(k+nlog(k));

6.选择题讲解

1.已知小根堆为8,15,10,21,34,16,12,删除关键字 8 之后需重建堆,在此过程中,关键字之间的比较次数是()。
A.1 B.2 C.3 D.4

解答:
在这里插入图片描述

2.最小堆[0,3,2,5,7,4,6,8],在删除堆顶元素0之后,其结果是()
A[3,2,5,7,4,6,8]
B[2,3,5,7,4,6,8]
C[2,3,4,5,7,8,6]
D[2,3,4,5,6,7,8]

解答:

根据堆的删除规则,删除操作只能在堆顶进行,也就是删除0元素。
然后让最后一个节点放在堆顶,做向下调整工作,让剩下的数组依然满足最小堆。
删除0后用8填充0的位置,为[8,3,2,5,7,4,6]
然后8和其子节点3,2比较,结果2最小,将2和8交换,为:[2,3,8,5,7,4,6]
然后8的下标为2,其两个孩子节点下标分别为22+1=5,22+2=6
也就是4和6两个元素,经比较,4最小,将8与4交换,为[2,3,4,5,7,8,6]
这时候8已经没有孩子节点了,调整完成。

7.总结

本文开始时对二叉树的顺序存储进行了相关的介绍,紧接着就引出了“堆”的概念,把堆的基本概念讲解完之后我们就对两种基本的算法进行展开详解,学习完算法之后,我们就根据一定的算法去实现堆的基本功能以及堆的运用,最后通过简单的练习题加深我们对“堆”的了解!!!

以上就是全文的基本内容了,希望对大家有所帮助,觉得写的不错的话记得三连哟。

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

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

相关文章

消息中间件-RocketMQ入门 消息发送的三种方式

消息中间件-RocketMQ入门 消息发送的三种方式消息中间件简介应用场景常用消息中间件RocketMQ核心概念入门案例-生产者和消费者代码逻辑消息发送的三种方式同步发送异步发送一次性消息消息中间件简介 应用场景 假设现在有订单微服务和积分微服务,正常请求流程之后是不是一个订…

Java程序员:为了跳槽刷完1000道真题,想不到老板直接给我升职了

同事&#xff1a;前阵子听说你要跳槽&#xff0c;现在准备得怎么样啊&#xff1f; 程序员T&#xff1a;不跳了 同事&#xff1a;啊&#xff1f;为什么&#xff1f; 程序员T&#xff1a;涨薪了呗&#xff1f; 同事&#xff1a;真的吗&#xff1f;涨了多少&#xff1f;你自己…

DAMA数据管理知识体系指南之文档和内容管理

第10章 文档和内容管理 10.1 简介 文档和内容管理是对存储在关系数据库以外的信息的采集、存储、访问以及使用的控制活动。文档和内容管理的侧重点在完整性和访问控制上。因此&#xff0c;它与关系数据库的数据操作管理大致相同。由于多数非结构化数据与存储在结构化文件中的…

数据存储技术复习(四)未完

1.什么是NAS。一般用途服务器与NAS设备之间有何不同。NAS是一个基于IP的专用高性能文件共享和存储设备。—般用途服务器可用于托管任何应用程序&#xff0c;因为它运行的是一般用途操作系统NAS设备专用于文件服务。它具有专门的操作系统&#xff0c;专用于通过使用行业标准协议…

151、【动态规划】leetcode ——2. 01背包问题:二维数组+一维数组(C++版本)

题目描述 原题链接&#xff1a;2. 01背包问题 解题思路 &#xff08;1&#xff09;二维dp数组 动态规划五步曲&#xff1a; &#xff08;1&#xff09;dp[i][j]的含义&#xff1a; 容量为j时&#xff0c;从物品1-物品i中取物品&#xff0c;可达到的最大价值 &#xff08;2…

算法顶级比赛汇总

可参赛的算法比赛 阿里云天池大数据竞赛 时间&#xff1a;每年各个季度很多类型都会出题&#xff08;比赛总时间大概为两个月&#xff09; 内容&#xff1a;各个类型的算法题都会出、奖金上万不等 形式&#xff1a;在线提交&#xff08;提交后在线检查结果&#xff09;、离线…

简洁易懂:源码+实战讲解Redisson并发锁及看门狗自动续期

1 缘起 有一次同事问Redisson存储的键是否为hash&#xff1f; 我当时&#xff0c;没有看Redisson的相关源码&#xff0c;只知道应用&#xff0c; 所以没有办法回答&#xff0c;于是开始看看Redisson实现的源码&#xff0c; 顺便写了一个单机Redisson测试&#xff0c; 发现Redi…

leaflet 加载CSV数据,显示图形(代码示例046)

第046个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+leaflet中加载CSV文件,将图形显示在地图上。 直接复制下面的 vue+openlayers源代码,操作2分钟即可运行实现效果; 注意如果OpenStreetMap无法加载,请加载其他来练习 文章目录 示例效果配置方式示例源代码(共74…

海思3559:BT656调试笔记

前言 海思3559a的sdk例子是没有提供BT1120和BT656视频接入的&#xff0c;但实际上硬件是可以支持接入的。不过前提是只支持逐行方式输入&#xff0c;不支持隔行视频&#xff0c;如果想输入PAL制式的隔行视频&#xff0c;请先用芯片转成逐行再接入。不知道是官方手册有意无意的忽…

弄懂自定义 Hooks 不难,改变开发认知有点不习惯

前言 我之前总结逻辑重用的时候&#xff0c;就一直在思考一个问题。 对于逻辑复用&#xff0c;render props 和 高阶组件都可以实现&#xff0c;同样官方说 Hooks 也可以实现&#xff0c;且还是在不增加额外的组件的情况下。 但是我在项目代码中&#xff0c;没有找到自定义 …

python | 第二章考试题和练习题

一、考试题 1、turtle八边形绘制 问题描述&#xff1a; 使用turtle库&#xff0c;绘制一个八边形。 参考代码&#xff1a; import turtle as t t.pensize(2) for i in range(8):t.fd(100)t.left(45) 2、turtle八角图形绘制 问题描述&#xff1a; 使用turtle库&#xff0c;…

SaleSmartly(ss客服)带你了解:缩短B2B销售周期的秘诀

缩短B2B销售周期的秘诀&#xff1a;即时聊天 关键词&#xff1a;B2B 销售&#xff1b;即时沟通&#xff1b;SaleSmartly&#xff08;ss客服&#xff09; 在B2B销售中&#xff0c;时间就是一切。在某些情况下&#xff0c;买家正在积极寻找即时解决方案&#xff0c;潜在客户以多种…

【2023unity游戏制作-mango的冒险】-开始画面API制作

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 秩沅 原创 收录于专栏&#xff1a;游戏制作 ⭐mango的冒险-开始画面制作⭐ 文章目录⭐mango的冒险-开始画面制作⭐&#x1f468;‍&…

EasyCVR视频云存储的架构解析与Sharelist云存挂载方法介绍

一、什么是视频云存储&#xff1f; 视频云存储主要用于为上层应用提供视频文件、结构化信息、事件信息的相关服务。云存储节点分为数据文件存储节点和结构化数据存储节点。数据文件存储节点主要用于视频、图片的存储。结构化数据存储节点用于存储结构化数据并提供相关服务。 …

【学习记录】PCA主成分分析 SVD奇异值分解

在看MSC-VO代码的过程中&#xff0c;大量出现了奇异值分解的内容&#xff0c;本身对这部分了解不多&#xff0c;这里补一下课&#xff0c;参考b站up主小旭学长的视频&#xff0c;链接为&#xff1a;PCA主成分分析和SVD主成分分析 PCA主成分分析 PCA根本目的在于让数据在损失尽…

机器学习笔记之生成模型综述(三)生成模型的表示、推断、学习任务

机器学习笔记之生成模型综述——表示、推断、学习任务引言生成模型的表示任务从形状的角度观察生成模型的表示任务从概率分布的角度观察生成模型的表示任务生成模型的推断任务生成模型的学习任务引言 上一节介绍了从监督学习、无监督学习任务的角度介绍了经典模型。本节将从表…

概率论面试题1:玫瑰花

概率论面试题 1. 一个活动&#xff0c;n个女生手里拿着长短不一的玫瑰花&#xff0c;无序的排成一排&#xff0c;一个男生从头走到尾&#xff0c;试图拿更长的玫瑰花&#xff0c;一旦拿了一朵就不能再拿其他的&#xff0c;错过了就不能回头&#xff0c;问最好的策略&#xff1…

3年自动化测试这水平?我还不如去招应届生

公司前段缺人&#xff0c;也面了不少测试&#xff0c;结果竟然没有一个合适的。一开始瞄准的就是中级的水准&#xff0c;也没指望来大牛&#xff0c;提供的薪资在10-20k&#xff0c;面试的人很多&#xff0c;但平均水平很让人失望。看简历很多都是3年工作经验&#xff0c;但面试…

什么是响应性?

响应性&#xff1a; 这个术语在今天的各种编程讨论中经常出现&#xff0c;但人们说它的时候究竟是想表达什么意思呢&#xff1f;本质上&#xff0c;响应性是一种可以使我们声明式地处理变化的编程范式。一个经常被拿来当作典型例子的用例即是 Excel 表格&#xff1a; 这里单元…

angular相关知识点总结

创建 angualr 组件和传值 angular组件其实就是个xxx.component.ts,本质还是ts文件一个html文件 1.创建组件&#xff1a;在Angular中&#xff0c;可以使用命令行工具ng generate component创建一个新组件。例如&#xff1a; ng generate component my-component这将创建一个名…