数据结构之堆

本文详细介绍了完全二叉树的概念及其与满二叉树的区别,阐述了完全二叉树适合使用数组存储的原因。并通过实例演示了如何利用数组存储完全二叉树,并在此基础上构建小堆和大堆,包括初始化堆、调整堆以及插入和删除操作的具体实现。
摘要由CSDN通过智能技术生成

写在前面:

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把 (一种二叉树) 使用顺序结构的数组来存储。

二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构,今天总结一下顺序结构。

首先来了解一下完全二叉树

在这里插入图片描述

  1. 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是 说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。
  2. 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K 的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对 应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树

这里主要涉及到堆排序的问题,通过将一维数组构建成一个堆,然后进行排序插入,查找,删除等操作。

初始化堆与调整成为大堆或小堆

开始初始化为:下图(一个无序的堆),是没有太大的意义的,因此我们需要通过算法的调整来构建成一个小堆或大堆,以方便以后的查找和删除等操作。
在这里插入图片描述

这里引入两个个概念: 小堆 大堆
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为 小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆

简单来说:
1.大堆:根节点是最大的,且除根节点外所有的父亲结点都大于它的孩子结点
2.小堆:根节点是最小的,且除根节点外所有的父亲结点都小于它的孩子结点
比如:
在这里插入图片描述

如何初始化堆并调整成为一个小堆或大堆

我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。如何调整为大堆或小堆呢?这里我们从根节点开始调整,一直调整到非叶子节点,就可以调整成堆。
所以首先我们需要初始化一个堆,但在这之前需要定义堆类型的结构体。

具体实现

因为我们知道堆在实际上是用数组来存储的
1.定义结构体:

typedef int HPDataType;
typedef struct  Heap//定义结构体
{
	HPDataType* _a;//堆在实际上是一个数组,完全二叉树是逻辑上的存储,但物理上还是存在数组里的
	int _size;//大小
	int _capacity;//容量
}Heap;

2.定义一个结构体变量和一个数组:

	int a[]={2,13,23,6,35,16,56,36};
	Heap hp;

3.初始化一个堆并构建成小堆或者大堆:
这里就以初始化小堆为例
(1)先申请内存,初始化为一个堆:

void HeapInit1(Heap*hp,HPDataType* a,int n)//初始化为小堆
{   //这里的 n 是接受数组元素个数的形参
	int i;
	hp->_a=(HPDataType*)malloc(sizeof(HPDataType)*n);//申请内存
	memcpy(hp->_a,a,sizeof(HPDataType)*n);//将数据复制到申请的内存中
	hp->_size=n;//大小和容量
	hp->_capacity=n;
	//构建成堆
	for(i=(n-1-1)/2;i>=0;--i)//循环调整堆
	{
     AdjustDown1(hp->_a,hp->_size,i);//构建成小堆
	 }

}

(2)向下调整为小堆:

void AdjustDown1(HPDataType* a,size_t n,size_t parent)//向下调整构建小堆
{//通过父亲节点和孩子节点进行比较
	size_t child=parent*2+1;//先找到左孩子,就找到右孩子通过下标访问
	while(child<n)
	{
		if((child+1)<n &&a[child+1]<a[child])//找到左右孩子中小的那个数
		{//调整为大堆则与此相反,找到左右孩子中大的那个数,如果大于父亲节点就交换
			++child;
		}
		if(a[child]<a[parent])//如果小于父亲节点的话就交换
		{
			Swap(&a[child],&a[parent]);//传地址才能真正的交换值
			parent=child;//并且继续向下遍历
			child=parent*2+1;
		}
		else
		{
			break;
		}
	}
}

先来看一下结果:
在这里插入图片描述
再直观的感受一下:
在这里插入图片描述
我们可以看到从根节点,父亲节点均小于它的左右两个孩子节点,那么我们就成功的构建成了一个小堆。

构建成堆了之后就可以进行一些对堆的操作了。

功能实现:
void HeapInit1(Heap*hp,HPDataType* a,int n);//初始化为小堆
void HeapInit2(Heap*hp,HPDataType* a,int n);//初始化为大堆
void AdjustDown1(HPDataType* a,size_t parent,size_t n);//向下调整为小堆
void AdjustDown2(HPDataType* a,size_t n,size_t parent);//向下调整为大堆
void AdjustUp(HPDataType* a,size_t child);//向上调整
void HeapPrint(Heap*hp);//打印数据
void HeapDestory(Heap*hp);//删除堆
void HeapPush(Heap*hp,HPDataType x);//插入数据
void HeapPop(Heap*hp);//删除堆顶
HPDataType HeapTop(Heap*hp);//取堆顶元素
size_t HeapSize(Heap*hp);//堆的大小

这里我们讲一个向上调整,因为在做数据的插入时如果从堆顶插入的话,实现起来不太现实,而且有可能会导致堆顺序的混乱,因此我们采用向上调整法,从数组的最后一个元素后插入,再向上调整构建成大堆或小堆。

同样以小堆为例:

void HeapPush(Heap*hp,HPDataType x)//插入一个数,使用向上调整法
{
	if(hp->_size==hp->_capacity)//如果满了之后
	{
		size_t newcapacity=hp->_capacity== 0 ? 2 : hp->_capacity*2;//成2倍的增
		hp->_a=(HPDataType*)realloc(hp->_a,sizeof(HPDataType)*newcapacity);
		hp->_capacity=newcapacity;//将新的容量给到原来的容量
	}

	hp->_a[hp->_size]=x;
	hp->_size++;

	AdjustUp(hp->_a,hp->_size-1);//向上调,从最后一个下标开始调

}
void	AdjustUp(HPDataType* a,size_t child)//向上调整
{
	int parent = (child -1)/2;//首先计算插入进来的数据的parent,
	//while(parent>=0)//这个有问题
	while(child>0)//走到根
	{
		if(a[child]<a[parent])//如果插入进来的小于parent就交换
		//if(a[child]>a[parent])//如果插入进来的大于parent就交换

		{
			Swap(&a[child],&a[parent]);//只是交换了值,位置并没有交换
			child=parent;//child继续往上走,把parent变成下一个child
			parent=(child-1)/2;//重新计算parent
		}
		else
		{
			break;//没有返回值的情况尽量使用break;表示跳出向上调整的循环
		}
	}
}

测试一下:
在这里插入图片描述
直观验证一下:
在这里插入图片描述

由此可见,通过“尾插”的方式,插入一个数据,然后先找到他的父亲节点,然后与他的父亲节点进行比较,如果它之前是一个小堆的话,如果新插入的数据比父亲节点大则交换,依次循环,而如果是大堆的话则判断它是否大于父亲节点,如果比父亲节点小的话则交换,并继续循环,最终调整成为小堆或大堆。

代码汇总:
头文件:
#include<stdlib.h>
#include<string.h>
#include<stdio.h>
#include<stdlib.h>
typedef int HPDataType;
typedef struct  Heap//定义结构体
{
	HPDataType* _a;//堆在实际上是一个数组,完全二叉树是逻辑上的存储,但物理上还是存在数组里的
	int _size;
	int _capacity;
}Heap;
void HeapInit1(Heap*hp,HPDataType* a,int n);//初始化为小堆
void HeapInit2(Heap*hp,HPDataType* a,int n);//初始化为大堆
void AdjustDown1(HPDataType* a,size_t parent,size_t n);//向下调整
void AdjustDown2(HPDataType* a,size_t n,size_t parent);//向下调整
void AdjustUp(HPDataType* a,size_t child);//向上调整
void HeapPrint(Heap*hp);//打印数据
void HeapDestory(Heap*hp);//删除堆
void HeapPush(Heap*hp,HPDataType x);//插入数据
void HeapPop(Heap*hp);//删除堆顶
HPDataType HeapTop(Heap*hp);//取堆顶元素
size_t HeapSize(Heap*hp);//堆的大小

测试代码:
#include<stdio.h>
#include"Heap.h"
void TestHeap()
{
	int a[]={2,13,23,6,35,16,56,36};
	Heap hp;
	printf("构建的小堆为:\n");
	HeapInit1(&hp,a,sizeof(a)/sizeof(int ));//初始化
	HeapPrint(&hp);
	printf("插入一个数据调整为小堆则为:\n");
    HeapPush(&hp,4);//插入一个数,使用向上调整法
	HeapPrint(&hp);
	//printf("构建的大堆为:\n");
	//HeapInit2(&hp,a,sizeof(a)/sizeof(int ));//初始化
    //HeapPush(&hp,-1);//插入一个数,使用向上调整法
	//HeapPrint(&hp);
    //HeapPop(&hp);//删除,不能动相对位置,只能动头和尾
	//HeapPrint(&hp);
    //HeapPop(&hp);//删除,不能动相对位置,只能动头和尾
    //printf("%d\n",HeapTop(&hp));
	//HeapPrint(&hp);
}

int main()
{

 TestHeap();
 system("pause");
 return 0;
}
子函数:
#include"Heap.h"

void HeapInit1(Heap*hp,HPDataType* a,int n)//初始化为小堆
{
	int i;
	hp->_a=(HPDataType*)malloc(sizeof(HPDataType)*n);//申请内存
	memcpy(hp->_a,a,sizeof(HPDataType)*n);//将数据复制到申请的内存中
	hp->_size=n;//大小和容量
	hp->_capacity=n;
	//构建成堆
	for(i=(n-1-1)/2;i>=0;--i)//循环调整堆
	{
     AdjustDown1(hp->_a,hp->_size,i);//构建成小堆
	 }

}
void HeapInit2(Heap*hp,HPDataType* a,int n)
{
	int i;
	hp->_a=(HPDataType*)malloc(sizeof(HPDataType)*n);
	memcpy(hp->_a,a,sizeof(HPDataType)*n);
	hp->_size=n;
	hp->_capacity=n;
	//构建成堆
	for(i=(n-1-1)/2;i>=0;--i)//循环调整数据
	{
     AdjustDown2(hp->_a,hp->_size,i);//构建成大堆
	 }

}
void Swap(HPDataType*p1,HPDataType* p2)//交换函数
{
	HPDataType x=*p1;//注意解引用
	*p1=*p2;
	*p2=x;

}
void AdjustDown1(HPDataType* a,size_t n,size_t parent)//向下调整构建小堆
{
	size_t child=parent*2+1;
	while(child<n)
	{
		if((child+1)<n &&a[child+1]<a[child])//找到左右孩子中小的那个数
		{
			++child;
		}
		if(a[child]<a[parent])//如果小于父亲节点的话就交换
		{
			Swap(&a[child],&a[parent]);//传地址才能真正的交换值
			parent=child;//并且继续向下遍历
			child=parent*2+1;
		}
		else
		{
			break;
		}

	}

}
void AdjustDown2(HPDataType* a,size_t n,size_t parent)//向下调整构建为大堆
{
	size_t child=parent*2+1;
	while(child<n)
	{
		if((child+1)<n &&a[child+1]>a[child])//找到左右孩子中大的那个数
		{
			++child;
		}
		if(a[child]>a[parent])//如果大于父亲节点的话就交换

		{
			Swap(&a[child],&a[parent]);//传地址才能真正的交换值
			parent=child;//并且继续向下遍历,重新计算孩子和父亲
			child=parent*2+1;
		}
		else
		{
			break;
		}

	}

}
void	HeapPrint(Heap*hp)//打印数据
{
	int i=0;
	for(i=0; i<hp->_size; ++i)
	{
		printf("%d ",hp->_a[i]);
	}
	printf("\n");

}
void HeapDestory(Heap*hp)//删除堆
{
	if(hp->_a)
	{
		free(hp->_a);
	}
	hp->_size=hp->_capacity=0;
}
void HeapPush(Heap*hp,HPDataType x)//插入一个数,使用向上调整法
{
	if(hp->_size==hp->_capacity)//如果满了之后
	{
		size_t newcapacity=hp->_capacity== 0 ? 2 : hp->_capacity*2;//成2倍的增
		hp->_a=(HPDataType*)realloc(hp->_a,sizeof(HPDataType)*newcapacity);
		hp->_capacity=newcapacity;//将新的容量给到原来的容量
	}

	hp->_a[hp->_size]=x;
	hp->_size++;

	AdjustUp(hp->_a,hp->_size-1);//向上调,从最后一个下标开始调

}
void	AdjustUp(HPDataType* a,size_t child)//向上调整
{
	int parent = (child -1)/2;//首先计算插入进来的数据的parent,
	//while(parent>=0)//这个有问题
	while(child>0)//走到根
	{
		if(a[child]<a[parent])//如果插入进来的小于parent就交换
		//if(a[child]>a[parent])//如果插入进来的大于parent就交换

		{
			Swap(&a[child],&a[parent]);//只是交换了值,位置并没有交换
			child=parent;//child继续往上走,把parent变成下一个child
			parent=(child-1)/2;//重新计算parent
		}
		else
		{
			break;//没有返回值的情况尽量使用break;表示跳出向上调整的循环
		}
	}
}
void HeapPop(Heap*hp)//删除,不能动相对位置,只能动头和尾
{
	Swap(&hp->_a[0],&hp->_a[hp->_size-1]);
	hp->_size--;//删掉最后一个数即删掉头
    AdjustDown1(hp->_a,hp->_size,0);//再次向下调整成小堆
}
HPDataType HeapTop(Heap*hp)
{
	return hp->_a[0];

}
size_t HeapSize(Heap*hp)
{

	return hp->_size;
}

另外还有堆排序,会在后面的排序一起总结。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值