堆以及堆排序的实现

一、树的概念

树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因 为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的
有一个特殊的结点,称为根结点,根结点没有前驱结点 除根结点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i <= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继因此,树是递归定义的。

结点的度:一个结点含有的子树的个数称为该结点的度; 如上图:A的为6
叶结点或终端结点:度为0的结点称为叶结点; 如上图:B、C、H、I...等结点为叶结点
非终端结点或分支结点:度不为0的结点; 如上图:D、E、F、G...等结点为分支结点
双亲结点或父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点; 如上图:A是B的父结点
孩子结点或子结点:一个结点含有的子树的根结点称为该结点的子结点; 如上图:B是A的孩子结点
兄弟结点:具有相同父结点的结点互称为兄弟结点; 如上图:B、C是兄弟结点
树的度:一棵树中,最大的结点的度称为树的度; 如上图:树的度为6
结点的层次:从根开始定义起,根为第1层,根的子结点为第2层,以此类推;
树的高度或深度:树中结点的最大层次; 如上图:树的高度为4
堂兄弟结点:双亲在同一层的结点互为堂兄弟;如上图:H、I互为兄弟结点
结点的祖先:从根到该结点所经分支上的所有结点;如上图:A是所有结点的祖先
子孙:以某结点为根的子树中任一结点都称为该结点的子孙。如上图:所有结点都是A的子孙
森林:由m(m>0)棵互不相交的树的集合称为森林;

二、二叉树

一棵二叉树是结点的一个有限集合,该集合:
1. 或者为空
2. 由一个根结点加上两棵别称为左子树和右子树的二叉树组成

从上图可以看出:  

1. 二叉树不存在度大于2的结点

2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
注意:对于任意的二叉树都是由以下几种情况复合而成的:

三、特殊的二叉树

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

四、二叉树的顺序结构以及实现

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

五、堆的概念以及结构

堆就是以二叉树的顺序存储方式来存储元素,同时又要满足父亲结点存储数据都要大于儿子结点存储数据(也可以是父亲结点数据都要小于儿子结点数据)的一种数据结构。堆只有两种即大堆和小堆,大堆就是父亲结点数据大于儿子结点数据,小堆则反之。

堆的性质:
堆中某个节点的值总是不大于或不小于其父节点的值;
堆总是一棵完全二叉树。

六、堆的实现 

1、函数的声明

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	int size;//有效数据个数
	int capacity;//空间大小
}HP;
void Swap(HPDataType* p1, HPDataType* p2);//交换函数
void AdjustUp(HPDataType* a, int child);//向上调整
void AdjustDown(HPDataType* a, int n, int parent);//向下调整
void HPInit(HP* php);//初始化
void HPDestroy(HP* php);//销毁
void HPPush(HP* php, HPDataType x);//插入
void HPPop(HP* php);//删除树顶
HPDataType HPTop(HP* php);//树顶的数据
bool HPEmpty(HP* php);//判断是否为空

2、函数实现

a、初始化和销毁

void HPInit(HP* php)//初始化
{
	assert(php);
	php->a = NULL;
	php->capacity = php->size = 0;
}
void HPDestroy(HP* php)//销毁
{
	assert(php);
	free(php->a);
	php->a = NULL;
	php->capacity = php->size = 0;
}

b、获取堆顶元素和判断是否为空

HPDataType HPTop(HP* php)//树顶的数据
{
	assert(php);
	assert(php->a);
	assert(php->size > 0);
	return php->a[0];
}
bool HPEmpty(HP* php)//判断是否为空
{
	assert(php);
	return php->size == 0;
}

c、向上调整算法以及插入

向上调整即我们插入一个数据是要保持还是堆的形式(以小堆为例):

 父亲节点(n-1)/2,所以我们会有入下代码

void AdjustUp(HPDataType* a, int 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;
		}
	}
}

有了以上算法我们就可以轻易的写出插入函数

void HPPush(HP* php, HPDataType x)//插入
{
	assert(php);

	if (php->size == php->capacity)
	{
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a, newcapacity * sizeof(HPDataType));
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}

		php->a = tmp;
		php->capacity = newcapacity;
	}

	php->a[php->size] = x;//插入
	php->size++;
	AdjustUp(php->a, php->size - 1);//将要插入的数据进行向上调整
}

d、向下调整以及删除

删除是删除堆顶的数据,如果我们重新插入时间复杂的更高,这里就会用到向下调整的算法,我们会将堆顶数据和堆底数据进行交换,这样堆顶的左右数仍然是小堆,比较两个儿子之间的大小,与更小的那个交换(这样保证出来一定是小堆),循环即可。

左儿子为2*n+1:

void AdjustDown(HPDataType* a, int n, int parent)//向下调整
{
	// 先假设左孩子小
	int child = parent * 2 + 1;
	while (child<n)//最后一次交换后child最坏情况在堆底,如果写出parent<n则可能导致越界访问
	{
		if (a[child] > a[child + 1]&& child + 1 < n)//每一次都要找最小的所以放循环里,同理防止++后产生越界
		{
			child++;
		}
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

 有了以上算法我们可以很容易写出删除函数

void HPPop(HP* php)//删除树顶
{
	assert(php);
	assert(php->size > 0);
	Swap(&php->a[0], &php->a[php->size - 1]);//先交换
	php->size--;
	AdjustDown(php->a, php->size, 0);
}

以上是小堆的,要是想改成大堆,只需要将大小判断改一下就可以了。 

七、堆排序

法一:粗暴一点的方法,直接将一个一个数据插入堆中,然后依次取堆顶数据,取出来就是有顺序的。(大堆取出来是升序,小堆是降序)。但是这样空间复杂度为O(N)。(这里不进行演示)

方法二就是正经的堆排序算法。

堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1. 建堆
升序:建大堆
降序:建小堆
2. 利用堆删除思想来进行排序建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。

1、向上调整的方法

前面我们提到的插入是指往堆中插入,而这里是一种伪插入,即将最后的数据依次向上调整最后出来就是堆。

若建大堆则为向上调整,建小堆为向下调整(因为这里博主向上向下调整是以建小堆为前提所以这里是这样调整)。这里以降序为例。

实际上就是先认为该数组只有最后一个元素,往头部取插,那么前面的几个数据则一定是堆结束时堆建好 ,这样就避免了申请空间的问题。

当堆建好后,我们先将堆顶数据和堆底数据交换,在向下调整使其保持为堆,那么每次堆底数据为最小的,然后让倒数第二给交换,到结束使堆顶为最大数据,堆底为最小数据。

void AdjustUp(HPDataType* a, int 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;
		}
	}
void HeapSort(int* a, int n)
{
	// 降序,建小堆
	// 升序,建大堆
	for (int i = 1; i < n; i++)
	{
		AdjustUp(a, i);
	}
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}
void TestHeap2()
{
	int a[] = { 4,2,8,1,5,6,9,7,2,7,9 };
	HeapSort(a, sizeof(a) / sizeof(int));
}

假设每一层的数据都需要调整 

 

2、只用向下调整法

以如下堆为例

我们从每一个树出发,用向下调整法,将每一个树都变为大堆 ,即每个夫节点都向下调整,最后出来即为大堆。

void AdjustDown(HPDataType* a, int n, int parent)//向下调整
{
	// 先假设左孩子大
	int child = parent * 2 + 1;
	while (child<n)//最后一次交换后child最坏情况在堆底,如果写出parent<n则可能导致越界访问
	{
		if (a[child] < a[child + 1]&& child + 1 < n)//每一次都要找最大的所以放循环里,同理防止++后产生越界
		{
			child++;
		}
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
void HeapSort(int* a, int n)
{
	// 降序,建小堆
	// 升序,建大堆
	/*for (int i = 1; i < n; i++)
	{
		AdjustUp(a, i);
	}*/
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)//每次找父亲位置
	{
		AdjustDown(a, n, i);
	}

	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}
void TestHeap2()
{
	int a[] = { 4,2,8,1,5,6,9,7,2,7,9 };
	HeapSort(a, sizeof(a) / sizeof(int));
}


假设每一层的数据都需要调整  

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

suiyi_freely

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值