首页 > 其他分享 >数据结构-堆及堆排序

数据结构-堆及堆排序

时间:2025-01-20 20:31:46浏览次数:3  
标签:arr php 堆排序 结点 算法 child 堆及 数据结构 size

1.堆的定义

  • 堆(Heap)是一种数据结构,通常是一个完全二叉树。在堆中,每个节点都有一个与其相关的值,并且满足堆的性质。堆分为两种类型:大堆和小堆。
  • 大堆:在大堆中,对于每个非叶子节点,其值都大于或等于它的子节点的值。也就是说,根节点的值是整个堆中的最大值。
  • 小堆:与大堆相反,在小堆中,对于每个非叶子节点,其值都小于或等于它的子节点的值。根节点的值是整个堆中的最小值。

左边的这幅图就是大堆,大堆中所有的父结点都大于子结点,并且是完全二叉树。右边这幅图就是小堆,小堆所有的父结点都小于子结点。

因为堆也就是完全二叉树的性质,我们可以用数组来实现,在数组中呢,所有的数据都是按顺序排的,相比于非完全二叉树,空间浪费的情况会好很多,这也就是实现二叉树中的顺序存储。

2.代码示例

因为堆分为大堆和小堆,所以实现哪一种都行,这里我实现的是大堆。(这会影响下面的向上调整算法和向下调整算法)

typedef int HPDateType;
typedef struct HeapNode
{
	HPDateType* arr;
	int size;
	int capacity;
}HP;

堆的结构

因为底层结构是数组,所以堆的结构就和顺序表一样,定义一个数组arr,数组中有效数据的个数size和数组的容量capacity。

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

堆的初始化

1.判断传入的指针是否为假

2.初始化跟顺序表也一样,数组置为空,size和capacity也都初始化为0

void HPDestory(HP* php)
{
	assert(php);
	if (php->arr != NULL)
	{
		php->arr = NULL;
		php->size = php->capacity = 0;
	}
}

销毁堆

1.判断传入的指针是否为假

2.判断数组此时是否为空,非空的话就将数组置为空,size和capacity置为0

void Swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

交换函数

因后面的入堆操作会用到,所以将交换功能封装成一个函数。

因为形参相当于实参的一份临时拷贝,所以形参的改变那不会影响实参,故我们如果想要交换两个数,需要传地址过去,也就是传址调用,这样解引用过后还是实参,之后就进行交换即可。

void HPAdjustUp(HPDateType* arr, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		//小堆:<
		//大堆:>
		if (arr[child] > arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

向上调整算法(关键!!!)

向上调整算法和向下调整算法是实现堆的核心,所以我讲这个功能封装成一个函数。向上调整算法主要用于入堆操作,在解释这段代码之前,先解释一下这个算法是干什么的。 

前提:使用向上调整算法的前提是你插入的数据会破坏堆的结构,例如:插入到尾端的数据比它的父结点要大,那么此时这颗子树就不是大堆,而整体也就不是大堆。就是不管插入还是删除数据,都必须严格遵守大堆或小堆的结构,而让数组保持大堆或小堆就要用到向上调整算法来调整数组的结构,使其发生改变后也能调整回大堆或小堆。

向上调整算法(大堆):顾名思义,就是让子结点和父结点进行比较。如果子结点比父结点要大,我们就要交换子结点和父结点的值,这样这颗子树才是大堆,之后让新的子结点,也就是之前那棵子树的父结点继续向上比较,直到子结点转移到根结点时结束,因为根结点无父结点。

在实现向上调整算法时我们要用到子结点(child)和父结点(parent)

1.因为是入堆操作,所以传入的子结点(child)就是最后一个数据,而它的父结点就是:(下标-1)/2

2.因为要不断向上比较,当子结点到根结点时就要停止,所以循环条件是child>0,然后根据入堆的数据来和它的父结点进行比较,如果小于父结点,我们就不需要调整,此时还是大堆。如果大于它的父结点,就要交换两者的值,并将child移到原父结点的位置,再根据新的child来找到新的parent以此类推

注意:传入的数据只需要数组arr和子结点的下标即可,不需要父结点的下标

void HPPush(HP* php, HPDateType x)
{
	assert(php);
	//判断空间是否充足
	//空间不足
	if (php->size == php->capacity)
	{
		int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
		HPDateType* tmp = (HPDateType*)realloc(php->arr, newcapacity * sizeof(HPDateType));
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(1);
		}
		php->arr = tmp;
		php->capacity = newcapacity;
	}
	//空间充足
	php->arr[php->size] = x;
	//向上调整算法
	HPAdjustUp(php->arr, php->size);
	php->size++;
}

入堆

1.判断传入的指针是否为假

2.分情况讨论。空间不足时需要增容,增容操作我在线性表那一章讲过,故这里就不再赘述

3.空间充足时,直接将数据放到下标为size的位置处

4.利用向上调整算法来调整整个数组的结构

5.size++

注意:size++这步操作要放在向上调整算法之后,如果放在向上调整算法之前,那么最后一个数据的下标就是size-1,到时我们传入的child就要是size-1

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

打印堆中的数据

1.判断传入的指针是否为假

2.遍历整个数组,打印出每个位置的数据

3.换行操作

bool HPEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}

判断堆是否为空

因为下面的出堆操作要判断堆是否为空,故将这个功能封装成一个函数。

1.判断传入的指针是否为假

2.判断size是否等于0,等于0说明数组为空,不等于0说明数组非空

void HPAdjustDown(HPDateType* arr, int parent, int n)
{
	int child = parent * 2 + 1;
	//默认是左子树
	//循环条件是左子树不能越界访问
	while (child < n)
	{
		//比较左子树和右子树的大小
		//注意右子树不能越界访问
		if (child + 1 < n && arr[child] < arr[child + 1])
		{
			child = child + 1;
		}
		//向下调整
		if (arr[child] > arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

向下调整算法

前提:因出堆操作是把堆顶的数据给删除掉,而在出堆操作之后,剩下的结构会被打乱,这是我们就要用向下调整算法来调整剩下的结构,使其重新调整为大堆或小堆。

向下调整算法(大堆):顾名思义,就是让父结点和子结点进行比较。因为是向下调整,所以我们要从根结点开始,首先让根结点和子结点进行比较,而在和子结点比较之前,要先比较两个子结点,也就是左子树和右子树,让较大的那个子结点和根结点进行比较,这么做是因为较大的子结点如果比根结点要大的话,那么交换两者的值之后,以根结点为父结点的子树就符合大堆的结构,所以没必要两个都比较。比较一次之后,让父结点转移到原较大子结点的位置,再找到新的子结点,再进行比较,而结束的条件是左子树要小于size,不能越界访问。

1.根据父结点的下标,让其*2+1就找到了左子树,而右子树就是左子树的下标。而默认求左子树是因为堆是完全二叉树,结点从左到右是依次排列的,如果一个父结点有右子树,那么这颗树一定有左子树,而有左子树,不一定有右子树,故默认求的是左子树

2.循环条件就是左子树的下标要小于size,进入到循环里面,先比较的就是左右子树的大小,找到大的那个,再与父结点进行比较。如果比父结点小,直接退出循环即可。如果比父结点大,就交换两者的值,然后父结点转移到原较大子结点的位置,再找到新的子结点,再进行比较,以此类推。

注意:在比较左右子树时要考虑没有右子树的情况,所以在判断左右子树的条件中要加上child+1要小于size,不能越界访问。

void HPPop(HP* php)
{
	assert(!HPEmpty(php));
	//出堆出的是堆顶的数据
	//先将堆顶和堆底的数据进行交换
	Swap(&php->arr[0], &php->arr[php->size - 1]);
	php->size--;
	//向下调整算法
	HPAdjustDown(php->arr, 0, php->size);
}

出堆

1.判断堆是否为空

2.交换堆顶和堆底的数据

3.size--

4.调用向下调整算法

注意:size--要放在向下调整算法之前,不然的话先调用向下调整算法,会使数组变为原数组。

HPDateType HPTop(HP* php)
{
	assert(php);
	return php->arr[0];
}

取堆顶的数据

1.判断传入的指针是否为假

2.直接返回数组中下标为0的数据即可

注意:上面的向上调整算法和向下调整算法如果要实现小堆的话,只需要将>改为<即可。

3.堆排序

这篇既然讲到了堆,而后面我们学排序时会学堆排序,故趁热打铁直接把堆排序给讲完。

堆排序:顾名思义,就是利用堆的结构来将数组中的数据进行排序,而堆排序的时间复杂度是要比冒泡排序的时间复杂度要低的,比冒泡排序更高效。

堆排序我依旧以大堆为例。

堆排序思想:将一个随机的数组通过向下调整算法或向上调整算法,将数组的结构转变为堆结构。之后再将堆顶的数据和堆底的数据进行交换,固定好一个值,再将前面的数据调整为堆结构,再次交换堆顶和堆底的数据,一直循环,直到排好序为止。

可能会有人疑惑为什么这么做就能排序?因为堆顶的数据一定是当前数组中最大或最小的数据,再利用堆排序,每次都能把当前数组中最大或最小的数据排在后面,等排之后,数组就会变为有序数组。

void HeapSort(int* arr, int n)
{
	//创建堆
	//从最后一棵树开始
	//利用向下调整算法创建堆
	for (int i = (n - 2) / 2; i >= 0; i--)
	{
		HPAdjustDown1(arr, i, n);
	}
	//堆排序
	int end = n - 1;
	for (int i = end; i > 0; i--)
	{
		Swap1(&arr[0], &arr[end]);
		HPAdjustDown1(arr, 0, end);
		end--;
	}
}

1.将随机数组调整为堆结构。这一步的思想是先找到最后一颗树,先对最后一颗树进行调整,再调整下一棵树,以此类推,当i=0是,也就是到了根结点,在对整棵树进行调整,这样就能将数组调整为堆结构

2.将堆顶和堆底的数据进行交换,这里我是用一个临时变量end来记录最后一个数据的下标,因为一次固定一个数据,故循环次数就是size-1,也就是end,交换之后利用向下调整算法,把前面的数据(也就相当于一个新的数组)调整为堆结构,最后让end--,以此类推,重复上述过程。

这里我用的是向下调整算法来实现堆排序,也可以用向上调整算法来实现,大家可以去试试。

通过排序我们可以发现,如果你建的是大堆,那么排序的结果就是升序,建的是小堆,排序的结果就是降序。

4.总结

堆和堆排序的关键就是要掌握向上调整算法和向下调整算法,把这两个掌握,堆和堆排序实现起来就会轻松很多,最后还是实现一个功能,检验一个功能。

标签:arr,php,堆排序,结点,算法,child,堆及,数据结构,size
From: https://blog.csdn.net/2301_80236968/article/details/145267342

相关文章

  • 【第一天】零基础入门刷题Python-算法篇-数据结构与算法的介绍(持续更新)
    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档文章目录前言一、Python数据结构与算法的详细介绍1.基本概念2.Python中的数据结构1.列表(List)2.元组(Tuple)3.字典(Dictionary)4.集合(Set)5.字符串(String)3.Python中的常用算法1.排序算法2.搜索算法3.递......
  • 数据结构——栈
    1、栈的概念(1)是一种特殊的线性表,只能在一端进行插入或删除操作(2)逻辑结构:线性结构;存储结构:既可以是顺序存储,也可以是链式存储(3)栈顶:允许插入或删除的一端(4)栈底:不允许插入或删除的一端,位置固定不变(5)空栈:栈中没有元素(6)使用特点:LIFO(后进先出)2、操作#define_CRT_SECURE_NO_......
  • 2024网安数据结构恐龙提纲
    2024网安数据结构......
  • NOIP 冲刺之——数据结构
    \(\texttt{0x00}\)前言本篇文章主要记录笔者NOIP冲刺阶段复习的各种数据结构题型及tricksanstips,同时也用于及时复习与巩固。那么,开始吧。\(\texttt{0x01}\)树状数组、线段树知识点\(1\):二维偏序众所周知,逆序对可以用归并排序离线求,但是要求在线呢?这时候我们会想到......
  • 扬帆数据结构算法之雅舟航程,漫步C++幽谷——链表分类探析与双链表之定义与构筑
    人无完人,持之以恒,方能见真我!!!共同进步!!文章目录一、链表的分类二、双链表的实现1.双链表结构的定义2.双链表的初始化和销毁初始化函数1初始化函数2销毁函数3.双链表的打印以及节点的申请打印函数节点的申请4.双链表的头插和尾插头插函数尾插函数5.双链表的查找和......
  • 数据结构与算法之栈: LeetCode 71. 简化路径 (Ts版)
    简化路径https://leetcode.cn/problems/simplify-path/description/描述给你一个字符串path,表示指向某一文件或目录的Unix风格绝对路径(以‘/’开头),请你将其转化为更加简洁的规范路径在Unix风格的文件系统中规则如下一个点‘.’表示当前目录本身此外,两个......
  • 数据结构期末复习
    数据结构期末复习1绪论算法的基本概念数据结构的基本概念数据抽象和抽象数据类型描述数据结构和算法算法分析的基本方法2线性表线性表的定义及基本操作线性表的顺序存储线性表的链接存储3栈和队列栈和队列的基本概念栈和队列的顺序存储结构栈和队列的链式存储结构......
  • [数据结构学习笔记16] 线性查找(Linear Search)
    查找算法是指从一个集合里比如数组,列表,树里查找我们想要的值。我们从最简单的线性查找开始。线性查找,就是遍历集合里的元素,查看是否有和我们想要查找的值相同的,有则查找成功,没有则查找失败。比如:5,8,6,9,1,7,3,2,4我们要找3,那从5开始依次往后,到了第7个(下标6),我们找到了3。如果我们要找......
  • 科普文:算法和数据结构系列【高效的字符串检索结构:字典树Trie树原理、应用及其java示例
    概叙科普文:算法和数据结构系列【算法和数据结构概叙】-CSDN博客科普文:算法和数据结构系列【非线性数据结构:树Tree和堆Heap的原理、应用、以及java实现】-CSDN博客科普文:算法和数据结构系列【树:4叉树、N叉树】_动态维护四叉树-CSDN博客科普文:算法和数据结构系列【二叉树总结......
  • 科普文:算法和数据结构系列【死磕字典树:字典树的升级版三叉树Ternary Search Tree优化
    概叙科普文:算法和数据结构系列【死磕字典树:来一个英文字母游戏】-CSDN博客科普文:算法和数据结构系列【高效的字符串检索结构:字典树Trie树原理、应用及其java示例代码解读】-CSDN博客‌原理‌:Trie树利用字符串之间的公共前缀来减少不必要的字符串比较,从而提高查询效率。每个......