堆排序算法

什么是堆?

(二叉)堆是一个数组,可以被看成一个近似的完全二叉树,如下图:图(a)是堆的存储结构,图中红色线表示逻辑结构中的父子关系指向;图(b)是堆的逻辑结构。

堆的分类

最大堆:除了根以外的所有结点(假设结点下标为i)都满足:Arrparent(i) >= Arri

最小堆:除了根以外的所有结点(假设结点下标为i)都满足:Arrparent(i) <= Arri

堆的性质

1.堆中某个节点的值总是不大于或不小于其父节点的值

2.堆总是一棵完全二叉树。

3.堆的高度:该节点到叶结点最长简单路径上边的数目

维护堆的性质代码及执行过程(AdjustDown)

适用场景,用于堆的插入或删除时维护堆的性质

最大堆:使得下标i为根结点的子树重新遵循最大堆的性质

假设:Arr.length:表示数组元素个数;Arr.heap-size:表示堆元素个数

如下图:Arr.heap-size当前=10,在执行MAX-HEAPAdjustDown(Arr,2)时,(a):下标为2的结点违背了最大堆的性质,(b):找到孩子中最大的孩子与下标为2的结点元素做交换,同时下标为4的结点也违背了最大堆的性质,(c):找到孩子中最大的结点与下标为4的结点元素做交换,最后在调用MAX-HEAPAdjustDown(Arr, 9),不再有数据交换就返回。

MAX-HEAPAdjustDown的时间复杂度为O(h),h为树的高度(该节点到叶结点的最长简单路径上边的数目)

最小堆反之

堆的创建

我们采用自底向上的方法利用MAX-HEAPAdjustDown把一个大小为n=Arr.length的数组转换为最大堆。我们从(二叉)堆最后一个非叶结点往前遍历调用MAX-HEAPAdjustDown算法建堆。

时间复杂度为O(n)

注意:必须采用自底向上的方法,因为我们先处理下层子树,保证子树已经是最大堆,上层调用堆化才有效。

堆排序算法

什么是堆排序算法?

就是将一个已经满足最大堆或者最小堆的数组排序,让数组最后的结果按从高到低或者从高到低排序。我们以最大堆为例,将数组中的元素排序。如下图:由于数组第一个元素是最大的,我们将其与最后一个元素交换位置,使得最大元素到达她正确的位置,堆的大小减一,然后调用MAX-HEAPAdjustDown算法使得由堆尾交换来的结点向下调整来维持最大堆的性质,持续这样做直到堆的大小为1时结束,此时数组如图(g)所示是有序数组

时间复杂度为O(nlgn)

升序要不要建小堆? 答案是**建大堆**。理由是:小堆的堆顶最小,应该待在结果的第一位,可我们后面要把它跟末尾元素交换------最小的元素会被扔到最后面去,那就成降序了。

参见后面的向下调整算法,堆排序就是"先垫出来一个大堆,然后把堆顶一次次扔到数组尾部":

cpp 复制代码
void HeapSort(int* a, int n)
{
	// 1. 建大堆:从最后一个非叶节点倒着向下调整
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
		AdjustDown(a, n, i);

	// 2. 交换堆顶与末尾,缩小范围,再向下调整
	for (int end = n - 1; end > 0; --end)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);   // 注意第二个参数是 end,即"当前堆的元素个数"
	}
}

复杂度:建堆 O(N) + n 次删除 O(N·logN) = O(N·logN),空间 O(1)。

最大堆的模拟实现

前文为了便于理解,数组下标是从1开始的,现在我们使用数组下标为0开始模拟堆的实现

堆的结构体

cpp 复制代码
typedef int HeapDataType;//堆中存储的数据类型

//实现小堆
typedef struct Heap
{
	HeapDataType* _a;//适用动态开辟空间
	int _size;//记录数组元素个数
	int _capacity;//记录数组还能用于存储数据的空间大小
}Heap;

为什么堆不用链表?因为堆要求"完全二叉树",而完全二叉树的节点编号是连续的,用数组存不会浪费任何一格,还能 O(1) 随机访问任意节点的父/子。链表做不到这一点,也没必要做到。

核心调整算法

注意:在调整之前,堆已经是最大堆了

cpp 复制代码
//向下调整,用于最大堆在删除堆顶元素后或建堆维持堆的性质使用
//向下调整最多走 树的高度 = logN 步
void AdjustDown(HeapDataType* a, int n, int root)//参数:a表示堆的地址,n表示堆的大小,root表示根的下标
{
	//首先找到孩子中最大的值与父亲做比较,出现情况:比父亲大,交换;反之,已经满足了堆的性质结束
	int parent = root;
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child + 1] > a[child])//child + 1 < n判断左孩子算法存在,否则会越界
		{
			++child;//左孩子比右孩子大
		}
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}

	}
}

//向上调整,用于最大堆在堆尾插入新元素后,调整堆
///向上调整最多走 树的高度 = logN 步
void AdjustUp(HeapDataType* a, int child)//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;
		}
	}
}

|------|---------|-----------|
| | 比较对象 | 使用场景 |
| 向下调整 | 和更大的孩子比 | 建堆,删除堆顶元素 |
| 向上调整 | 只和父亲比 | 插入元素 |

其他算法

cpp 复制代码
//交换元素代码
void Swap(HeapDataType* p1, HeapDataType* p2)
{
	//交换
	HeapDataType tmp = *p1;
	* p1 = *p2;
	*p2 = tmp;
}

//HeapInit:把一棵乱序二叉树"整理"成堆
void HeapInit(Heap* php, const HeapDataType* a, int n)//参数:php表示堆的地址,a表示数组地址,n表示数组大小
{
	assert(php);//判空
	assert(n >= 0);
	//为堆开辟空间
	php->_a = (HeapDataType*)malloc(sizeof(HeapDataType) * n);
	if (php->_a == NULL)
	{
		perror("malloc fail");//开辟空间失败
		return;
	}
	memcpy(php->_a, a, sizeof(HeapDataType) * n);//复制数组
	php->_size = n;
	php->_capacity = n;
	//建堆:从最后一个非叶子结点倒着往前调,掉用向下调整算法
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(php->_a, php->_size, i);
	}
	//注意 i >= 0 不能写成 i > 0,否则根节点漏掉;也不能顺手写成 unsigned,i 减到 -1 时会翻成一个巨大的正数,直接死循环。


}

//向堆中插入元素,尾插
void HeapPush(Heap* php, HeapDataType x)
{
		assert(php);

		if (php->_size == php->_capacity)     // 满了就扩容
		{
			int newCapacity = php->_capacity == 0 ? 4 : php->_capacity * 2;
			HeapDataType* tmp = (HeapDataType*)realloc(php->_a, sizeof(HeapDataType) * newCapacity);
			if (tmp == NULL)
			{
				perror("realloc fail");
				return;         // realloc 失败时原内存仍在
			}
			php->_a = tmp;
			php->_capacity = newCapacity;
		}

		php->_a[php->_size] = x;              // 尾插,保证物理上仍是完全二叉树
		php->_size++;
		AdjustUp(php->_a, php->_size - 1);//数组是从0开始的所有要减1
	
}

//删除删除堆顶元素
void HeapPop(Heap* php)
{
	assert(php);
	assert(php->_size > 0);

	Swap(&php->_a[0], &php->_a[php->_size - 1]);
	php->_size--;
	AdjustDown(php->_a, php->_size, 0);
}

HeapDataType HeapTop(Heap* php)
{
	assert(php);
	assert(php->_size > 0);
	return php->_a[0];
}

int  HeapSize(Heap* php) { assert(php); return php->_size; }
bool HeapEmpty(Heap* php) { assert(php); return php->_size == 0; }

void HeapDestory(Heap* php)
{
	assert(php);
	free(php->_a);
	php->_a = NULL;
	php->_capacity = php->_size = 0;
}

模拟实现路径:堆的模拟实现(堆的模拟实现 · 28bd342 · Sylvia/C_code - Gitee.com)

实验结果:

相关推荐
Peregrine91 小时前
数据结构 - > 树和二叉树
数据结构·算法
我是人✓1 小时前
Java面向对象核心特性深度解析
java·前端·算法
j7~1 小时前
【算法】专题四.前缀和:一维二维模板,配套 LeetCode 真题,结合同余定理、哈希前缀和,搞定子数组与子矩阵区间求和问题。
数据结构·c++·算法·前缀和·优选算法
看浪的路人1 小时前
第1讲:2026 大模型应用开发全景——Prompt / RAG / 微调 / Agent 到底先用哪个
人工智能·算法
怕浪猫1 小时前
AI Agent 的安全怎么保障?面试最后一关的终极问题
算法·面试·github
晓德2 小时前
7、LLM 模型是怎么训练生成的?(4)指令微调
人工智能·学习·算法·信息安全
启观川2 小时前
数据结构与算法 -第 3 章 常用算法-回溯算法
数据结构·笔记·python·算法
零基础1232 小时前
LLM4AD:面向算法设计的大语言模型智能体框架
人工智能·算法·语言模型
启观川10 小时前
数据结构与算法 -第 3 章 常用算法-动态规划
数据结构·笔记·python·算法