🧑💻作者: @情话0.0
📝专栏:《数据结构》
👦个人简介:一名双非编程菜鸟,在这里分享自己的编程学习笔记,欢迎大家的指正与点赞,谢谢!
二叉树(上)
- 前言
- 一、树的基本概念
- 1.树的定义
- 2.树的基本术语
- 3.树的表示
- 二、二叉树
- 1.二叉树的定义及主要性质
- 1.1二叉树的定义
- 1.2几个特殊的二叉树
- 1.3二叉树的性质
- 2.二叉树的存储结构
- 2.1顺序存储结构
- 2.2链式存储结构
- 三、二叉树的顺序存储实现
- 1.堆的概念及结构
- 2. 堆的实现
- 2.1 堆向下调整算法
- 2.2 堆的创建
- 2.3 堆的插入
- 2.4 堆的删除
- 总结
前言
本文将主要对树、二叉树的相关概念和性质展开讲解,同时用代码实现一种特殊的二叉树(堆)的顺序存储实现。
一、树的基本概念
1.树的定义
树是一种非线性的数据结构,它是由 n(n >= 0)个有限结点组成一个具有层次关系的集合。当 n = 0 时,称为空树。
① 有且仅有一个特殊的点称为根结点,根结点没有前驱结点;
② 当 n > 1 时,其余结点可分为 m(m > 0)个互不相交的有限集 T1、T2、……、Tm,其中每个集合本身又是一棵树,并且称为根的子树。
很明显,树的定义是递归的,即再树的定义中又用到了其自身,树是一种递归的数据结构。树作为一种逻辑结构,同时也是一种分层结构,具有以下两个特点:
① 树的根结点没有前驱,除根结点外的所有的结点有且只有一个前驱结点;
② 树中所有结点可以有零个或多个后继结点。
注意: 树适合于表示具有层次结构的数据。树中的某个结点(除根结点外)最多只和上一层的一个结点(其父节点)有直接关系,根结点没有直接上层结点,因此在 n 个结点的树中有 n-1 条边。而树中的每个结点与其下一层的零个或多个节点有直接关系。也就是说,树形结构中,子树之间不能有交集,否则就不是树形结构。
2.树的基本术语
结点的度:树中一个结点的孩子个数被称为该结点的度,如上图:A结点的度为6;
树的度:树中结点最大的度数称为树的度,如上图:树的度为6;
叶子结点:树中度为0的结点被称为叶子结点(或者终端结点),如上图: B C H I P Q K L M N都为叶子结点;
分支节点:度大于0的结点被称为分支节点(或者非终端结点),如上图: D E J F G;
孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点
兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点
树的高度、深度:结点的层次从树根开始定义,根结点在第一层,它的子结点在第二层,以此类推。结点的高度是从叶结点开始自底向上逐层累加的;结点的深度是从根结点开始自顶向下逐层累加的。树的高度(或深度)是树中结点的最大层数,如上图树的高度为 4。
堂兄弟节点:双亲在同一层的结点互为堂兄弟,如上图:H、I互为兄弟结点。
结点的祖先:从根到该结点所经分支上的所有结点,如上图:A是所有结点的祖先。
子孙:以某结点为根的子树中任一结点都称为该结点的子孙。如上图:所有结点都是A的子孙。
森林:由m(m>0)棵互不相交的树的集合称为森林;
3.树的表示
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既要保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。我们这里就简单的了解其中最常用的孩子兄弟表示法。
typedef int DataType;
struct Node
{
struct Node* _firstChild1; // 第一个孩子结点
struct Node* _pNextBrother; // 指向其下一个兄弟结点
DataType _data; // 结点中的数据域
};
二、二叉树
1.二叉树的定义及主要性质
1.1二叉树的定义
二叉树是一种特殊的树,其特点就是每个结点最多有两棵子树(即二叉树中不存在度大于2的结点),二叉树是 n (n>=0)个结点的有限集合:
①:或者为空二叉树,即 n = 0;
②:或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成,左子树和右子树有分别是一棵二叉树。
二叉树是有序树,若将其左右子树颠倒,则成为另一棵不同的二叉树。即树中结点只有一棵子树,也要区分它是左子树还是右子树。对于任意的二叉树都是由以下几种情况复合而成的:
二叉树与度为 2 的有序树的区别:
①:度为 2 的树至少有 3 个结点,而二叉树可以为空;
②:度为 2 的有序树的孩子的左右次序是相对于另一个孩子而言的,若某个结点只有一个孩子,则这个孩子就无需区分其左右次序,而二叉树无论孩子数是否为 2 ,均需要确定其左右次序,即二叉树的结点次序不是相对于另一个结点而言的,而是确定的。
1.2几个特殊的二叉树
1)满二叉树:一个二叉树如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是 (2^k) - 1 ,则它就是满二叉树。
2)完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树(也就是说,前 k-1 层是满的,第 k 层从左到右的叶子结点是连续的)。 要注意的是满二叉树是一种特殊的完全二叉树。
1.3二叉树的性质
- 若规定根节点的层数为1,则一棵非空二叉树的第 k 层上最多有 2^(k-1)个结点;
- 若规定根节点的层数为1,则深度为 h 的二叉树的最大结点数是 (2^h)-1;
- 对任何一棵二叉树, 如果度为 0 其叶结点个数为 n0 , 度为 2 的分支结点个数为 n2,则有 n0= n2+1;
证明:设度为0,1和2的结点个数分别为n0,n1和n2,结点总数n=n0+n1+n2。再看二叉树中的分支数,除根结点外,其余结点都有一个分支进入,设B为分支总数,则n=B+1。由于这些分支是由度为1或2的结点射出的,所以又有B=n1+2n2。于是得n0+n1+n2=n1+2n2+1,则n0=n2+1。
- 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h= log2(n+1);
- 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:
①:若 i > 0,i 位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点;
②:若 2i+1 < n,左孩子序号:2i+1;若 2i+1>=n否则无左孩子;
③:若 2i+2 < n,右孩子序号:2i+2;若 2i+2>=n否则无右孩子。
2.二叉树的存储结构
2.1顺序存储结构
二叉树的顺序存储是指用一组地址连续的存储单元依次自上而下、自左而右存储二叉树上的结点。根据二叉树的性质,完全二叉树和满二叉树采用顺序存储比较合适,树中结点的序号可以唯一的反映结点之间的逻辑关系,这样既能最大可能节省存储空间,又能利用数组元素的下标值确定结点在二叉树的位置,以及结点之间的关系。但对于一般的二叉树,为了让数组下标能反映出二叉树中结点之间的逻辑关系,只能添加一些并不存在的空结点,让其每个结点与完全二叉树上的结点相对照,再存储到一维数组的相应分量中。
2.2链式存储结构
因为顺序存储的空间利用率较低,因此二叉树一般采用链式存储结构,二叉树的链式存储结构是指用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。
typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
struct BinTreeNode* lchild; // 指向当前节点左孩子
struct BinTreeNode* rchild; // 指向当前节点右孩子
BTDataType data; // 当前节点值域
};
// 三叉链
struct BinaryTreeNode
{
struct BinTreeNode* Parent; // 指向当前节点的双亲
struct BinTreeNode* _lchild; // 指向当前节点左孩子
struct BinTreeNode* _rchild; // 指向当前节点右孩子
BTDataType data; // 当前节点值域
};
三、二叉树的顺序存储实现
1.堆的概念及结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储。
对于一个连续数组,把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足所有的父亲结点都大于(小于)它的孩子结点,则称为大堆(或小堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
堆中某个节点的值总是不大于或不小于其父节点的值;
堆总是一棵完全二叉树。
2. 堆的实现
2.1 堆向下调整算法
假若现在给你一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
int array[] = {27,15,19,18,28,34,65,49,25,37};
void Swap(HPDataType* left, HPDataType* right)
{
HPDataType temp = *left;
*left = *right;
*right = temp;
}
void AdjustDown(Heap* hp,int n,int parent)
{
int child = parent * 2 + 1; //调整结点的左孩子
while (child < n) //当child大于结点个数时调整完毕
{
//判断是否有右孩子并且右孩子大于左孩子
if (child + 1 < n&&hp->array[child] < hp->array[child + 1])
{
child += 1;
}
if (hp->array[child]>hp->array[parent])
{
Swap(&hp->array[child], &hp->array[parent]); //若孩子大于父亲则交换
//继续向下调整继续判断
parent = child;
child = parent * 2 + 1;
}
else
{
return;
}
}
}
2.2 堆的创建
对于一个非堆的完全二叉树,我们要将其转换为一个堆就要从倒数第一个非叶子节点开始执行堆向下调整算法,一直调整直到根结点 ,这样这棵树就成了一个堆。
void HeapCreate(Heap* hp, HPDataType* a, int n)
{
assert(hp);
hp->array = (HPDataType*)malloc(sizeof(HPDataType)*n);
if (hp->array == NULL)
{
return;
}
//将一个数组全部拷贝到要调整的二叉树中
memcpy(hp->array, a, sizeof(HPDataType)*n);
hp->size = hp->capacity = n;
//从倒数第一个非叶子节点开始调整
for (int root = (n - 2) / 2; root >= 0; root--)
{
AdjustDown(hp, n, root);
}
}
2.3 堆的插入
先将待插入元素放置在数组的最后,也就是二叉树的最后一个叶子结点,然后再执行向上调整算法。而向上调整算法就是将刚插入的结点作为孩子,再找到它的父亲结点与之比较,若孩子结点大于父亲结点就交换,一直到根结点或者孩子结点小于父亲结点才结束。
void AdjustUP(Heap* hp, int child)
{
assert(hp);
int parent = (child - 1) / 2;
while (child)
{
if (hp->array[child]>hp->array[parent])
{
Swap(&hp->array[child], &hp->array[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
return;
}
}
}
void CheckCapacity(Heap* hp)
{
assert(hp);
if (hp->capacity == hp->size)
{
int newCapacity = hp->capacity + 3;
HPDataType* temp = (HPDataType*)realloc(hp->array,sizeof(HPDataType)* newCapacity);
if (NULL == temp)
{
assert(0);
return;
}
hp->array = temp;
hp->capacity = newCapacity;
}
}
void HeapPush(Heap* hp, HPDataType x)
{
assert(hp);
CheckCapacity(hp); //先检查空间是否已满,未满再插入,已满先扩容再插入
hp->array[hp->size++] = x;
AdjustUP(hp, hp->size - 1); //想上调整
}
2.4 堆的删除
首先将根结点和最后一个叶子结点交换,然后将二叉树的结点个数减 1 ,这就完成了根节点的删除,但这是二叉树已不是一个堆,问题就出现刚刚交换上去的根结点,所以只需对根节点完成向下调整算法即可。
void HeapPop(Heap* hp)
{
assert(hp);
Swap(&hp->array[0], &hp->array[hp->size-1]); //先交换
hp->size--; //再删除
AdjustDown(hp, hp->size, 0);
}
总结
本文主要针对于二叉树的理解、堆的创建和对堆的一系列操作。重点在于二叉树的相关性质以及完全二叉树的理解,同时一定对堆的向下调整算法理解清楚。当然,二叉树的相关知识点还未完结,后续会继续总结。
感谢您的阅读,若文章存在问题还烦请指出,感觉有帮到你的话还请一键三连。