C语言 二叉树的性质及堆的实现 + 堆排序

文章目录

  • 前言
  • 树的概念
    • 二叉树的概念
    • 二叉树的性质
  • 堆的概念
    • 物理与逻辑结构的转换
    • 堆的性质
  • 堆的实现
    • 堆结构的声明
    • 堆的基础接口
    • 堆的初始化与销毁
    • 堆的Push与Pop
    • 堆的判空,堆顶元素的返回与长度的返回
  • 堆排序

前言

二叉树是很重要的数据结构,但我们不需要实现它,只要知道它的性质,更多时候,二叉树只是作为其他结构(如AVL树,红黑树,堆)的基础结构,后者才是我们需要手撕以掌握规则的结构,所以这篇博客只介绍二叉树的性质,而后直接开始应用:实现堆结构,完成堆排序

树的概念

C语言 二叉树的性质及堆的实现 + 堆排序_第1张图片

树是一种非线性的数据结构,它是由n个节点组成的具有层次关系的集合,因为它像是一颗倒挂着的树所以把它称为树。它的叶子朝下,根却朝上
树有以下几个基本性质:

  • 根节点:根节点没有前驱
  • 除根节点外,其他节点被分为多个互不相交的集合,每个集合是一颗结构类似树的子树(子树不能相交,若相交则不是树结构
  • 关于子树:每颗子树的根节点只有一个前驱,可以有0个或多个后继。所以树是递归定义的
  • 节点的度:一个节点含有子树的个数称为度
  • 叶节点:度为0的节点为叶节点
  • 分支节点:度不为0的节点为分支节点
  • 父节点:除根节点外,其他节点都有一个前驱,该前驱为父节点
  • 子节点:若节点有后继,或者节点的子树的根节点,它们被称为子节点
  • 兄弟节点:具有相同父节点的子节点互称兄弟节点
  • 树的度:最大节点的度被称为树的度
  • 节点的层次:从根节点开始,根节点为第一层,根节点的子节点为第二层,以此类推
  • 节点的祖先:从根节点到某节点的所有可能路径上遇到的节点,都是该节点的祖先
  • 子孙:以某节点为根的子树中所有的节点被称为该节点的子孙
  • 森林:m颗不相交的树的集合称为森林

二叉树的概念

  • 二叉树为有序树,不能颠倒其左右子树
  • 二叉树不存在度为2的节点
  • 特殊概念
    • 满二叉树:若一颗二叉树每一层的节点树都达到最大值,它就是满二叉树。即第k层的节点数量为2的k-1次方
    • 完全二叉树:若一颗k层二叉树的由n个节点,当每个节点都与k层的满二叉树从1~n编号的节点对应时,它就是完全二叉树(明显区别见下图

C语言 二叉树的性质及堆的实现 + 堆排序_第2张图片
图源网络

二叉树的性质

  • 二叉树第k层的节点数量最多为2^(k-1)
  • k层二叉树的节点数量最多为(2^k) -1
  • 用n0表示度为0的节点,n2表示度为2的节点,有:n0 = n2 + 1,这个可以用节点总数和边数的关系推导
  • 具有n个节点的满二叉树的深度为log2(n+1)
  • 若对完全二叉树进行编号,对于编号为i的节点
    • 其父节点下标为:(i-1)/2,若i为0,无父节点
    • 其左孩子下标为:2 * i + 1,前提是其下标存在
    • 其右孩子下标为:2 * i + 2,前提是其下标存在

堆的概念

物理与逻辑结构的转换

C语言 二叉树的性质及堆的实现 + 堆排序_第3张图片
图片上的二叉树是一颗完全二叉树,并且它还是一颗满二叉树。而数组能完美的表示像完全二叉树这样的结构,这是二叉树的顺序表示,完全二叉树可以利用数组的所有空间,减少空间的浪费

从根节点开始,根节点为第一层,从上往下,从左往右,将数据存储到数组中。二叉树第i层的元素个数为2的(i - 1)次方,知道每层二叉树的元素个数,就能把数组中的数据转化为二叉树的形式。

第一层二叉树有2的(1 - 1)次方,1个元素,一层一层地将数组转化成二叉树
C语言 二叉树的性质及堆的实现 + 堆排序_第4张图片

堆的性质

1.堆总是一颗完全二叉树
2.堆的节点总是不大于或不小于其父节点

若一颗二叉树根节点的值小于其孩子节点,并且该树满足堆的性质,该树称作小堆。反之根节点大于其孩子节点,该树称作大堆。

堆的实现

堆在物理上是一个数组,虽然在逻辑上不是顺序表,但可以用顺序表的结构表示堆。

堆结构的声明

typedef int HDataType;//HDataType表示堆中存储的数据类型
typedef struct Heap
{
	HDataType* data; //像顺序表一样,堆结构中有一个data指针指向存储数据的数组
	size_t size;     //size表示当前堆的数据个数
	size_t capacity; //capacity表示堆的容量
}Heap;

堆的基础接口

void HeapInit(Heap* php);					//堆的初始化
void HeapDestory(Heap* php);				//堆的销毁
void HeapPush(Heap* php, HDataType(Heap* php);//插入数据到堆中
void HeapPop(Heap* php);					//堆顶数据的删除
HDataType HeapTop(Heap* php);				//堆顶元素的返回
bool HeapEmpty(Heap* php);					//堆的判空
size_t HeapSize(Heap* php); 				//堆元素个数的返回

堆的初始化与销毁

//对堆的成员赋初值
void HeapInit(Heap* php)
{
	assert(php);
	
	php->size = 0;
	php->capacity = 0;
	php->data = NULL;
}
//将malloc申请的空间释放,并且对堆的容量与元素个数置0
void HeapDestory(Heap* php)
{
	assert(php);
	
	free(php->data);
	php->capacity = 0;
	php->data = NULL;
}

堆的Push与Pop

C语言 二叉树的性质及堆的实现 + 堆排序_第5张图片

假设往图片中的小堆插入一个9,将9插入数组的最后一位,插入数据时要先检查数组的空间是否足够存储,检查扩容。找到9的父节点,比较其与父节点的大小,如果父节点大于9,则把父节点与9交换,交换后再与交换后的父节点比较,判断是否要再次交换。最坏的情况就是交换到根节点

父节点与子节点的下标关系,(子节点下标 - 1) / 2得到父节点的下标
在这里插入图片描述
C的下标为2,- 1 再 / 2得到0,说明A是C的父节点,B的下标是1,-1 再 /2得到0,说明A是B的父节点。
C语言 二叉树的性质及堆的实现 + 堆排序_第6张图片

交换节点时,只会影响到插入节点的祖先节点。

//先浏览HeapPush接口
//交换接口
void Swap(HDataType* pa, HDataType* pb)
{
	HDataType tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

//向上调整接口
void AdjustUp(HDataType* data, size_t child)
{
	size_t parent = (child - 1) / 2;
	while(child > 0)//孩子节点不是根节点,循环继续
	{
		//如果孩子节点小于父亲节点,交换
		if (data[child] < data[parent])
		{
			Swap(&data[child], &data[parent]);
			child = parent;
			parent = (child - 1 ) / 2;
		}
		else//父亲节点大于孩子节点,满足堆的性质,跳出循环
		{
			break;
		}
	}
}

//堆的Push接口
void HeapPush(Heap* php, HDataType x)
{
	assert(php);
		
	//检查扩容	
	if (php->size == php->capacity)
	{
		//如果数组满了,需要扩容。新的容量根据原来容量是否为0来判断,如果是0,新容量为4,不是0,新容量为原来的两倍
		size_t newCapacity = (php->capacity == 0) ? 4 : (php->capacity * 2);
		//将原来空间扩容,用tmp接收新开辟空间的地址
		HDataTpye* tmp = (HDataTpye*)realloc(php->data,sizeof(HDataTpye) * newCapacity);
		if (!tmp)
		{
			printf("realloc fail\n");//若开辟空间失败,if条件成立,需要结束程序
			exit(-1);
		}
		php->data = tmp;//若开辟成功,将开辟好的空间地址给data
		php->capacity = newCapacity;
	}
	
	php->data[php->size] = x;
	php->size++;
	
	//插入元素后要将元素像上调整,使其始终是一个堆
	AdjustUp(php->data, php->size - 1);
}

删除堆顶元素,不能将后面的数据往前覆盖,删掉第一个节点,这样操作会打乱堆中元素的父子关系
C语言 二叉树的性质及堆的实现 + 堆排序_第7张图片
如图片中的小堆,逻辑上的表示如下C语言 二叉树的性质及堆的实现 + 堆排序_第8张图片
将堆顶元素删除,得到的小堆是
C语言 二叉树的性质及堆的实现 + 堆排序_第9张图片
3的父节点6大于3,很明显不满足小堆的性质,如果删除使用这样的算法,维护堆的成本将会是巨大的。所以删除堆顶元素得用其他算法。

将堆顶元素与数组的最后一个元素交换,再将堆的size减1,此时的堆的堆顶不满足堆的性质,将堆顶元素向下调整:找到两个子节点中小的那个,将两者交换,再找交换后两子节点中小的那个,再交换,直到子节点中小的那个大于自身。

以上作为一个循环,当其没有子节点时,说明到了叶节点,循环结束。细节:两个子节点中,可能只有一个左子节点,所以要判断右节点是否存在,否则会出现越界访问

// 向下调整接口
// size是data数组的大小,root是要向下调整的根节点在data中的下标
void AdjustDown(HDataType* data, size_t size, size_t root)
{
	size_t parent = root;//父节点
	size_t child = root * 2 + 1;//父节点的左子节点

	while (child < size)//若父节点的子节点下标在数组范围内,循环继续
	{	
		//假设左节点是两节点中最小的,交换时只要交换parent与child,但要判断右节点的大小
		//若父节点的右节点存在,并且右节点小于左节点,将child换为右节点
		if (child + 1 < size && \
			data[child + 1] < data[child])
		{
			child++;//右节点的下标比左节点大1
		}
		if (data[parent] > data[child])//父节点大于子节点,交换
		{
			Swap(&data[parent], &data[child]);
			//父节点与子节点的更新
			parent = child;
			child = parent * 2 + 1;
		}
		else//父节点小于子节点,满足堆的性质,跳出循环
		{
			break;
		}
	}
}

//堆的Pop接口
void HeapPop(Heap* php)
{
	assert(php);
	assert(php->size > 0);//堆中的元素数不能少于1
	
	//将数组中的第一个元素与最后一个元素交换
	Swap(&php->data[0], &php->data[php->size - 1]);
	AdjustDown(php->data, php->size, 0);//将堆顶元素向下调整
}

堆的判空,堆顶元素的返回与长度的返回

三个接口较简单

//判空接口
bool HeapEmpty(Heap* php)
{
	assert(php);
	return (php->size == 0);
}
//堆顶元素返回接口
HDataTpye HeapTup(Heap* php)
{
	assert(php);
	assert(php->size > 0);
	return php->data[0];
}
//堆的长度返回
HDataType HeapSize(Heap* php)
{
	assert(php);
	return php->size;
}

堆排序

堆顶元素总是堆中最小元素,若要将数组以升序的顺序排序,每次只要取出堆顶元素,取出后,调用堆的Pop接口,Pop数据,此时Pop会调整堆中元素的顺序,使得堆顶元素是最小的数,再取堆顶数据,再调Pop接口…

//将arr数组的数据堆排序,size是数组长度
void HeapSort(HDataType* arr, size_t size)
{
	Heap hp = { 0 };//定义一个堆结构hp
	HeapInit(&hp);
	
	for (int i = 0; i < size; i++)
	{
		HeapPush(&hp, arr[i]);//将arr中的数据Push到堆中
	}

	for (int i = 0; i < size; i++)
	{
		arr[i] = HeapTop(&hp);//将堆顶元素(最小的数)存到arr数组中
		HeapPop(&hp);//删除堆顶元素
	}
}	

C语言 二叉树的性质及堆的实现 + 堆排序_第10张图片
运行结果如下
C语言 二叉树的性质及堆的实现 + 堆排序_第11张图片

你可能感兴趣的:(数据结构与算法,c语言,开发语言,后端)