C语言 堆详解

1.堆的引入

之前我们在 二叉树详解文章里谈过二叉树的顺序结构存储方式,普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。如下图可视:
在这里插入图片描述
所以我们通常把堆(这特殊的一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统中虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

2.堆的概念及结构

概念:如果有一个集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki <= K(2i+1) 且 Ki<= (K2i+2) 或者(Ki >= K(2i+1) 且 Ki >= K(2i+2)) i = 0,1,2…,则称为小堆(或大堆)。
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
要掌握的说明:对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则
对于序号为i的结点
有:
1.若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
2.若2i+1<n,左孩子序号:2i+1,2i+1>=n则无左孩子
3.若2i+2<n,右孩子序号:2i+2,2i+2>=n则无右孩子
此性质可适用于接下来对堆这种完全二叉树的实现理解
堆的性质:
1.堆中某个节点的值总是不大于或不小于其父节点的值;
2.堆总是一棵完全二叉树。

小根堆:
小根堆
大根堆:
在这里插入图片描述

3.堆的实现

关于二叉树的性质还不怎么掌握的可以看往期的二叉树详解,以便理解下面的知识。
3.1调整算法
1 向下调整算法
现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆或大堆。
但是向下调整算法有一个前提:左右子树必须是一个堆,才能调整。所以我们必须从数组的最后一个非叶子节点开始往回对每一个非叶子节点,直到根节点使用向下调整算法来调整维护这个堆。
2向上调整算法
我们通过从最后一个叶子节点开始的向上调整算法可以把它调整成一个小堆或大堆。
也是通过根据此叶子节点找到父亲节点并开始调整,从最后一个叶子节点开始往回直到根节点位置是使用向上调整算法来调整维护。

void JustDown(HPDataType* a, int n, int root)
{
	int parent = root;//传入要开始向下调整的根节点
	int child = 2 * parent + 1;//左孩子
	while (child < n)
	{
		if (a[child] < a[child + 1] && child+1<n)//选出两个孩子中最大的 ,且还要判断是否有右子树,防止越界
		{
			child = child + 1;
		}
		if (a[child]>a[parent])//子节点比父节点大,则交换    //控制大于小于号可调整成大堆或者小堆
		{
			swap(&a[child], &a[parent]);
			//更新条件继续向下
			parent = child;       
			child = parent * 2 + 1;
		}
		else//不比父亲节点大就跳出
		{
			break;
		}
	}
}
void JustUp(HPDataType* a, int n, int _child)
{
	int child = _child;
	int parent = (child - 1) / 2;//根据子节点的下标找到父亲节点下标
	while (parent >= 0)
	{
		if (a[child] >a[parent])//子节点比父亲节点还大,交换
		{
			swap(&a[child], &a[parent]);
			//更新条件 继续迭代
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

3.2堆的创建
下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?
若使用向下调整算法,这里我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。

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

在这里插入图片描述

void HeapCreate(Heap* hp, HPDataType* a, int n)
{
	hp->_a = (HPDataType*)malloc(sizeof(HPDataType));
	hp->_capacity = 2 * n;
	hp->_size = n;
	memcpy(hp->_a, a, sizeof(HPDataType) * n);//memcpy可以拷贝自定义类型数据
	// 建堆: 从最后一个非叶子节点开始进行调整
	// 最后一个非叶子节点,按照规则: (最后一个位置索引 - 1) / 2
	// 最后一个位置索引: n - 1
	// 故最后一个非叶子节点位置: (n - 1-1) / 2
	for (int i = (n - 2) / 2; i >= 0; --i)
	{
		JustDown(hp->_a, hp->_size, i);
	}

	//向上调整算法初始化堆
	//从最后一个节点开始调整
	/*int end = n - 1;
	while (end > 0)
	{
		JustUp(hp->_a, hp->_size, end);
		end--;
	}*/
}
*

3.3堆的插入*
先插入这个数到数组的尾上,再进行向上调整算法,在一个调整直到满足堆。

// 堆的插入
void HeapPush(Heap* hp, HPDataType x)
{
	assert(hp);
	if (hp->_size == hp->_capacity)//检查容量是否满了, 扩容
	{
		hp->_capacity *= 2;
		hp->_a = (HPDataType*)realloc(hp->_a, sizeof(HPDataType) * hp->_capacity);
	}
	//开始尾插
	hp->_a[hp->_size] = x;
	hp->_size++;
	JustUp(hp->_a, hp->_size, hp->_size - 1);//再向上调整

}

3.4堆的删除
删除堆是删除堆顶的数据,若是直接删除的话,堆结构会被破坏掉,所以要将堆顶的数据与最后一个叶子节点数据一换,然后删除数组最后一个数据(达到删除堆顶数据的目的),再进行向下调整算法维护这个堆。

// 堆的删除
void HeapPop(Heap* hp)
{
	swap(&hp->_a[0], &hp->_a[hp->_size - 1]);  //不可能直接删除根,不然后面全乱了,所以首尾交换,尾删,再把交换过来的向下调整
	hp->_size--;
	JustDown(hp->_a, hp->_size, 0);
}

3.5对数组进行堆排序

// 对数组进行堆排序
void HeapSort(int* a, int n)
{
	// 排升序需要建大堆:
	// 因为每次都会把堆顶元素拿出来放到当前堆的最后一个位置
	// 相当于每次都会把剩余元素中的最大值(即堆顶元素)找出来
	// 放到它该有的位置(当前堆的最后一个位置
	
	//1.所以先创建维护一个大堆
	for (int i = (n - 2) / 2; i >= 0; i--)
	{
		JustDown(a, n, i);
	}
	//2.然后首尾交换,大的值一旦被换下来就一直不会变动了,所有元素都交换完毕后,升序就排好了
	int end = n - 1;
	while (end > 0)
	{
		swap(&a[0], &a[end]);//大值换到数组最后面
		JustDown(a, end, 0);//再调整,准备下一步再调换
		--end;
	}
}

3.6堆的实现具体代码
头文件,功能声明与结构定义

#pragma once
#include<stdio.h>
#include <string.h>
#include <malloc.h>
#include<assert.h>
typedef int HPDataType;
typedef struct Heap
{
	HPDataType* _a;
	int _size;  //有效元素个数
	int _capacity;//容量
}Heap;
// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);
//堆的打印
void HeapPrintf(Heap* hp);
// 对数组进行堆排序
void HeapSort(int* a, int n);

功能函数的具体实现

#include "Heap.h"
#define FILE_BUFFER_LENGTH 30000
void swap(HPDataType* h1, HPDataType* h2)
{
	HPDataType tmp = *h1;
	*h1 = *h2;
	*h2 = tmp;
}
void JustDown(HPDataType* a, int n, int root)
{
	int parent = root;//传入要开始向下调整的根节点
	int child = 2 * parent + 1;//左孩子
	while (child < n)
	{
		if (a[child] < a[child + 1] && child+1<n)//选出两个孩子中最大的 ,且还要判断是否有右子树,防止越界
		{
			child = child + 1;
		}
		if (a[child]>a[parent])//子节点比父节点大,则交换    //控制大于小于号可调整成大堆或者小堆
		{
			swap(&a[child], &a[parent]);
			//更新条件继续向下
			parent = child;       
			child = parent * 2 + 1;
		}
		else//不比父亲节点大就跳出
		{
			break;
		}
	}
}
void JustUp(HPDataType* a, int n, int _child)
{
	int child = _child;
	int parent = (child - 1) / 2;//根据子节点的下标找到父亲节点下标
	while (parent >= 0)
	{
		if (a[child] >a[parent])//子节点比父亲节点还大,交换
		{
			swap(&a[child], &a[parent]);
			//更新条件 继续迭代
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}
// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n)
{
	hp->_a = (HPDataType*)malloc(sizeof(HPDataType));
	hp->_capacity = 2 * n;
	hp->_size = n;
	memcpy(hp->_a, a, sizeof(HPDataType) * n);//memcpy可以拷贝自定义类型数据
	// 建堆: 从最后一个非叶子节点开始进行调整
	// 最后一个非叶子节点,按照规则: (最后一个位置索引 - 1) / 2
	// 最后一个位置索引: n - 1
	// 故最后一个非叶子节点位置: (n - 1-1) / 2
	for (int i = (n - 2) / 2; i >= 0; --i)
	{
		JustDown(hp->_a, hp->_size, i);
	}

	//向上调整算法初始化堆
	//从最后一个节点开始调整
	/*int end = n - 1;
	while (end > 0)
	{
		JustUp(hp->_a, hp->_size, end);
		end--;
	}*/
}
// 堆的销毁
void HeapDestory(Heap* hp)
{
	free(hp->_a);
	hp->_a = NULL;
	hp->_capacity = hp->_size = 0;
}
// 堆的插入
void HeapPush(Heap* hp, HPDataType x)
{
	assert(hp);
	if (hp->_size == hp->_capacity)//检查容量是否满了, 扩容
	{
		hp->_capacity *= 2;
		hp->_a = (HPDataType*)realloc(hp->_a, sizeof(HPDataType) * hp->_capacity);
	}
	//开始尾插
	hp->_a[hp->_size] = x;
	hp->_size++;
	JustUp(hp->_a, hp->_size, hp->_size - 1);//再向上调整

}
// 堆的删除
void HeapPop(Heap* hp)
{
	swap(&hp->_a[0], &hp->_a[hp->_size - 1]);  //不可能直接删除根,不然后面全乱了,所以首尾交换,尾删,再把交换过来的向下调整
	hp->_size--;
	JustDown(hp->_a, hp->_size, 0);
}
// 取堆顶的数据
HPDataType HeapTop(Heap* hp)
{
	return hp->_a[0];
}
// 堆的数据个数
int HeapSize(Heap* hp)
{
	return hp->_size;
}
// 堆的判空
int HeapEmpty(Heap* hp)
{
	return hp->_size == 0;
}
//打印堆
void HeapPrintf(Heap* hp)
{
	assert(hp);
	for (int i = 0; i < hp->_size; i++)
	{
		printf("%d ", hp->_a[i]);
	}
	printf("\n");
}
// 对数组进行堆排序
void HeapSort(int* a, int n)
{
	// 排升序需要建大堆:
	// 因为每次都会把堆顶元素拿出来放到当前堆的最后一个位置
	// 相当于每次都会把剩余元素中的最大值(即堆顶元素)找出来
	// 放到它该有的位置(当前堆的最后一个位置
	
	//1.所以先创建维护一个大堆
	for (int i = (n - 2) / 2; i >= 0; i--)
	{
		JustDown(a, n, i);
	}
	//2.然后首尾交换,大的值一旦被换下来就一直不会变动了,所有元素都交换完毕后,升序就排好了
	int end = n - 1;
	while (end > 0)
	{
		swap(&a[0], &a[end]);//大值换到数组最后面
		JustDown(a, end, 0);//再调整,准备下一步再调换
		--end;
	}
}

最后的测试

#include "Heap.h"
int main()
{
	Heap hp;
	int arr[] = {1,2,3,4,5,6,7,8,9,0};
	int len = sizeof(arr) / sizeof(int);
	HeapCreate(&hp, arr, len);
	HeapPrintf(&hp);

	HeapPush(&hp, 16);//插入一个元素
	HeapPrintf(&hp);
	printf("堆顶元素:%d \n", HeapTop(&hp));
	printf("堆里的元素个数:%d\n", HeapSize(&hp));

	HeapSort(arr,len);//对数组进行堆排序 升序
	for (int i = 0; i < len; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

在这里插入图片描述

发布了29 篇原创文章 · 获赞 8 · 访问量 1995

猜你喜欢

转载自blog.csdn.net/qq_44785014/article/details/104255833
今日推荐