【DS】C语言实现堆(动画演示)

本文详细介绍了二叉树的基础概念,特别是完全二叉树和满二叉树,随后重点讲解了堆的数据结构,包括堆的概念、性质、插入和删除数据的调整策略,以及堆的初始化、销毁和基本操作。
摘要由CSDN通过智能技术生成

二叉树相关介绍

由于堆总是一棵完全二叉树,介绍堆前需要对二叉树有一定了解。所以先进行二叉树的简要介绍。

树的概念

树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因
为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的

  • 有一个特殊的结点,称为根结点,根节点没有前驱结点:如下图的A节点
  • 除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i <= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继
  • 因此,树是递归定义的。

也就是说树由两部分组成:父亲节点和N颗子树,每颗树都可以分割为父亲节点和左右子树。

树的介绍
树的相关概念

  • 节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A节点的度为3。
  • 双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点。
  • 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点。
  • 兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B,C是兄弟节点
  • 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推。
  • 树的高度或深度:树中节点的最大层次;如上图:树的高度为4。

以上就是需要了解的常用的树的概念,下面是二叉树的介绍

二叉树的概念及结构

一棵二叉树是结点的一个有限集合,该集合:

  1. 或者为空
  2. 由一个根节点加上两棵别称为左子树和右子树的二叉树组成

二叉树介绍
从上图可以看出:

  1. 二叉树不存在度大于2的结点
  2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

对于任意的二叉树都是由以下几种情况复合而成的:
二叉树介绍
特殊的二叉树
介绍堆时已经提到了堆是一颗完全二叉树,下面正式介绍完全二叉树

满二叉树
提到完全二叉树又不得不提满二叉树

一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是2^k-1 ,则它就是满二叉树。

完全二叉树

完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。
对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。

在这里插入图片描述

堆的概念及结构

对二叉树有相关了解之后,正式开始堆的介绍
堆的性质

  • 堆中某个节点的值总是不大于或不小于其父节点的值;
    • 父节点总是大于等于孩子节点的叫大堆
    • 父节点总是小于等于孩子节点的叫小堆
  • 堆总是一棵完全二叉树。
    堆图示
    只有符合以上条件的才是堆。

父子下标关系
堆是用数组实现的,物理结构是数组,逻辑结构是一颗二叉树,其节点间下标的关系可用父子关系表示。注意:以下的都是下标关系

  • 父亲下标找孩子
    • leftchild=parent*2+1
    • rightchild=parent*2+2
  • 孩子下标找父亲
    • parent=(child-1)/2
    • 由于计算机整型的除法的结果只保留整型,所以左右孩子用下标找父节点都可以用一个公式计算

可以对照下图自己感悟,接下来就是堆的实现。

父子下标关系

堆的结构

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}HP;

堆的物理结构为数组,所以依旧采用动态数组来实现堆

  • 一个指向数组的指针
  • 记录堆中元素个数的size
  • 检查数组是否还有空间的capacity

创建堆的最重要部分就是插入数据,删除数据时对堆的调整

插入数据

void HPPush(HP* php, HPDataType x)
{
	assert(php);
	if (php->capacity == php->size)
	{
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		int* tmp = (int*)realloc(php->a, sizeof(HP) * newcapacity);
		if (tmp == NULL)
		{
			perror("realloc failed");
			return;
		}
		php->a = tmp;
		php->capacity = newcapacity;
	}
	php->a[php->size++] = x;
	AdjustUp(php->a, php->size-1);//传的是下标
}

再来回顾一下堆的特性

  • 堆总是一颗完全二叉树
  • 堆分为大堆和小堆
    • 父节点总是大于等于孩子节点的叫大堆
    • 父节点总是小于等于孩子节点的叫小堆
      堆图示

堆创建的思路

以建大堆为例,由于现在是尾插,要保证每次插入数据后仍为大堆,那就需要将插入的数据与堆中的数据比较,如果插入的数据大于堆中的数据,我们就需要进行向上调整AdjustUp堆在逻辑上是一颗完全二叉树,所以在调整堆的时候就需要用到前面提到的父子间下标关系公式来进行数据的比对

AdjustUp
由该函数来实现向上调整堆:当我们每次尾插数据时,数组已经是一个堆了,所以只需要在对应的左子树或右子树向上调整即可。

  • 函数参数有两个:一个是用来接受堆中的数组的指针,另一个用来接受数组元素个数的
void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

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

示例

以插入一个80为例,此时80插入到数组下标为6的位置,也就是逻辑结构中30的右边。此时80作为30的孩子,30为80的父亲。

向上调整动图

调整过程

一进入AdjustUp,用孩子80的下标通过父子下标关系公式找到80的父亲——30对应的下标。现在是建大堆,所以将大的元素往上调整,80比30大,交换他们的位置,完成一次调整。

但调整还并没有结束,需要继续向上比对。此时80又变为了70的孩子,又需要比对这对父子,所以将父子下标更新,让80变为70的孩子,70变为80的父亲,然后又进行上述的操作。
终止条件
上述调整是在循环中进行的,父亲的最后下标为0,也就是根的位置。当父亲下标为0时已经是最后一次调整了,也就是说child的下标永远不能等于0,所以循环的终止条件就是child>0。上述的是一种情况,还有一种情况就是当孩子不大于父亲时,也就是走了esle条件直接跳出循环。这是因为在尾插数据时,数组已经是堆,遵从大堆的性质。这同时也是每次循环为什么只调父子关系,而不用调兄弟关系的原因。

删除数据

删除数据是指删除根节点的数据

void HPPop(HP* php)
{
	assert(php);
	assert(!HPEmpty(php));//判空,不空才能删
	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;
	AdjustDown(php->a, php->size, 0);
}

思路
如果我们直接挪动覆盖来删除根位置的数据时,我们又需要去第二层选出新的根,那这样会有什么什么问题呢?

这样的操作可能会将原本的堆彻底破坏,并且父子关系也会被打乱,导致父子变兄弟,兄弟变父子。这就需要重新彻底建堆。当然也有可能删除之后仍然是堆的情况,但只是偶然,所以这种方法是不可取的。

我们要利用此时已经时堆的这个条件。所以,我们的思路是先将根的值与最后一个位置的值交换,再将size–达到删除的目的,而此时堆中除了根位置,其他位置的父子关系都没有变,我们只需要将根向下调整一轮即可完成堆的调整,这样的代价时非常小的,后面会进行证明。

AdjustDown

  • 向下调整这个函数参数有:一个指向数组的指针,元素个数,父节点的下标
void AdjustDown(int* a, int n, int parent)//n为个数。parent为下标
{
	int 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;
		}
	}
}

示例
直接用刚才插入了80的大堆
向下调整动图

调整过程
进入该函数,先计算出孩子的下标,这里默认算出左孩子的下标,但还需要将左右孩子进行比对选出合适的值,以大堆为例,左右孩子谁大谁就可能是新的根(注意只有左孩子没有右孩子的情况),之后就是与向上调整类似的逻辑,孩子大于父亲则交换他们的位置,然后继续向下比对

终止条件
直到孩子越界了,就终止循环。还有就是走了else条件,因为此时的堆还是遵从大堆原则。

堆的简易接口

这些接口过于简单,这里就一笔带过了

堆的初始化

void HPInit(HP* php)
{
	assert(php);
	php->a = NULL;
	php->capacity = php->size = 0;
}

将指针置为NULL,并把size和capacity置为0。

堆的销毁

void HPDestroy(HP* php)
{
	assert(php);
	free(php->a);
	php->a = NULL;
	php->capacity = php->size = 0;
}

将在堆区开辟的数组free掉,防止内存泄漏,再将指针置为NULL,size和capacity置为0。

堆中元素个数

int HPSize(HP* php)
{
	assert(php);
	return php->size;
}

函数返回类型为int,将size返回。

判断堆是否为空

bool HPEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}

该函数一般配合assert使用,返回值为布尔类型,具体使用方法请看判断栈是否为空

返回堆顶元素

HPDataType HPTop(HP* php)
{
	assert(php);
	assert(!HPEmpty(php));
	return php->a[0];
}

先判断堆是否为空,不为空才能返
堆定即数组的第一个元素,所以返回**php->a[0]**即可

结语

这篇博客主要介绍了堆的相关知识,对堆进行了一定介绍,并进行了堆的模拟实现。接下来还会对堆的应用即堆排序进行介绍和堆的相关时间复杂度进行证明。

完整代码

分为三个文件

Heap.h

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
#include<assert.h>

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}HP;

void AdjustUp(int* a, int child);//push时向上调整
void AdjustDown(int* a, int n,int parent);//pop时向下调整

void HPInit(HP* php);//初始化
void HPPush(HP* php,HPDataType x);//插入数据
void HPPop(HP* php);//删除数据
HPDataType HPTop(HP* php);//返回堆定元素
int HPSize(HP* php);//堆中元素个数
bool HPEmpty(HP* php);//判空
void HPDestroy(HP* php);//销毁

Heap.c

#define _CRT_SECURE_NO_WARNINGS 1 
#include"Heap.h"

void HPInit(HP* php)
{
	assert(php);
	php->a = NULL;
	php->capacity = php->size = 0;
}

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void AdjustUp(int* 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->capacity == php->size)
	{
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		int* tmp = (int*)realloc(php->a, sizeof(HP) * newcapacity);
		if (tmp == NULL)
		{
			perror("realloc failed");
			return;
		}
		php->a = tmp;
		php->capacity = newcapacity;
	}
	php->a[php->size++] = x;
	AdjustUp(php->a, php->size-1);
}

void AdjustDown(int* a, int n, int parent)//n为个数。parent为下标
{
	int 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 HPPop(HP* php)
{
	assert(php);
	assert(!HPEmpty(php));
	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;
	AdjustDown(php->a, php->size, 0);
}

HPDataType HPTop(HP* php)
{
	assert(php);
	assert(!HPEmpty(php));
	return php->a[0];
}

int HPSize(HP* php)
{
	assert(php);
	return php->size;
}

bool HPEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}

void HPDestroy(HP* php)
{
	assert(php);
	free(php->a);
	php->a = NULL;
	php->capacity = php->size = 0;
}

Test.c

#define _CRT_SECURE_NO_WARNINGS 1 
#include"Heap.h"

void HPPrint(HP* php)
{
	//printf("HPTop: %d\n",HPTop(php));
	//printf("HPSize: %d\n", HPSize(php));

	while (!HPEmpty(php))
	{
		int top = HPTop(php);
		HPPop(php);
		printf("%d ", top);
	}
	printf("\n");
}

void HPTest()
{
	HP hp;
	HPInit(&hp);
	HPPush(&hp, 0);
	HPPush(&hp, 4);
	HPPush(&hp, 1);
	HPPush(&hp, 3);
	HPPush(&hp, 2);
	//HPPop(&hp);

	HPPrint(&hp);
	
	HPDestroy(&hp);   
}

int main()
{
	//HPTest();

	return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值