在上篇文章中我们讲解了树的定义及表示,在树形结构中有一个非常重要的类型–二叉树,这篇文章我们就来讲解二叉树的定义、分类、性质和存储结构
上文链接:树的定义及表示
1.二叉树的定义
二叉树是n(n≥0)个结点的有限集合,由一个根节点及两棵互不相交的、分别称为左子树和右子树的二叉树组成。
如图1就是一棵二叉树,因为每个结点最多只有两个子结点,因此称之为二叉
二叉树有两个基本特征:
- 每个结点最多只能有两个子结点
- 二叉树是有序树,左子树和右子树顺序不能颠倒,即使树中某个结点只有一棵子树,也要区别是左子树和右子树
如图2就是两棵不同的二叉树
2.二叉树的分类
根据二叉树中结点的排布,可将二叉树分为非完全二叉树、完全二叉树、满二叉树。
非完全二叉树即为普通二叉树,不多说。
2.1 完全二叉树
完全二叉树除最后一层外,每一层上的结点数均达到最大值,在最后一层上只缺少右边的若干结点。如图三所示就是一棵完全二叉树:
完全二叉树有以下特点:
- 叶子结点只能出现在最下两层
- 最下层的叶子结点一定集中在左边连续位置
- 如果结点的度为1,那么该结点只有左孩子,不存在只有右孩子的情况
- 同样结点的二叉树,完全二叉树的深度最小
2.2 满二叉树
满二叉树除叶子结点外,每个结点都有两个孩子结点,每一层的结点数都达到最大。如图4所示就是一棵满二叉树:
在同样深度的二叉树中,满二叉树的结点最多。如果一棵深度为k的二叉树有2^k-1个结点,那么这就是一棵满二叉树
满二叉树是完全二叉树的特殊情况,可以说满二叉树是一棵完全二叉树,但完全二叉树不一定是满二叉树
2.3 斜树
一棵只有左孩子或者只有右孩子的二叉树叫做斜树,只有左孩子的二叉树叫做左斜树,只有右孩子的二叉树叫做右斜树。
斜树的每一层都只有一个结点,结点的个数与二叉树的深度相同。可以看出,斜树与线性表结构是相同的,其实线性表可以看成树的一种特殊形式。
3.二叉树的性质
性质1:在二叉树的第i层上最多有2^(i-1)个结点。
因为第一层是2 ^ 0,每一层结点数最多是上一层的2倍,所以每一层最多有2 ^ (i-1)个结点
性质2:深度为k的二叉树最多有2^k-1个结点(k>0)
这个我们在讲满二叉树的时候提及过
性质3:对于任何一棵二叉树,若度为2的结点数有n2个,则叶子结点(n0)必定为n2+1,即n0 = n2+1
证明:设二叉树中度为1的结点数为n1,总结点数为n,
∵n = n0+n1+n2 ①
又∵除根节点外,其余每个结点要么是度为1的结点的孩子结点,要么是度为
2的结点的孩子结点
∴n = n1+2n2+1 ②
由①、②可得n0 = n2+1
性质4:具有n个结点的完全二叉树,它的深度必为⌊㏒₂n⌋+1
假设完全二叉树的深度为k,结点数为n,那么它一定小于等于同深度的
满二叉树的结点数2 ^ k-1,且一定大于2 ^ (k-1)-1,即:
2 ^ (k-1) -1 < n ≤ 2 ^ k -1
∵n是正整数
∴2 ^ (k-1) ≤ n < 2 ^ k
∴k-1≤log₂n<k
∴k=⌊㏒₂n⌋+1
性质5:若对完全二叉树中的结点从上至下,从左至右编号,则编号为i(1 ≤ i ≤ n)的结点,其左孩子编号为2i,右孩子编号为2i+1,父节点编号为⌊i/2⌋(i=1时除外)
(注意:这里是从1开始编号,而非0)
4.二叉树的存储结构
4.1 二叉树的顺序存储
二叉树的顺序存储是用一组连续的存储单元来存放二叉树中的结点元素。对完全二叉树来说,分配一段相应大小的空间,对树中的结点从上而下、从左至右进行存储。
在顺序存储时,一般从下标1开始存储,这样是为了既存储了树中的结点元素,又存储了结点之间的逻辑过膝。根据上面谈到的性质5,我们可知如果知道了一个结点元素的下标,那么它的父节点与子结点均能轻易找到。
如结点B,下标为2,按照性质5,其左孩子下标为4,对应E,右孩子下标为5,对应F,父节点下标为1,对应A,这与树中的逻辑关系是一致的。
注意:对于完全二叉树,顺序存储结构中数组元素下标之间的关系可以反映结点之间的逻辑关系,但对于普通二叉树却不一定可以
4.2 二叉树的链式存储
二叉树的链式存储利用链表来实现,链表中的结点存储树结点中的元素和树结点之间的关系,二叉树的链式存储有两种常用方式:二叉链表表示法、三叉链表表示法
4.2.1 二叉链表示法
使用二叉链表表示树,通常树中的每个结点由三部分组成:数据域、左指针域、右指针域
结点的数据类型定义如下:
typedef struct TreeNode{
DataType data; //DataType为我们给某种数据类型起的别名
struct TreeNode* lchild;
struct TreeNode* rchild;
}TreeNode;
typedef struct TreeNode* Tree;
其中数据域data存储结点的数据信息,lchild、rchild分别存储指向左孩子、右孩子和指针。
一个二叉链表由根指针root唯一确定,若二叉链表为空,则root为空
代码实现上图二叉树:
TreeNode nodeA,nodeB,nodeD,nodeI,nodeJ,nodeL;
//将结点都初始化为NULL
memset(&nodeA,0,sizeof(TreeNode));
memset(&nodeB,0,sizeof(TreeNode));
memset(&nodeD,0,sizeof(TreeNode));
memset(&nodeF,0,sizeof(TreeNode));
memset(&nodeI,0,sizeof(TreeNode));
memset(&nodeL,0,sizeof(TreeNode));
//给每个结点数据域赋值,这里只写nodeA的
nodeA.data=1;
//...
//各结点关系表示
nodeA.lchild=&nodeB;
nodeA.rchild=&nodeD;
nodeB.rchild=&nodeF;
nodeD.lchild=&nodeI;
nodeF.lchild=&nodeL;
4.2.3 三叉链表表示法
三叉链表表示法相对二叉链表表示法多了一个指向父结点的指针parent
三叉链表表示法与二叉链表表示法相似,这里不再讲解,给出数据类型定义如下:
typedef struct TreeNode{
DataType data;
struct TreeNode* lchild;
struct TreeNode* rchild;
struct TreeNode* parent;
}TreeNode;
typedef struct TreeNode* Tree;
4.3 双亲表示法
双亲表示法利用一维数组来存储树的结点,结点结构体中包含结点数据、父节点在数组中的位置、自己是父节点和左孩子还是右孩子
typedef struct PTreeNode{
DataType data;
int parent_position; //存储父节点的位置,数组的下标
char lr_tag; //表示自己是左孩子还是右孩子,1表示左孩子,2表示右孩子
}PTreeNode;
再定义一个头结点,存储所有结点(即上面说的数组),结点数目,根节点
typedef struct PTree{
PTreeNode nodes[MAX] //MAX为定义的宏,表示最大结点数目
int node_num; //结点数目
int root; //根节点在数组的位置
}PTree;
在实际项目开发中,如果树不是太复杂,一般选择此方式