【数据结构初阶】6. 树和堆

1. 树概念及结构

1.1 树的概念

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

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

任何一个结点都包含0~N个孩子结点,结点可以分成自身结点和子树(跟自己同样结构定义的树)两部分
例如:A结点可以分成自身A结点,B子树,C子树,D子树。

注意:树形结构中,子树之间不能有交集,否则就不是树形结构
在这里插入图片描述
上图的数据结构类型都不是树,而是图的概念

1.2 树的相关概念

在这里插入图片描述
节点的度:一个节点含有的子树的个数称为该节点的度; 如上图: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)棵互不相交的树的集合称为森林;

其中节点的层次是可以自己定义的,若如上所述:将根定义为第一层,那么树的深度/高度就是4 ,空树的深度为0;(更加符合日常生活中的认知) ,同样也可以将根定义为第0层,那么树的深度/高度就是3,空树的深度为-1;
拓展:为什么数组的下标要从0开始?
因为数组访问的是内存地址空间,例如,访问a[0],其实等价与*(a+0)
a表示数组首元素的地址,在首元素的地址上再加0 正好契合地址的访问

1.3 树的表示

树的表示方法多种多样:

1.3.1 静态数组

在这里插入图片描述
规定孩子结点的上限为N,当面对超过N个孩子结点时无法存储,而叶子结点没有孩子结点时,空间浪费严重,所以通常不采用

1.3.2 顺序表

在这里插入图片描述
通过struct TreeNode* 指向struct TreeNode的孩子结点,再创建一个数组存储这些struct TreeNode* ,所以是二级指针的形式
实现动态存储孩子结点

1.3.3 孩子兄弟表示法

typedef int DataType;
struct TreeNode
{
 struct TreeNode* child; // 左边第一个孩子结点
 struct TreeNode* brother; // 指向其下一个兄弟结点
 DataType data; // 结点中的数据域
};

在这里插入图片描述

在这里插入图片描述
child 指向左边的第一个孩子结点,而brother用于链接下一个兄弟结点
同一层的孩子结点都通过brother链接起来,而访问下一层就访问第一个孩子结点(child)开始
这种方式实现,树的结构可以还原出来,也不会出现环路的情况。

1.3.4 双亲表示法

在这里插入图片描述
通过数组的方式实现,单纯的存储父亲结点的下标,适合结点找祖先的情况

1.4 树在实际中的应用

在这里插入图片描述
通常用来表示操作系统中的文件系统的目录树结构

2. 二叉树概念及结构

2.1 概念

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

  1. 或者为空
  2. 由一个根节点加上两棵别称为左子树和右子树的二叉树组成 在这里插入图片描述
    从上图可以看出:
  • 二叉树不存在度大于2的结点
  • 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
    注意:对于任意的二叉树都是由以下几种情况复合而成的:
    在这里插入图片描述

2.2 现实中的二叉树:

在这里插入图片描述
左图为标准的满二叉树,右图为完全二叉树

2.3 特殊的二叉树:

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

3. 堆

gitee代码提交:堆的代码实现

3.1 堆的概念及结构

这里的堆指的也是数据结构中的一种,要和操作系统中的堆(指的是进程地址内存区域的划分)区分开

在这里插入图片描述

3.2 堆的代码实现(重点)

实现堆过程中出现的问题:在这里插入图片描述

01 堆结构体声明

typedef int HPDataType;

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

使用类似顺序表:动态开辟数组的方式实现堆

02 初始化接口

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

03 插入接口(重点)

要实现插入接口首先要保证插入数据后,还得保持堆的数据结构特征:小根堆:双亲<孩子 / 大根堆:双亲>孩子
这里就需要用到向上调整算法
在这里插入图片描述

// 交换两结点的值要传 HPDataType* 类型
// 更标准的写法
//void Swap(HPDataType* p1, HPDataType* p2)
//{
//	HPDataType tmp = *p1;
//	*p1 = *p2;
//	*p2 = tmp;
//}
void Swap(HPDataType* a, int parent, int child)
{
	int tmp = a[child];
	a[child] = a[parent];
	a[parent] = tmp;
}
// 向上调整算法
void AdjustUp(HP* php, int child)
{
	//向上和父节点进行比较
	int parent = (child - 1) / 2;
	//while (parent >= 0) 
	while (child > 0)
	{
		// 小跟堆(要实现大根堆将 > 换成 < 即可)
		if (php->a[parent] > php->a[child])
		{
			// 交换
			Swap(php->a, parent, child);
			// 往上遍历
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}
void HeapPush(HP* php, HPDataType x)
{
	assert(php);
	// 扩容
	if (php->capacity == php->size)
	{
		int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a, newCapacity * sizeof(int));
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}
		php->capacity = newCapacity;
		php->a = tmp;
	}
	// 1.插入结点
	php->a[php->size] = x;
	php->size++;
	// 2.调整
	AdjustUp(php, php->size - 1);
}

04 弹出接口(重点)

因为堆主要实现的是Topk(前k大/前k小)问题,所以弹出实现的是将堆顶元素(最小/最大元素)弹出
如果只是单纯的将数组往前覆盖可以实现嘛?
在这里插入图片描述
很明显无法采用,这里就需要采用向下调整算法

现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
int array[] = {27,15,19,18,28,34,65,49,25,37};
在这里插入图片描述
若要实现向下调整算法,要保证左右子树都为小根堆 / 大根堆

在这里插入图片描述

void AdjustDown(HP* php, int parent)
{
	// 找最小孩子
	// 假设左孩子为最小孩子
	int minchild = parent * 2 + 1;
	while (minchild < php->size)
	{
		//若 右孩子 < 左孩子
		if (php->a[minchild + 1] < php->a[minchild] && minchild + 1 < php->size)
		{
			// 最小孩子换成右孩子
			minchild = parent * 2 + 2;
		}
		//上述确定最小孩子
		if (php->a[parent] > php->a[minchild])
		{
			Swap(php->a, parent, minchild);
			parent = minchild;
			minchild = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
void HeapPop(HP* php)
{
	assert(php);
	assert(!HeapEmpty(php));

	Swap(php->a, 0, php->size - 1);
	php->size--;
	AdjustDown(php, 0);
}

05 销毁接口

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

06 判空接口

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

07 返回堆顶元素

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

08 打印接口

void HeapPrint(HP* php)
{
	assert(php);
	assert(!HeapEmpty(php));
	for (int i = 0; i < php->size; i++)
	{
		printf("%d ", php->a[i]);
	}
	printf("\n");
}

09 堆大小接口

int HeapSize(HP* php)
{
	assert(php);
	assert(!HeapEmpty(php));
	return php->size;
}

10 测试接口

void TestHeap()
{
	// 创建数组
	int a[] = { 65, 100, 70, 32, 50, 60 };
	HP hp;
	HeapInit(&hp);
	for (int i = 0; i < sizeof(a)/sizeof(a[0]); i++)
	{
		// 将整个数组插入进去
		HeapPush(&hp, a[i]);
	}
	HeapPrint(&hp);
	HeapPop(&hp);
	HeapPrint(&hp);
}

看打印的结果是否符合堆的数据结构特征
在这里插入图片描述
验证发现,符合!成功实现堆
其实我们发现通过实现堆也可以快速的实现堆排序:
在这里插入图片描述

4. 堆排序

如果在堆创建完毕的情况下,实现堆排序仅仅是几行代码,但是当我们遇到未建堆的数组,不可能通过现场实现堆的方式(代价太大:首先要实现堆数据结构,然后不断调用接口Push插入堆)
那么如何快速建堆呢?
gitee代码提交:堆排序的代码实现

4.1 建堆

在这里插入图片描述
这里采取的向下调整建堆,建小堆后只是保证堆顶的元素为最小值,子树还不是有序的

4.2 建堆的时间复杂度

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果):
在这里插入图片描述
在这里插入图片描述
所以向上调整和向下调整建堆,相对而言向下调整建堆更优

4.3 升序选大堆还是小堆

在这里插入图片描述
在这里插入图片描述
升序 – 大堆
降序 – 小堆

5. TopK问题

在这里插入图片描述
在这里插入图片描述
建堆选数:时间复杂度为O(N)的算法

TopK问题建小堆选数(文件版)的代码实现

gitee代码提交:Heap_TopK 堆解决TopK问题

01 创建N个随机数的Data文件

void CreateDataFile(const char* filename, int N)
{
	//写入
	FILE* fin = fopen(filename, "w");
	if (fin == NULL)
	{
		perror("fopen fail");
		exit(-1);
	}
	//随机种子
	srand((unsigned int)time(NULL));
	for (int i = 0; i < N; i++)
	{
		//输入N个随机数
		fprintf(fin, "%d\n", rand() % 10000);
		// 所有数在0~10000 内 再手动插入10个大于10000的数 看选数过后是否选的出来
	}
	fclose(fin);
}
int main()
{
	const char* filename = "Data.txt";
	//从N个数当中选出前K大
	int N = 10000;
	int K = 10;
	// 创建N个数的文件
	CreateDataFile(filename, N);
	return 0;
}

在这里插入图片描述
该函数只能调用一次,因为是“w”的形式打开文件,若文件有内容会进行删除重写

02 读取写入Data当中的数据

void PrintTopK(const char* filename, int K)
{
	assert(filename);
	//读取数据
	FILE* fout = fopen(filename, "r");
	if (fout == NULL)
	{
		perror("read fail");
		exit(-1);
	}

以只读的方式打开filename == “Data.txt” 的文件

03 开辟K个空间建堆

	//将前K个数据取出
	int* minHeap = (int*)malloc(sizeof(int) * K);
	if (minHeap == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	
	// 将文件前K个数插入minHeap
	for (int i = 0; i < K; i++)
	{
		fscanf(fout, "%d", &minHeap[i]);
	}

	//将K个数建堆 (K-2)/2 最后一个叶子结点
	for (int j = (K - 2) / 2; j >= 0; j--)
	{
		//向下调整建堆
		AdjustDown(minHeap, K, j);
	}

04 选N-K次数

	//继续读取后N-K
	int val = 0;
	while (fscanf(fout, "%d", &val) != EOF)
	{
		// 选出比当前堆大的元素 放入堆中
		if (val > minHeap[0])
		{
			minHeap[0] = val;
			AdjustDown(minHeap, K, 0);
		}
	}

经过前几步的操作,当前 minHeap 内数据为前K个最大数

05 将堆内数据打印

	for (int i = 0; i < K; i++)
	{
		printf("%d ", minHeap[i]);
	}
	free(minHeap);
	fclose(fout);
}

在这里插入图片描述

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值