实现“顺序结构二叉树”

文章目录

  • [一. 树](#一. 树)
  • [二. 二叉树](#二. 二叉树)
    • [2.1 概念](#2.1 概念)
    • [2.2 满二叉树](#2.2 满二叉树)
    • [2.3 完全二叉树](#2.3 完全二叉树)
    • [2.4 二叉树的性质](#2.4 二叉树的性质)
    • [2.5 二叉树的存储结构](#2.5 二叉树的存储结构)
    • [2.5.1 顺序结构](#2.5.1 顺序结构)
    • [2.5.2 链式结构](#2.5.2 链式结构)
  • [三. 实现顺序结构二叉树](#三. 实现顺序结构二叉树)

一. 树

1. 树的概念

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

  1. 有一个特殊的结点,称为根结点,根结点没有前驱结点
  2. 除根结点外,其余结点被分成 M (M>0) 个互不相交的集合 T1、T2、......、Tm。其中每一个集合 又是一棵结构与树类似的子树 。每棵子树的根结点有且只有一个前驱,可以有 ≥0 个后继。因此,树是递归定义的。
  3. 子树是不相交的。(相交的话是图)
  4. 除根结点外,每个结点有且只有一个父结点。
  5. 一棵N个结点的树有N-1条边。(忽略图中的箭头)

父结点/双亲结点:若一个结点含有子结点,则这个结点称为其子结点的父结点;

子结点/孩子结点:一个结点含有的子树的根结点称为该结点的子结点;

兄弟结点:具有相同父结点的结点互称为兄弟结点(亲兄弟);

结点的度:这个结点有几个孩子,他的度就是多少;

树的度:一棵树中,最大的结点的度 称为树的度;

叶子结点/终端结点:度为 0 的结点;

分支结点/非终端结点:度不为 0 的结点;

结点的层次:从根开始定义起,根为第 1 层,根的子结点为第 2 层,以此类推;

树的高度或深度:树中结点的最大层次;

结点的祖先:(从根到该结点)所经分支上的所有结点;

路径:一条从树中任意结点出发,沿父结点-子结点连接,达到任意结点的序列;

子孙:以某结点为根的子树中任一结点都称为该结点的子孙。

森林:由 m(m>0) 棵互不相交的树的集合称为森林;

2.树的表示

树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间的关系。

孩子兄弟表示法:

cpp 复制代码
struct TreeNode
{
struct Node* child; // 左边开始的第⼀个孩⼦结点
struct Node* brother; // 指向其右边的下⼀个兄弟结点
int data; // 结点中的数据域
};

二. 二叉树

2.1 概念

二叉树是树的一种,二叉树是在树的基础上添加了一些规则。

在树形结构中,我们最常用的就是二叉树,一棵二叉树是结点的一个有限集合(集合是什么呢?集合由一个根结点 加上两棵别称为左子树右子树的二叉树组成或者为空)

二叉树的特点:

1.二叉树不存在度大于 2 的结点(度是孩子结点的个数)

2.二叉树的子树有左右之分,次序不能颠倒 ,因此二叉树是有序树

对于任意的二叉树都是由以下几种情况复合而成的

2.2 满二叉树

一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。(层数是k,则该层的结点数是2^(k-1)^个

结点总数是每一层的加起来(是等比数列),结果是2^k^-1

2.3 完全二叉树

完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。满二叉树是特殊的完全二叉树,当完全二叉树的最后一层的结点数是最大结点数,即2^k-1^,则为满二叉树。

对于深度(层数)为 K 的,有 n 个结点的二叉树,当且仅当其每一个结点都与 深度为K的满二叉树中编号从 1 至 n 的结点一一对应时,称之为完全二叉树。

再解释一下完全二叉树:假设二叉树的层次是k,除了最后一层(第k层),其余每层的结点个数都达到了最大结点数,且第k层的结点的顺序是从左到右,这种二叉树叫做 "完全二叉树"。

2.4 二叉树的性质

由满二叉树推理而得的二叉树的性质:

若规定根结点的层数为1

  • 非空二叉树的第i层最多有2^i-1^个结点
  • 深度为h的非空二叉树的最大结点数是2^h^-1
  • 具有 n 个结点的满二叉树的深度

2.5 二叉树的存储结构

2.5.1 顺序结构

顺序结构存储就是使用数组来存储 ,⼀般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费

我们不能直接存右图的A,B,C,D,E,F,这样会打乱顺序,之后没法通过公式找到某个结点。

2.5.2 链式结构

二叉树的链式存储结构:用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。

链表中每个结点由三个域组成:数据域和左右指针域(左右指针:该结点的左孩子和右孩子所在的链结点的存储地址)

链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链。后面课程学到高阶数据结构如红黑树等会用到三叉链。

三. 实现顺序结构二叉树

3.1 堆的概念和结构

在二叉树里面,还有一个特殊的二叉树:堆

  1. 堆:通常使用顺序结构来存储数据
  2. 堆:具有⼆叉树的特性的同时,还具备其他的特性。

堆的性质:

  1. 堆中某个结点的值≤或≥根结点。(分为大小堆)
  2. 堆总是一颗完全二叉树。(从左到右的顺序)

小堆(小根堆):根结点的值最小的堆-------->叫做小根堆(小堆的堆顶值是min的)

大堆(大根堆):根结点的值最大的堆-------->叫做大根堆(大堆的堆顶值是max的)

3.2 堆的性质

对于具有 n 个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从0开始编号,则对于序号为 i 的结点有:

  1. 若i=0,则 i 为根结点的序号(没有父结点)
  2. 若i > 0,i 这个位置的结点的父结点序号是:(i-1)/2;左孩子结点的序号:2i +1;右孩子结点序号:2i +2
  3. 左孩子序号<结点个数(2i+1<n),则有左孩子;右孩子序号<结点个数(2i+2<n),则有右孩子;
  4. 左孩子序号>=结点个数(2i+1>=n),则没有左孩子;右孩子序号>=结点个数(2i+1>=n),则没有右孩子;

10是根结点,序号是0,只有孩子结点。i=0,左孩子结点:0*2+1=1,右孩子结点:0 *2+2=2.

3.3 堆的实现

堆底层结构为数组

堆的初始化和销毁

cpp 复制代码
//Heap.h
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
//定义堆的结构---数组
typedef int HPDataty;
typedef struct Heap
{
	HPDataty* arr;
	size_t size;  //有效个数
	size_t capacity;  //空间大小
}HP;

//堆的初始化
void HPInit(HP* php);
void HPDestroy(HP* php);
cpp 复制代码
//Heap.cpp
#include"Heap.h"
void HPInit(HP* php)
{
	assert(php);
	/*HPDataty* arr1=(HPDataty*)malloc(sizeof(HPDataty));
	if (arr1 == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}*/
	php->arr = NULL;
	php->size = php->capacity = 0;
}
void HPDestroy(HP* php)
{
	assert(php);
	if (php->arr != NULL)
	{
		free(php->arr);
	}
	php->arr = NULL;
	php->size = php->capacity = 0;
}

向堆里插入数据

cpp 复制代码
void HPPush(HP* php, HPDataty x)
{
	assert(php);
	//判断空间是否足够
	if (php->size == php->capacity)
	{
		//扩容
		size_t newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
		HPDataty* tmp = (HPDataty*)realloc(php->arr, newcapacity * sizeof(HPDataty));
		if (tmp == NULL)
		{
			perror("realloc fail!");
			exit(1);
		}
		php->arr = tmp;
		php->capacity = newcapacity;
	}
	//空间足够,添加数据
	php->arr[php->size] = x;
	php->size++;
}

重点:插入数据之后,并不代表这个数据就是堆了。堆它还有其它要求呢,根结点的值最大/最小;父结点的值≥或≤孩子结点。

以小堆为例:

新插入的数据肯定是孩子结点,将arr[孩子]和arr[父结点]比较。

堆的向上调整算法

从最后一个插入的数据arr[size-1]开始,最后一个数据只可能是孩子结点,将children=size-1。

将这个孩子结点和它的父结点arr[(children-1)/2]比较,如果孩子结点<父结点,将其交换。循环往上走,直到父结点=0.

cpp 复制代码
void Swap(int* x, int* y)
{
	int t = *x;
	*x = *y;
	*y = t;
}
void AdjustUp(HPDataty* arr,int child)
{
	int parent = (child - 1) / 2;
	while (child>0)   //当孩子结点是0时,那就是已经走到根结点了,父结点已,越界无需再向上比较
	{
		if (arr[child] < arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			child = parent;
			parent = (child- 1) / 2;
		}
		else
		{
			break;
		}
	}
}
void HPPush(HP* php, HPDataty x)
{
	assert(php);
	//判断空间是否足够
	if (php->size == php->capacity)
	{
		//扩容
		int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
		HPDataty* tmp = (HPDataty*)realloc(php->arr, newcapacity * sizeof(HPDataty));
		if (tmp == NULL)
		{
			perror("realloc fail!");
			exit(1);
		}
		php->arr = tmp;
		php->capacity = newcapacity;
	}
	//空间足够,添加数据
	php->arr[php->size] = x;
	php->size++;
	AdjustUp(php->arr,php->size-1);
}
void Print(HP* php)
{
	int i = 0;
	while (i < php->size)
	{
		printf("%d ", php->arr[i++]);
	}
}

删除堆里的数据

删除堆里的数据,删除的是堆顶的数据。

但是如果直接将堆顶的数据删除,接下来的堆就没有堆顶了。

我们可以将根结点和最后一个结点交换,然后将最后一个结点删除掉,那接下来的堆就不是有效的,我们需要将数据再次排序调整----->向下调整算法

向下调整算法

要将根结点和孩子结点比较,【比较左右孩子结点的值,找出那两个孩子中 最小的孩子结点】,将那个小的孩子结点和父结点比较,若是父结点比 (那个小的孩子结点) 大,则将它们两个交换,那么此时的根结点放的就是最小的值了。循环往下...

接下来的一个重点是:那这个循环什么时候停止呢?

取堆顶数据

cpp 复制代码
HPDataty HPTop(HP* php)
{
	assert(php && php->size);
	return php->arr[0];
}

判空

cpp 复制代码
bool HPEmpty(HP* php)
{
	assert(php);
	return php->size==0;
	//如果size不等于0,堆不是空的,那么返回假。 !HPEmpty就是真
}

可以将判空用于读取堆里的所有数据。

先读取堆顶数据,然后将堆顶数据删除,下一次又可以读堆顶数据。那循环什么时候停止呢?当数据是空的时候,这时候判断条件就可以用判空那个函数了。

cpp 复制代码
while (!HPEmpty(&hp))
{
	printf("%d ", HPTop(&hp));
	HPPop(&hp);
}
相关推荐
敲上瘾5 分钟前
操作系统的理解
linux·运维·服务器·c++·大模型·操作系统·aigc
不会写代码的ys12 分钟前
【类与对象】--对象之舞,类之华章,共绘C++之美
c++
兵哥工控14 分钟前
MFC工控项目实例三十二模拟量校正值添加修改删除
c++·mfc
长弓聊编程24 分钟前
Linux系统使用valgrind分析C++程序内存资源使用情况
linux·c++
cherub.31 分钟前
深入解析信号量:定义与环形队列生产消费模型剖析
linux·c++
布说在见41 分钟前
个人实施工作的一天 —— 繁琐的数据输入与未来的句里录数据
经验分享·实习实施
暮色_年华1 小时前
Modern Effective C++item 9:优先考虑别名声明而非typedef
c++
重生之我是数学王子1 小时前
QT基础 编码问题 定时器 事件 绘图事件 keyPressEvent QT5.12.3环境 C++实现
开发语言·c++·qt
梅见十柒1 小时前
wsl2中kali linux下的docker使用教程(教程总结)
linux·经验分享·docker·云原生
我们的五年1 小时前
【Linux课程学习】:进程程序替换,execl,execv,execlp,execvp,execve,execle,execvpe函数
linux·c++·学习