【数据结构入门】堆(Heap)

2023-10-27

一、堆的结构及实现(重要)

1.1 二叉树的顺序结构

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


1.2 堆的概念及结构

堆(Heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵完全二叉树数组对象。堆总是满足下列性质:

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

堆是非线性数据结构,相当于一维数组,有两个直接后继。

大根堆和小根堆】:

根结点最大的堆叫做大根堆,树中所有父亲都大于或等于孩子。

根结点最小的堆叫做小根堆,树中所有父亲都小于或等于孩子。

image-20220211220635741

思考】这个大根堆和小根堆有什么特点呢?

最值总在 0 号位,根据这个特点我们就可以做很多事情,比如TopK问题 (在一堆数据里面找到前 K 个最大 / 最小的数),生活中也有很多实例,比如点餐软件中有上千家店铺,我想选出该地区好评最多的十家川菜店,我们不用对所有数据排序,只需要取出前 K 个最大 / 最小数据。使用堆排序效率也更高


1.3 堆的实现

1.3.1 堆的向下调整算法

下面给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:该节点的左右子树必须是一个 (大 / 小) 堆,才能调整

int array[] = { 27,15,19,18,28,34,65,49,25,37 }; // 根节点的左右子树都是小堆
image-20220211232010153

上面的数组,因为根节点的左右子树都是小堆,所以我们从根节点开始调整,将其调成小堆。

向下调整算法思路(调成小堆)

  • 从根节点开始,不断往下调。
  • 选出根节点的左右孩子中「最小的孩子」,与「父亲」进行比较。
  • (1) 如果父亲小于孩子,就不需处理了,整个树已经是小堆了。
  • (2) 如果父亲大于孩子,就跟父亲交换位置,并将原来小的孩子的位置当成父亲继续向下进行调整,直到调整到叶子结点为止。

向下调整算法过程演示(调成小堆,把大的节点往下调整)

image-20220211234628597

向下调整算法代码

// 向下调整算法,建小堆,把大的节点往下调整
// 前提是:左右子树都是小堆
void AdjustDown(int* a, int size, int parent)
{
	// 指向左孩子,默认左孩子最小
	int child = parent * 2 + 1;

	while (child < size)
	{
		// 1. 选出左右孩子最小的那个,先判断右孩子是否存在
		if (child + 1 < size && a[child] > a[child + 1])
		{
			child++; // 指向右孩子
		}

		// 2. 最小的孩子与父亲比较
		if (a[parent] > a[child]) // 如果父亲大于孩子
		{
			// 父亲与孩子交换位置
			Swap(&a[parent], &a[child]);

			// 更新父子下标,原先最小的孩子作为父亲,继续往下调
			parent = child;
			child = parent * 2 + 1;
		}
		else // 如果父亲小于孩子,说明已经为小堆了,停止调整
		{
			break;
		}
	}
}

1.3.2 向下调整算法的时间复杂度

我们以满二叉树计算,最坏情况下,向下调整算法最多进行满二叉树的高度减1次比较,则说明向下调整算法最多调整满二叉树的高度减1次,n 个节点的满二叉树高度为 log2(n+1),估算后所以时间复杂度为 O(log2n)


1.3.3 堆的创建(向下调整)

下面给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但不是一个堆,我们需要通过算法把它构建成一个堆。如果根节点左右子树不是一个 (大 / 小) 堆,我们应该怎么调整呢

我们倒着调整,从下到上,从「倒数第一个非叶子节点的子树」开始,依次遍历完所有非叶子节点,分别对每个子树进行「向下调整」成 (大 / 小) 堆,一直调整到「根节点」,就可以建成一个 (大 / 小) 堆

为什么要倒着调整呢?因为这样我们可以把「倒数第一个非叶子节点的子树」的左右子树看成是一个 (大 / 小) 堆,此时才能去使用向下调整算法。比如下图中的黄色填充的子树,3 的左子树 6 就可以看成是一个大堆。

实例】:将下面的数组建成一个大堆

int a[] = { 1,5,3,8,7,6 };

image-20220212171941844

建堆过程演示(以建大堆为例):从下到上,依次遍历完所有非叶子节点,分别对每个子树进行向下调整。

依次对 每一步 中,方框内的树 进行 向下调整 为一个 大堆

image-20220212223246567

建堆代码

// 交换函数
void Swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

// 向下调整算法,建大堆,把小的节点往下调
// 前提是:左右子树都是大堆
void AdjustDown(int* a, int size, int parent)
{
	// 指向左孩子,默认左孩子最大
	int child = parent * 2 + 1;

	while (child < size)
	{
		// 1. 选出左右孩子最大的那个,先判断右孩子是否存在
		if (child + 1 < size && a[child] < a[child + 1])
		{
			child++; // 指向右孩子
		}

		// 2. 最大的孩子与父亲比较
		if (a[parent] < a[child]) // 如果父亲小于孩子
		{
			// 父亲与孩子交换位置
			Swap(&a[parent], &a[child]);

			// 更新父子下标,原先最大的孩子作为父亲,继续往下调
			parent = child;
			child = parent * 2 + 1;
		}
		else // 如果父亲大于孩子,说明已经为大堆了,停止调整
		{
			break;
		}
	}
}

void HeapSort(int* a, int size)
{
    /* 建堆(大堆)
    * 倒着调整,从倒数第一个非叶子节点的子树进行向下调整,直到调整到根节点的树
    */
	int parent = ((size - 1) - 1) / 2; // 最后一个叶子节点的父亲的下标
	for (int i = parent; i >= 0; i--)  // 从下到上,依次遍历完所有子树,分别对其进行调整
	{
		AdjustDown(a, size, i);
	}
    /* 堆排序
    * 排升序 --> 建大堆,每次选出一个最大的数放到最后
    * 排降序 --> 建小堆,每次选出一个最小的数放到最后
    */
    // 下面是排升序:
	int end = size - 1; // 记录堆中最后一个元素的下标
	while (end > 0)
	{
		Swap(&a[0], &a[end]);  // 将堆顶元素和堆中最后一个元素交换,把最大的数(堆顶)放到最后
		AdjustDown(a, end, 0); // 不看最后一个数,从根节点开始,对前面的数进行向下调整成大堆
		end--;
	}
}

1.3.4 堆排序

排升序 --> 建大堆:

  1. 思考】排升序,建小堆可以吗?-- 可以是可以,但没啥意思。
    首先对 n 个数建小堆,选出最小的数,接着对剩下的 n-1 个数建小堆,选出第2小的数,不断重复上述过程……。建 n 个数的堆时间复杂度是O(N),所以上述操作时间复杂度为O(N2),效率太低,尤其是当数据量大的时候,效率更低,同时堆的价值没有被体现出来,还不如用直接排序。

  2. 最佳方法排升序,因为数字越来越大,需要找到最大的数字,得建大堆

    1. 首先对 n 个数建大堆。
    2. 将最大的数(堆顶)和最后一个数交换,把最大的数放到最后
    3. 前面 n-1 个数的堆结构没有被破坏(最后一个数不看做堆里面的),根节点的左右子树依旧是大堆,所以我们进行一次向下调整成大堆即可选出第2大的数,放到倒数第二个位置,然后重复上述步骤……。
  3. 时间复杂度】:建堆时间复杂度为O(N),向下调整时间复杂度为O(log2N),这里我们最多进行N-2次向下调整,所以堆排序时间复杂度为O(N*log2N),效率是很高的

image-20220214212436297

排降序 --> 建小堆:

最佳方法排降序,因为数字越来越小,需要找到最小的数字,得建小堆

  1. 首先对 n 个数建小堆。
  2. 将最小的数(堆顶)和最后一个数交换,把最小的数放到最后
  3. 前面 n-1 个数的堆结构没有被破坏(最后一个数不看做堆里面的),根节点的左右子树依旧是小堆,所以我们进行一次向下调整成小堆即可选出第2小的数,放到倒数第二个位置,然后重复上述步骤……。
  4. 时间复杂度】:建堆时间复杂度为O(N),向下调整时间复杂度为O(log2N),这里我们最多进行N-2次向下调整,所以堆排序时间复杂度为O(N*log2N),效率是很高的

1.3.5 建堆的时间复杂度

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明,计算起来比较好算(时间复杂度本来看的就是近似值,多几个节点不影响最终结果):

建堆要从倒数第一个非叶子节点开始调整,也即是从倒数第二层开始调,可得出时间复杂度公式:
T ( n ) = ∑ ( 每 层 节 点 数 ∗ ( 堆 的 高 度 − 当 前 层 数 ) ) T(n) = ∑ (每层节点数*(堆的高度-当前层数)) T(n)=(())

image-20220214175436266

所以,建堆的时间复杂度为O(N)


上面学了那么多,这里小小总结一下

  1. 堆的向下调整算法就是,在该节点左右子树都是一个小/大堆的前提下,将以该节点为根的树调整成一个小/大堆。
  2. 堆的创建就是倒着调整,从下到上,从倒数第一个非叶子节点的子树开始,依次遍历完所有子树,分别对其进行向下调整。
  3. 时间复杂度:堆的向下调整算法为O(log2N),堆的创建为O(N)。

二、堆的相关接口实现(以大堆为例)

首先新建一个工程( 博主使用的是 VS2019 )

  • Heap.h(堆的类型定义、接口函数声明、引用的头文件)
  • Heap.c(堆接口函数的实现)
  • Test.c(主函数、测试堆各个接口功能)

Heap.h 头文件代码如下:

#pragma once

#include<stdio.h>   // printf, perror
#include<stdbool.h> // bool
#include<assert.h>  // assert
#include<stdlib.h>  // malloc, free
#include<string.h>  // memcpy

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a; // 指向动态开辟的数组
	int size;      // 数组中有效元素个数
	int capacity;  // d容量
}Heap;

// 交换函数
void Swap(HPDataType* a, HPDataType* b);
// 向下调整函数(调成大堆,把小的往下调)
void AdjustDown(HPDataType* a, int size, int parent);
// 向上调整函数(调成大堆,把大的往上调)
void AdjustUp(HPDataType* a, int child);

// 初始化堆
void HeapInit(Heap* php, HPDataType* arr, int n);
// 销毁堆
void HeapDestroy(Heap* php);
// 插入元素(插入到堆的末尾),插入后并保持它依然是堆
void HeapPush(Heap* php, int x);
// 删除堆顶元素,删除后保持它依然是堆
void HeapPop(Heap* php);
// 获取堆顶元素,也即是最值
HPDataType HeapTop(Heap* php);
// 判断堆是否为空,为空返回true,不为空返回false
bool HeapEmpty(Heap* php);
// 获取堆中有效元素个数
int HeapSize(Heap* php);
// 打印堆
void HeapPrint(Heap* php);

2.1 堆的初始化

堆的初始化,首先需要实现一个向下调整算法

// 交换函数
void Swap(HPDataType* a, HPDataType* b)
{
	HPDataType tmp;
	tmp = *a;
	*a = *b;
	*b = tmp;
}

// 向下调整算法(调成大堆,把小的往下调)
void AdjustDown(HPDataType* a, int size, int parent)
{
	// 左孩子下标,初始默认左孩子最大
	int child = parent * 2 + 1;
	while (child < size)
	{
		// 选出左右孩子最大的那个,先判断右孩子是否存在
		if (child + 1 < size && a[child] < a[child + 1])
		{
			child++; // 右孩子最大
		}
		// 最大的孩子与父亲比较
		if (a[parent] < a[child]) // 如果父亲小于孩子
		{
			// 父亲与孩子交换位置
			Swap(&a[parent], &a[child]);

			// 更新父子下标,原先最大的孩子作为父亲,继续往下调
			parent = child;
			child = parent * 2 + 1;
		}
		else // 如果父亲大于孩子,说明已经为大堆了,停止调整
		{
			break;
		}
	}
}

堆的初始化代码

// 初始化堆,用一个给定的数组来初始化
void HeapInit(Heap* php, HPDataType* arr, int n)
{
	assert(php); // 断言
	
	// 动态开辟n个空间
	php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
	if (php->a == NULL)
	{
		perror("malloc");
		exit(-1);
	}

	// 把给定数组的各元素值拷贝过去
	memcpy(php->a, arr, sizeof(HPDataType) * n);
	php->size = php->capacity = n;

	// 建堆(建大堆)
	int parent = ((php->size - 1) - 1) / 2; // 倒数第一个非叶子节点下标
	for (int i = parent; i >= 0; i--) // 从下到上,依次遍历完所有子树,分别对其进行调整
	{
		AdjustDown(php->a, php->size, i);
	}
}

2.2 堆的销毁

// 销毁堆
void HeapDestroy(Heap* php)
{
	assert(php);

	free(php->a); // 释放动态开辟的空间
	php->a = NULL;
	php->size = php->capacity = 0;
}

2.3 堆的插入

先插入一个新元素到数组的尾上,从插入的新元素开始,进行向上调整算法,直到满足(大/小)堆。

堆的插入过程演示

image-20220215215128171

堆的插入,首先需要实现一个向上调整算法

// 向上调整算法(调成大堆,把大的往上调)
void AdjustUp(HPDataType* a, int child)
{
	// 父节点的下标
	int parent = (child - 1) / 2;

	//while (parent >= 0) parent不会小于0
	while (child > 0)
	{
		// 孩子与父亲进行比较
		if (a[child] > a[parent]) // 如果孩子大于父亲
		{
			// 孩子与父亲交换
			Swap(&a[child], &a[parent]);

			// 更新父子下标,原先父亲作为孩子,继续往上调
			child = parent;
			parent = (child - 1) / 2;
		}
		else // 如果孩子小于父亲,说明已经为大堆了,停止调整
		{
			break;
		}
	}
}

堆的插入代码

// 插入元素(插入到堆的末尾),插入后并保持它依然是堆
void HeapPush(Heap* php, int x)
{
	assert(php);

	// 先检查空间是否已满
	if (php->capacity == php->size)
	{
		// 增容两倍
		php->capacity *= 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * php->capacity);
		if (tmp != NULL)
		{
			php->a = tmp;
			tmp = NULL;
		}
	}

	// 插入元素
	php->a[php->size] = x;
	php->size++;

	// 从插入的元素开始,进行向上调整,保持它依然是堆
	AdjustUp(php->a, php->size - 1);
}

2.4 堆的删除

  1. 将堆顶元素和最后一个元素交换(这样就变成尾删了,很方便)
  2. 删除堆中最后一个元素
  3. 从根节点开始,对剩下元素进行向下调整,调成(大/小)堆

堆的删除过程演示

image-20220215215951562

堆的插入,首先需要实现一个向下调整算法:前面已经实现过了,这里就不展示了。

堆的删除代码

// 删除堆顶元素,删除后保持它依然是堆
void HeapPop(Heap* php)
{
	assert(php);
	assert(!HeapEmpty(php)); // 堆不能为空

	// 将堆顶元素和最后一个元素交换
	Swap(&php->a[0], &php->a[php->size - 1]);

	// 删除堆中最后一个元素
	php->size--;

	// 从根节点开始,对剩下元素进行向下调整成大堆,保持它依然是堆
	AdjustDown(php->a, php->size, 0);
}

2.5 获取堆顶元素

// 获取堆顶元素,也即是最值
HPDataType HeapTop(Heap* php)
{
	assert(php);
	assert(!HeapEmpty(php)); // 堆不能为空

	return php->a[0];
}

2.6 堆的判空

// 判断堆是否为空,为空返回true,不为空返回false
bool HeapEmpty(Heap* php)
{
	assert(php);

	return php->size == 0;
}

2.7 拓展1:找出堆中前k个最大元素

堆的相关接口实现好了,因为是大堆,所以我们可以很方便的来找出堆中前 k 个最大元素。

这里要和前面的堆排序区分开哦,这里我们并不是在堆中对所有元素排好序。

void TestHeap()
{
	int a[] = { 1,5,3,8,7,6 };

	Heap hp;
	HeapInit(&hp, a, sizeof(a) / sizeof(a[0])); // 初始化堆

	int k = 0;
	scanf("%d", &k);

	printf("找出堆中前%d个最大元素:\n", k);
	while (!HeapEmpty(&hp) && k--)
	{
		printf("%d ", HeapTop(&hp)); // 获取堆顶元素
		HeapPop(&hp); // 删除堆顶元素
	}
	printf("\n");
}

运行结果:

image-20220215224119599

2.8 拓展2:堆的创建(向上调整)

下面给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但不是一个堆,我们需要通过「向上调整算法」把它构建成一个堆。如果根节点左右子树不是一个 (大 / 小) 堆,我们应该怎么调整呢

我们从上到下,从「第一个节点(也就是根节点)的左孩子」开始,依次遍历完所有节点,分别对每个节点进行「向上调整」,一直到「最后一个节点」,就可以建成一个 (大 / 小) 堆

我们把数组中的「第一个元素」看作是一个「堆」,剩余的元素依次插入到这个「堆」中。前面我们也实现了堆的插入接口,原理就是向上调整。

image-20220217173916889
// 向上调整算法建堆
void CreateHeap(int* a, int size)
{
	// 把第一个元素看作是堆,剩余的元素依次插入堆中
	for (int i = 1; i < size; i++)
	{
		AdjustUp(a, i);
	}
}

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【数据结构入门】堆(Heap) 的相关文章

随机推荐

  • Qt Remote Objects 简介

    QtRO是Qt RemoteObjects的简称 是从5 9开始Qt官方推出的一个用于进程间通信 IPC 的新模块 虽然该模块目前仍处于TP阶段 但已经足够稳定 由于项目开发需要 我们将一个大项目划分成了若干个子工程 各个子工程都是独立的程
  • Vue自定义指令给box添加水印

    1 创建directive js 并引入 src utils下 import Vue from vue Vue directive watermark el binding gt function addWaterMarker str pa
  • C# 接收邮件

    使用pop协议实现接收邮件 转载 C 接收邮件之QQ邮箱 c qq邮箱收邮件 Csharp 小记的博客 CSDN博客 using MailKit using MailKit Net Imap using MailKit Search usi
  • VMware Centos7下载安装教程(超详细)

    作者主页 士别三日wyx 作者简介 CSDN top100 阿里云博客专家 华为云享专家 网络安全领域优质创作者 Centos7下载安装 一 下载镜像 二 创建虚拟机 三 安装Centos 7 四 配置网络 一 下载镜像 点击进入 清华大学
  • Altium Designer:地PAD和铺铜不连接问题

    一个非典型的问题解决过程 1 发现问题 检查PCB时发现 TOP层有些GND管脚和via过孔 没有和铺地铜皮连到一起 但是有些GND pin却是正常连接铜皮的 2 网络搜索解决办法 按照网络上的办法 检查铺铜网络和管脚PIN的网络 都是GN
  • Mac OS 终端利器 iTerm2

    之前一直使用 Mac OS 自带的终端 用起来虽然有些不太方便 但总体来说还是可以接受的 是有想换个终端的想法 然后今天偶然看到一个终端利器 iTerm2 发现真的很强大 也非常的好用 按照网上配置了主题什么的 还是有些坑的 这边再记录下
  • LeetCode-Python-377. 组合总和 Ⅳ

    给定一个由正整数组成且不存在重复数字的数组 找出和为给定目标正整数的组合的个数 示例 nums 1 2 3 target 4 所有可能的组合为 1 1 1 1 1 1 2 1 2 1 1 3 2 1 1 2 2 3 1 请注意 顺序不同的序
  • WPF拖动事件

    1 定义一个Dragthumb模板
  • QT 文件操作 QFile

    目录 QFile类介绍 写入数据到txt文件 实例代码 从txt文件中读取所有数据 实例代码 从txt文件中一行一行读取数据 实例代码 部分函数参数及作用 QFile类介绍 QIODevice 类是 Qt 中所有 I O 设备的基础接口类
  • 欧拉角万向节死锁

    https malei0311 gitbooks io threejs doc cn content getstart euler angle gimbal lock html 欧拉角万向节死锁 什么是欧拉角 Eular Angles 欧拉
  • ctf练习题

    攻防世界 WEB Challenge area Web php include WEB Challenge area upload1 WEB Challenge area ics 06 WEB Challenge area baby web
  • 保险项目测试,保险分类

    项目紧任务重时间少的996下 我还是需要抽个空来整理一下子 理一理自己的逻辑 保险项目 保险项目中关键词 保险主要分为社保和商保 社保 商保 商保细分 测试保险项目 保单生命周期 项目测试流程 什么是短期健康险 实际项目中具体负责了哪些测试
  • 探索【Stable-Diffusion WEBUI】的插件:ControlNet 1.1

    文章目录 零 前言 一 ControlNet 二 ControlNet v1 1 2 1 模型 2 2 新版界面 2 3 预处理器 三 偷懒 零 前言 本篇主要提到ControlNet新版本的使用 和旧版本的变化 并偷懒参考了别人很不错的文
  • 基于web的图片资源库管理系统的设计与实现

    技术 Java JSP等 摘要 本系统是一种基于B S架构的图片资源管理系统 它采用目前最流行的Java语言编写 用到了当今先进的技术如 JSP技术 Hibernate Spring Struts框架等来实现该系统 系统分为五大模块 图片夹
  • sqli-lab-less8

    sqli lab less8 一 靶标地址 Less 8 GET Blind Boolian Based Single Quotes 单引号布尔盲注 http 127 0 0 1 sqli less 8 二 漏洞探测 由于探测的fuzz参数
  • 【SLAM】A-LOAM 算法部署与测试(Win10 + VMWare + Ubuntu18.04)

    基础环境 ubuntu及ROS安装 略 安装完ROS以后 默认已经安装好了PCL和Eigen库 安装Ceres 下载Ceres源文件 Vmware没有网络 到下面的网址手动下载安装包 https github com ceres solve
  • 【解决】使用uniapp做App,首页需要显示实时时间及日期,一秒跳动一下

    需求 App首页放置一个实时时间 效果图如下 每秒跳一下 解决 在onShow 里面使用定时器获取当前时间 在onHide 里面进行定时器清除 示例代码如下
  • Source Insight (SI) 变量、函数、宏定义变成黑色,无法快速查看调用的几种解决方法

    Source Insight 变量 函数 宏定义变成黑色 无法快速查看调用的几种解决方法 方法一 同步SI与本地的代码 方法二 重构SI工程 其他解决方法 在source insight中 一般即使鼠标点在函数或者变量处 context w
  • Puppeteer介绍

    前面已经介绍了Cypress框架 为什么还要介绍puppeteer呢 因为puppeteer支持的一些功能cypress不支持 例如多个tab页窗口切换的场景 同一个测试场景中访问不同域页面等 另外 puppeteer有google大厂支持
  • 【数据结构入门】堆(Heap)

    文章目录 一 堆的结构及实现 重要 1 1 二叉树的顺序结构 1 2 堆的概念及结构 1 3 堆的实现 1 3 1 堆的向下调整算法 1 3 2 向下调整算法的时间复杂度 1 3 3 堆的创建 向下调整 1 3 4 堆排序 1 3 5 建堆