C语言 顺序表实现大堆数据结构

树、二叉树与堆

树的定义

树是n个节点的有限集。在任意一棵非空树中应满足:

(1)有且仅有一个称为根 root 的结点。

(2)当n>1时,其余结点可分为若干个互不相交的集合,且这些集合中的每一集合本身又是一棵树,称为根的子树。

从逻辑结构看

1)树中只有根结点没有前趋;

2)除根外,其余结点有且仅一个前趋

3)树中的结点,可以有零个或多个后继;

4)除根之外的其它结点,都存在唯一一条从根到该结点的路径;  

5)树是一种分支结构。

树的基本组成

结点的度(Degree):结点的子树个数
树的度:树的所有结点中最大的度数
叶结点(Leaf):度为 0 的结点
父结点(Parent):有子树的结点是其子树的根结点的父结点
子结点(Child):若 A 结点是 B 结点的父结点,则称 B 结点是 A 结点的子结点,也称孩子结点
兄弟结点(Sibling):具有同一父结点的各个结点彼此是兄弟结点。
路径长度:路径所包含边的个数
祖先结点(Ancestor):沿树根到某一结点路径上的所有结点都是这个结点的祖先结点
子孙结点(Descendant):某一结点的子树中的所有结点是这个结点的子孙
结点的层次(Level):规定根结点在 1 层,其他任一结点的层数是其父结点的层数加一
树的深度(Depth):树中所有结点中的最大层次是这棵树的深度。

森林是多个不相交得数得集合。

如图,这颗是根结点为A的树,树中所有结点中度最大的结点为A,D都是3所以树的度为3。树中的叶节点为:K,L,F,G,M,I,J。

A是B,C,D的父节点,B,C,D是A的子节点,B,C,D是兄弟结点。根结点A是所有结点(除了自己)的祖先结点,除了A结点的所有结点都是A结点的子孙结点。

由A到K的路径长度为3,由A到J的路径长度为2。若我们将根所在的层定义为第一层,那么第二层结点有{B,C,D},第三层结点有{E,F,G,H,I,J},第四层结点有{K,L,M}。因为树是个逻辑结构并不是实际的物理存储方式,所以即使我们将根节点的层定义为0也没问题,只是因为还有空树,所以一般都定义为1。树的深度就是最大层数4。

二叉树

二叉树继承树的所有定义外还有一个限制,树的度最大为二(可以为1或0)。满足此条件的树就是二叉树。

完全二叉树

完全二叉树是一种特殊的二叉树

树中除了最后一层其余层数的节点都是满的(最后一层也可以是满的),若树的深度为n那么树的结点数为:[2^(n-1),2^n-1]这个区间内。最后一层的节点数为:[0,2^(n-1)],而且最后一层的结点必须从左到右连续,中间不能有空节点。

满二叉树

满二叉树是一种特殊的完全二叉树

树的每一层的节点都是满的,若树的高度n那么这个二叉树的结点数为:结点总数=2^0+2^1+2^2……+2^(n-1)=2^n-1。

堆是一种特殊的完全二叉树,堆分两种:

大堆

大堆中所有的节点的数值都要比它的子孙节点大

小堆

小堆中所有的节点的数值都要比它的子孙节点小。

堆的实现工具

树只是一个逻辑上的数据存储方式并不是物理上的,所以我们实现堆用的还是得依靠我们得链表或者数组。一般得树由于没有特别得规律每个节点后都有可能链接着无数节点,如果用数组实现会很复杂需要给每一个节点都创建一个数组存储子节点得地址,并且很可能导致内存得浪费,所以一般得树我们都会使用链表实现,使用两个指针一个指向左子节点一个指向右边得兄弟节点。

二叉树是一种特别得树,与一般得树相比是有一些明确得限制得,二叉树中每个节点最多只能有两个子节点,但是结构也是没有特别得规律得,所以要用数组得话也是需要给每个节点定义一个包含两个指针得数组。或者将其看作满二叉树所有数据存在一个数组中空节对应得数组位置不存储数据,这样子也会大量浪费内存空间。所以一般二叉树我们也是用得链表会比较方便,每个结构体只需要包含左节点得指针和右节点得指针即可。

完全二叉树和满二叉树的话因为有着明确的结构所以我们都可以选择使用数组来存储,数组创建方便支持随机读取都是很不错的优势。而且在乱序的完全二叉树结构中,数据添加只需要对根或者尾数据进行处理,一般而言都是从尾部进行的,数组的尾插和尾删就十分方便。删除操作的话数组和链表的表现其实也相差无几。

堆我们其实可以将其看作是一个排序过后的完全二叉树所以我们这里将使用数组来实现但我们还会添加一些方便管理堆而创建的数据所有我们使用一个顺序表来完成对堆的底层实现。

堆的实现

使用数组存放堆的数据我们需要先定义好如何存储,堆是按照深度层次来划分的数据结构,所以我们需要在数组中明确定义每一层的节点在数组中的什么地方

如图这是我们一个排序好的堆的逻辑图,我们在数组中的存储方式是由上到下由左到右完成在数组的数据存放。如此定义的好处是我们每个节点的下标与子节点小标和父节点下标是有明确对应关系的,父节点的下标=子节点下标-1再除2(舍去余数),左子节点的下标等于父节点下标*2再加一,右字节点下标等于父节点下标*2再加二。

大堆与小堆逻辑结构上是相同的,只是排序的逻辑是相反的,我们这里试着实现大堆,小堆只需要在排序时反过来就可以了。

堆的定义和创建

typedef int HeapDate;//顺序表实现大堆
typedef struct Heap
{
	HeapDate* arr;
	int size;
	int capacity;
	
}heap;

这里我们定义一个结构体来实现对堆的管理,结构体里面包含一个数组指针用于指向存放堆数据的数组首地址,定义一个整形变量存放堆中的有效数据个数,一个整形变量存放数组的容量方便灵活管理堆的空间大小。

这里我们创建并初始化堆,先是开辟一个结构体类型的空间,然后开辟一个堆的数据类型的数组,初始我们设定为4个数据大小的空间,然后将结构体的数组指针指向数组的首地址将有效数据个数设为0,容量设为4完成创建。

堆内数据的排序

堆的排序就是一个比较的过程。向上层比较:若是我们插入的数据比它的父节点要大那么就需要与父节点的值交换让父节点始终都是最大的值。向下层比较:若我们插入的数据是比子节点数据要小就要与子节点交换数据。

向上比大:

默认堆里有大于1个的有效数据并是排好序的。

void CompareUp(heap* comp,int i)//向上比较并交换
{
	while (i)
	{
		if (comp->arr[(i - 1) / 2] <= comp->arr[i])
		{
			HeapDate j = comp->arr[(i - 1) / 2];
			comp->arr[(i - 1) / 2] = comp->arr[i];
			comp->arr[i] = j;

		}
		else
			return;
		i = (i - 1) / 2;
	}
}

这里我们传入结构体指针和需要排序的数据所在的下标。向上比较若我们值是最大的话就需要一直交换直到成为根节点,所以我们的循环结束条件是数据所在的下标为0。进入循环后我们进行与父节点的对比若比父节点大便交换,若父节点大这次向上比较就结束了,因为在大堆中父节点是最大的所以父节点的父节点肯定是比父节点要大。

向下比小:

默认堆里有大于1个的有效数据并是排好序的。

void CompareDown(heap* head,int count)//向下比较并交换第一个数据的下标
{
	int left = count * 2 + 1;
	int right = count * 2 + 2;
	HeapDate tmp = 0;
	while (head->size > left)
	{
		if (head->size < right)
		{
			if (head->arr[count] < head->arr[left])
			{
				tmp = head->arr[count];
				head->arr[count] = head->arr[left];
				head->arr[left] = tmp;
				return;
			}
		}
		else
		{
			if (head->arr[left] > head->arr[right])
			{
				if (head->arr[count] < head->arr[left])
				{
					tmp = head->arr[count];
					head->arr[count] = head->arr[left];
					head->arr[left] = tmp;
					count = left;
				}
				else
					return;
			}
			else
			{
				if (head->arr[count] < head->arr[right])
				{
					tmp = head->arr[count];
					head->arr[count] = head->arr[right];
					head->arr[right] = tmp;
					count = right;
				}
				else
					return;
			}
		}
		left = count * 2 + 1;
		right = count * 2 + 2;
	}
}

向下比较会有两个节点所以会比向上比较麻烦一些。

第一步我们先定义两个整形分别代表左孩子和右孩子的下标(数组才能实现的随机访问),数据类型的临时变量是我们在交换数据时使用的。然后我们进入循环这层循环是最外层循环,意味着这里循环结束的时候是已经完成了排序的,若比较的数据是最小的那我们需要一直对比直到最下层即到了没有子节点度为0的叶节点才完成,因为是完全二叉树我们只需要判断现在的有效数据个数是否大于节点的左子节点的下标即可。进入循环后可确定的是至少有左子节点,但是也有可能只有左子节点这说明左子节点就是堆中的最后一个数据了所以我在这里加了一个判断,只需要直接与其比较,比较完便完成了这次的向下比小了。

else
{
	if (head->arr[left] > head->arr[right])
	{
		if (head->arr[count] < head->arr[left])
		{
			tmp = head->arr[count];
			head->arr[count] = head->arr[left];
			head->arr[left] = tmp;
			count = left;
		}
		else
			return;
	}
	else
	{
		if (head->arr[count] < head->arr[right])
		{
			tmp = head->arr[count];
			head->arr[count] = head->arr[right];
			head->arr[right] = tmp;
			count = right;
		}
		else
			return;
	}
}

当跳过了对右子节点的判断后进入else分支的时候就说明节点是拥有左右两个子节点的,我们先是对左右子节点判断大小

若是左子节点较大我们再进行左子节点与父节点的比较,若是子节点大进行交换,若是小说明大小关系是符合大堆排序的,直接返回即可。

若是左子节点不比右子节点大我们进入分支进行右子节点与父节点的对比与上面同左子节点的对比一样。

我们左右子节点大小对比和与父节点的大小对比的分支语句出来之后若是排序没问题已经返回了,若是完成了父子节点的值交换接下来我们需要继续向下对比,这时我们将交换完的子节点定义为父节点继续与其子节点比较。直到到达最下一层没有子节点了。

堆内数据的增删查改

数据插入:

void PushHeap(heap* head,HeapDate n)//插入数据
{
	DilaHeap(head);
	head->arr[head->size] = n;
	head->size++;
	if (head->size == 1)
		return;
	CompareUp(head,head->size-1);
}

第一个是函数调用是关于数据空间检查容量和扩容的,再确定的容量足够之后我们开始插入操作,数组最方便的就是后插了,我们这里就进行了一个后插,然后判断若是插入后只有一个有效数据说明这个就是根节点不需要排序直接返回。若不是我们调用上面写好的向上比大函数,将结构体指针和新插入数据的下标传给函数即可。

数据删除:

void PopHeap(heap* head,HeapDate n)//删除数据
{
	if (head->size == 0)
		return;
	if (head->size == 1)
	{
		if(head->arr[0]=n)
		head->size--;
		return;
	}
	int count = 0;
	while (head->arr[count] != n)
	{
		count++;
	}
	head->arr[count] = head->arr[head->size - 1];
	head->size--;
	if(head->arr[count]>head->arr[(count-1)/2])
		CompareUp(head, count);
	else
		CompareDown(head, count);
	return;
}

进入函数我们还是先进行判断若是堆中没有有效数据直接返回,若是只有一个数据也是直接将代表有效数据个数的变量置0就可不需要排序。若有效数据大于2我们可能需要进行排序,这时我们定义一个计数器通过循环找出数据所在的下标数以定位数据位置,找到后我们不急着直接删除,因为若我们直接删除了数据那么这个位置的数据如何填补,若处在中上层位置很可能在向下寻找填补数据的过程中出现空节点导致不成为完全二叉树

所以我们对二叉树的数据进行删除和插入都需要从尾节点进行,这时我们将删除的数据与尾结点的数据进行交换,其实也不是交换直接使用尾结点的数据覆盖点需删除的数据即可。

这时完成了删除后的排序需要怎么进行呢,因为我们是不知道这个位置具体在哪里而且尾结点的数据不比父节点的大但不一定比父节点的表亲节点小,还可能比它要他大,所以这时我们需要对比

若是换了位置后比父节点要小我们直接进行向下比小,若是比父节点要大那肯定也比子节点要大就向上比大就可以了。

在修改数据数据的时候跟删除的思路差不多,都需要进行与父节点的对比,因为代码都差不多这里也不进行展示了。

查找的话直接循环返回下标即可。

堆的深度

int HighHeap(heap* head)//堆的高度,根为1
{
	if (head->size == 0)
		return 0;
	if (head->size == 1)
		return 1;
	int n = 0;
	int total = 1;
	while (total < head->size)
	{
		int j = 1;
		for (int i = 0; i < n-1; i++)
		{
			j *= 2;
		}
		total += j;
		n++;
	}
	return n;
}

堆的深度计算我们是基于最下层节点数为[0,2^(n-1)]区间,和满二叉树的节点总数2^n-1的思路去计算的,这里我们假定堆的深度为n,在设定一个辅助变量为totai,循环计算深度为:1,2,3,4,5,6各种满二叉树的节点总数,若是我们有效数据个数比这个total要大说明还有下一层,我们将n+1,直到total比size大的时候便可以得到深度。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值