服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - C/C++ - C语言详解如何实现堆及堆的结构与接口

C语言详解如何实现堆及堆的结构与接口

2022-11-15 15:23CodeWinter C/C++

堆是计算机科学中一类特殊的数据结构的统称,通常是一个可以被看做一棵完全二叉树的数组对象。而堆排序是利用堆这种数据结构所设计的一种排序算法。本文将详细介绍堆的结构与接口,需要的可以参考一下

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

1.1 二叉树的顺序结构

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

1.2 堆的概念及结构

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

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

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

【大根堆和小根堆】:

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

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

C语言详解如何实现堆及堆的结构与接口

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

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

1.3 堆的实现

1.3.1 堆的向下调整算法

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

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

C语言详解如何实现堆及堆的结构与接口

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

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

从根节点开始,不断往下调。

选出根节点的左右孩子中「最小的孩子」,与「父亲」进行比较。

  • 如果父亲小于孩子,就不需处理了,整个树已经是小堆了。
  • 如果父亲大于孩子,就跟父亲交换位置,并将原来小的孩子的位置当成父亲继续向下进行调整,直到调整到叶子结点为止。

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

C语言详解如何实现堆及堆的结构与接口

向下调整算法代码:

// 向下调整算法,建小堆,把大的节点往下调整
// 前提是:左右子树都是小堆
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 };

C语言详解如何实现堆及堆的结构与接口

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

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

C语言详解如何实现堆及堆的结构与接口

建堆代码:

// 交换函数
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 堆排序

排升序 --> 建大堆:

【思考】排升序,建小堆可以吗?-- 可以是可以,但没啥意思。

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

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

  • 首先对 n 个数建大堆。
  • 将最大的数(堆顶)和最后一个数交换,把最大的数放到最后。
  • 前面 n-1 个数的堆结构没有被破坏(最后一个数不看做堆里面的),根节点的左右子树依旧是大堆,所以我们进行一次向下调整成大堆即可选出第2大的数,放到倒数第二个位置,然后重复上述步骤……。

【时间复杂度】:建堆时间复杂度为O(N),向下调整时间复杂度为O(log2N),这里我们最多进行N-2次向下调整,所以堆排序时间复杂度为O(N*log2N),效率是很高的。

C语言详解如何实现堆及堆的结构与接口

排降序 --> 建小堆:

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

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

1.3.5 建堆的时间复杂度

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

建堆要从倒数第一个非叶子节点开始调整,也即是从倒数第二层开始调,可得出时间复杂度公式:

T ( n ) = ∑ ( 每 层 节 点 数 ∗ ( 堆 的 高 度 − 当 前 层 数 ) )

C语言详解如何实现堆及堆的结构与接口

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

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

  • 堆的向下调整算法就是,在该节点左右子树都是一个小/大堆的前提下,将以该节点为根的树调整成一个小/大堆。
  • 堆的创建就是倒着调整,从下到上,从倒数第一个非叶子节点的子树开始,依次遍历完所有子树,分别对其进行向下调整。
  • 时间复杂度:堆的向下调整算法为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 堆的插入

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

堆的插入过程演示:

C语言详解如何实现堆及堆的结构与接口

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

// 向上调整算法(调成大堆,把大的往上调)
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 堆的删除

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

堆的删除过程演示:

C语言详解如何实现堆及堆的结构与接口

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

堆的删除代码:

// 删除堆顶元素,删除后保持它依然是堆
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 找出堆中前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");
}

运行结果:

C语言详解如何实现堆及堆的结构与接口

2.8 堆的创建(向上调整)

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

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

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

C语言详解如何实现堆及堆的结构与接口

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

到此这篇关于C语言详解如何实现堆的结构与接口的文章就介绍到这了,更多相关C语言堆的结构与接口内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://blog.csdn.net/weixin_48025315/article/details/123165836

延伸 · 阅读

精彩推荐
  • C/C++C语言实现学生宿舍管理系统

    C语言实现学生宿舍管理系统

    这篇文章主要为大家详细介绍了C语言实现学生宿舍管理系统,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    dream_it_10262022-10-21
  • C/C++C++设计模式之中介者模式

    C++设计模式之中介者模式

    这篇文章主要介绍了C++设计模式之中介者模式,本文讲解了什么是中介者模式、中介者模式的使用场合、中介者模式的优缺点等内容,需要的朋友可以参考下...

    果冻想8902021-02-05
  • C/C++C语言超详细讲解文件的操作

    C语言超详细讲解文件的操作

    C语言文件操作的方法有很多,函数也有很多你知道哪些呢?下面是小编为大家带来的C语言文件操作的方法,欢迎阅读...

    风&64610562022-11-15
  • C/C++C++如何获取当前系统时间及格式化输出

    C++如何获取当前系统时间及格式化输出

    这篇文章主要介绍了C++如何获取当前系统时间及格式化输出的实例代码,主要用到time()及strftime()函数,通过实例代码给大家介绍的非常详细,具有一定的参...

    小朋友学开发9332021-08-19
  • C/C++C++中各种初始化方式示例详解

    C++中各种初始化方式示例详解

    这篇文章主要给大家介绍了关于C++中各种初始化方式的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用C++具有一定的参考学习价值,需...

    linuxidc10162021-06-06
  • C/C++C语言实现顺序表基本操作汇总

    C语言实现顺序表基本操作汇总

    这篇文章主要介绍了C语言实现顺序表基本操作汇总,对学习数据结构的朋友有一定的借鉴价值,需要的朋友可以参考下...

    C语言程序设计11622021-01-23
  • C/C++C++实现学生管理系统

    C++实现学生管理系统

    这篇文章主要为大家详细介绍了C++实现学生管理系统,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    一个鸹貔9092021-09-17
  • C/C++cmake 学习笔记

    cmake 学习笔记

    这篇文章主要介绍了作者学习cmake 的相关资料与心得,有需要的小伙伴可以参考下...

    thinkpp4522021-05-24