首页 > 其他分享 >【数据结构】详解堆

【数据结构】详解堆

时间:2024-07-20 14:55:21浏览次数:12  
标签:int hp HPDataType 详解 Heap 数据结构 void size

一、堆的概念

堆(Heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵 完全二叉树的 数组对象。 堆是非线性数据结构,相当于一维数组,有两个直接后继。
如果有一个关键码的集合K = { k₀,k₁,k₂ ,k₃ ,…,kₙ₋₁  },把它的所有元素按完全二叉树的顺序存储方式存储,在一个一维数组中,并满足:Kᵢ  <= K₂ *ᵢ₊₁  且 Kᵢ  <= K₂ *ᵢ₊₂  (Kᵢ  >= K₂ *ᵢ₊ ₁ 且 Kᵢ  >= K₂ *ᵢ₊₂ ) i = 0,1,2…,则称为小堆 (或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

【大根堆和小根堆】:

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

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

共同特点:父亲 =(孩子-1)/2

大堆小堆有什么特点呢?

我们购物平台中,我想选择销量大的前k家,这个时候,我们不需要对所有的数据进行排序,只需要取出前k家最大的值就可以。而最值常常出现在0号位,我们就可以利用Topheap解决,大大减少了我们的时间复杂度;

特点:

  • 堆中某个节点的值总是不大于或不小于其父节点的值;
  • 堆总是一棵完全二叉树
  • 每层节点个数为2^(h-1)个

二、堆的创建

1、头文件的声明:

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

void HeapInit(Heap* hp);
// 堆的销毁
void HeapDestory(Heap* hp);

void swap(HPDataType* p1, HPDataType* p2);

void AdjustUp(HPDataType* a, int child);
void AdjustDown(HPDataType* a, int n, int parent);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);

2、代码实现:

2.1堆的初始化与堆的摧毁

//堆的初始化
void HeapInit(Heap* hp) {
	assert(hp);
	hp->a = NULL;
	hp->capacity = hp->size = 0;
}

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

2.2堆的插入

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

具体步骤如下:

1.将新插入的元素放置在堆的最后一个位置(通常是数组的末尾)。

2.将该元素与其父节点进行比较。

3.若该元素大于(或小于,具体根据堆是最大堆还是最小堆而定)其父节点的值,则交换该元素和其父节点的位置。 

4.继续向上对比和交换,直到满足堆的性质或达到堆的根节点。

// 堆的插入
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, newcapacity * sizeof(HPDataType));
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}

		hp->a = tmp;
		hp->capacity = newcapacity;
	}
	hp->size++;
	hp->a[hp->size] = x;
//向上调整法,因为每次的插入是在数组末尾
//每次插入需要与父亲比较大小交换
	AdjustUp(hp->a, hp->size - 1);
}
2.2.1向上调整法 

//向上调整法,因为每次的插入是在数组末尾
//每次插入需要与父亲比较大小交换
    AdjustUp(hp->a, hp->size - 1);

我们每次插入末尾的位置,相当于孩子,我们需要找到该孩子的父亲与之比较大小,这个时候就要利用堆的特点:父亲 =(孩子-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;
		}
	}
}

 交换函数:

因为在堆的实现中我们会经常使用父子交换

void swap(HPDataType* p1, HPDataType* p2) {
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
    //这里也可以回想前面学习
    //不使用第三个变量,利用换位与实现交换
}

2.4堆的删除

如果我们直接删除堆顶数据,将数组数据整体向前移动,这样会导致堆的乱序;

删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法

2.4.1向下调整法

void AdjustDown(HPDataType* a, int n, int parent) {
    //注:这里的parent为0,而数组a则是首尾交换的
	int child = parent * 2 + 1;
	//假设左孩子小

	while (child < n) {
        //while循环直到超出数组长度
		//左孩子大
		if (a[child] > a[child + 1]) {
			child++;
		}
		if (a[child]>a[parent]) {
			swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
	}
}

这样我们只需要完成交换,传指就可以了

void HeapPop(Heap* hp) {
	assert(hp);
	assert(hp->size);
	swap(&hp->a[0], &hp->a[hp->size - 1]);
	hp->size--;

	AdjustDown(hp->a, hp->size, 0);
}

3、总结

升序:建大堆

降序:建小堆

(1)升序:建大堆

【思考】排升序,建小堆可以吗?-- 可以(但不推荐)。

首先对 n 个数建小堆,选出最小的数,接着对剩下的 n-1 个数建小堆,选出第二小的数,不断重复上述过程。

【时间复杂度】建 n 个数的堆时间复杂度是 O(N),所以上述操作时间复杂度为 O(N²),效率太低,关系变得杂乱,尤其是当数据量大的时候,效率就更低。同时堆的价值也没有被体现出来,这样不如用直接排序。

排升序,因为数字依次递增,需要找到最大的数字,得建大堆。

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

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

因为在堆的实现中我们会经常使用父子交换

void swap(HPDataType* p1, HPDataType* p2) {
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
    //这里也可以回想前面学习
    //不使用第三个变量,利用换位与实现交换
}

我相信接下来对你来说简直轻而易举

// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);

4.1取堆顶数据

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

4.2 堆的数据个数

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

4.3堆的判空

int HeapEmpty(Heap* hp) {
	assert(hp);
	assert(hp->size>0);
	//为NULL,返回1,不为NULL,返回0;
	return hp->size == 0 ? 1 : 0;
}

5、堆的时间复杂度

5.1建堆的时间复杂度

5.1.1向下调整法建堆
// 堆的插入
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, newcapacity * sizeof(HPDataType));
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
 
		hp->a = tmp;
		hp->capacity = newcapacity;
	}
	hp->size++;
	hp->a[hp->size] = x;
//向下调整法,因为每次的插入是在数组末尾
//每次插入需要与父亲比较大小交换
	AdjustUp(hp->a, hp->size - 1);
}

  因此:建堆的时间复杂度为O(N)

等比数列求和公式: 
建堆要从倒数第一个非叶子节点开始调整,也即是从倒数第二层开始调,可得出时间复杂度公式:T ( n ) = ∑ ( 每 层 节 点 数 ∗ ( 堆 的 高 度 − 当 前 层 数 ) ) 
建堆的时间复杂度为O(N)。(向下调整算法)

为何使用向下调整建堆而不使用向上调整建堆

5.1.2向上调整法建堆

可以看出结点数多的层, 调整次数也多, 结点数少的层, 调整次数少, 时间复杂度为O(N*logN), 所以一般建堆都采用向下调整建堆法. 

6、TOPK问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大

比如:专业前十、世界500强、销量最高的前十、富豪榜等等

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能

数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

1. 用数据集合中前K个元素来建堆

前k个最大的元素,则建小堆

前k个最小的元素,则建大堆

2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素

将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

 我们常常会想到冒泡排序( O(N^2) )
对于少量的数据是可以拿捏的,但面对100000的数据就显得有点吃力,而堆排序O(N*logN)则觉得轻而易举

 

7、堆排序  

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

哇呜!点个赞走吧!!!

标签:int,hp,HPDataType,详解,Heap,数据结构,void,size
From: https://blog.csdn.net/Asuku_/article/details/140475626

相关文章

  • 【数据结构】超详解二叉树
    1、树的概念及结构堆与树的结构类似堆的概念及代码实现-CSDN博客树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。有一个特殊的结点,称为根结点,根结点没有前驱结点除......
  • 数据结构-队列、栈
    功能受限的表结构一、栈和队列介绍栈和队列是两种重要的线性结构,从数据结构角度,他们都是线性表,特殊点在于它们的操作被限制,也就是所谓的功能受限,统称功能受限的线性表从数据类型角度,它们也可以是看成处理、管理数据的一种规则二、栈结构栈(stack)是限定在表尾进行数据的插......
  • C语言指针详解(进阶)
    二、指针的进阶    本章重点:            1.字符指针        2.数组指针        3.指针数组        4.数组传参和指针传参        5.函数指针        6.函数指针数组    ......
  • 【数据结构】ArrayList与顺序表
    目录1.前言2.顺序表2.1定义一个IList接口2.2实现接口功能2.3自定义异常类2.4主程序代码3.ArrayList3.1ArrayList简介3.2ArrayList的构造3.3ArrayList常见操作3.4ArrayList的遍历 4.ArrayList的具体使用4.1简单的洗牌算法4.2杨辉三角  5.总结1.前言通过上......
  • 《数据结构》--顺序表
    C语言语法基础到数据结构与算法,前面已经掌握并具备了扎实的C语言基础,为什么要学习数据结构课程?--我们学完本章就可以实践一个:通讯录项目简单了解过后,通讯录具备增加、删除、修改、查找联系人等操作。要想实现通讯录项目必须有两个技术关键:(1)C语言语法基础(2)数据结构之顺序表/......
  • 数据结构(00)
    1.序:`数据结构`将与`菜鸟的Leetcode之路`不定时更新,也是一个系列的内容,将会包含许多的数据的逻辑结构,物理结构,数据运算。(具体怎么说,我也不太明白,我的理解是:对于不同类型数据,进行不同的排序和存储,通过指针和数组,方便后续算法对其`增加,删除,修改,查询`。)呃,正如英雄哥所言:数据结构......
  • 山东大学数据结构与算法实验8散列表(线性开型寻址/链表散列)
    A : 线性开型寻址题目描述要求使用线性开型寻址实现描述给定散列函数的除数D和操作数m,输出每次操作后的状态。有以下三种操作:插入x,若散列表已存在x,输出“Existed”,否则插入x到散列表中,输出所在的下标。查询x,若散列表不含有x,输出“-1”,否则输出x对应下标。......
  • 数据结构-线性表、链表
    一、线性表介绍1、线性结构​ 在数据元素存在非空有限集中:存在唯一的一个被称为“第一个”的数据元素存在唯一的一个被称为“最后一个”的数据元素除了第一个外,集合中每个数据元素都只有一个前趋元素除了最后一个外,集合中每个数据元素都只有一个后继元素2、线性表线性表......
  • Linux系统安装的详细步骤详解
    在VM虚拟机上安装Linux系统全过程,闭眼跟着走就行!!!1、准备好VMwareWorestation虚拟机软件和Linux系统的映像文件2、点击创建新的虚拟机3、在新建虚拟机向导中,选择典型安装模式。典型安装模式可以通过几个简单的步骤快速安装虚拟机,更方便操作。点击下一步。4、在安装来源......
  • PYTHON学习笔记(六、python数据结构--字典)
    (3)dict字典字典数据类型的含义是:根据一个信息查找另一个信息的方式构成了“键值对”,它表示索引用的键和对应的值构成对应的关系。1、字典的创建方式1)使用{ }直接创建字典使用{ }创建字典的语法结构如下:d={key1:value1,key2:value2......}例如:#使用{}创建字典d=......