文章目录
第六章 树
树的定义
树的定义:树(Tree)是n个结点的有限集,n=0时称为空树。在任意一棵非空树中:
- 有且仅有一个特定的根(Root)的结点
- 当n>1时,其余的结点可分为m个互不相交的有限集 T 1 , T 2 , . . . T m T_1,T_2,...T_m T1,T2,...Tm,其中每一个集合本身优势一棵树,并且称为根的子树(SubTree)
错误的树:
子树相交了
结点的分类
结点的度:结点拥有的子树树被称为结点的度(Degree)。
叶结点/终端结点:度为0的结点称为叶结点(Leaf)或终端结点。
非终端结点/分支结点:度不0的结点。
内部结点:除了根结点之外,分支结点也称为内部结点。
树的度:树内各结点的度的最大值。
结点间关系:
- 结点的子树的根称为该结点的孩子(Child),相应地,该节点称为孩子的双亲(Parent)。
- 同一个双亲的孩子之间互称兄弟(Sibling)。
- 结点的祖先是从根到该结点所经分支上的所有结点。
- 以某结点为根的子树中的任意结点都称为该节点的子孙。
树的其他相关概念:
- 结点的层次(Level)从根开始定义起,根为第一层,根的孩子为第二层。
- 双亲在同一层的结点互为堂兄弟。
- 树中结点的最大层次称为树的深度(Depth)或高度。深度是绝对的,从根结点开始算,高度是相对的,看自己的子树有多高。
- 如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则为无序树。
- 森林(Forest)是m棵互不相交的树的集合,对树中的每个结点而言,其子树的集合即为森林。
、
树的抽象数据类型
树的存储结构
利用顺序存储和链式存储结构的特点,完全可以实现对树的存储结构的表示
双亲表示法
思路:在每个结点中,附设一个指示器指示其双亲结点到链表中的位置,知道爹妈是谁,不知道孩子是谁(段正淳?)。
结点结构定义:
#define MAX_TREE_SIZE 100
typedef int TElemType;
typedef struct PTNode // 结点结构
{
TElemType data; // 结点数据
int parent; // 双亲位置
} PTNode;
typedef struct // 树结构
{
PTNode nodes[MAX_TREE_SIZE];
int r, n; // 根的位置和结点数
} PTreee;
根节点没有双亲,所以约定根结点的位置域设置为-1。
可以根据需求,再增加一个结点最左边孩子的域,叫长子域,如果没有孩子结点,长子域设置为-1:
还可以增加右兄弟域,如果存在右兄弟,就记录下右兄弟的小标,不存在就设置为-1:
那么左右是怎么确定的呢?
孩子表示法
多重链表表示法:由于树中每个结点可能有多棵子树,可以考虑用多重链表,即每个结点有多个指针域,其中每个指针指向一棵子树的根结点,我们把这张方法叫做多重链表表示法。
然而树的每个结点的度是不同的,有两种解决方案。
方案一:
让指针域的个数就等于树的度:
其中data是数据域,child1到childd是指针域,用来指向该结点的孩子结点。
缺点:但是这样很浪费空间,除非说各个树的结点度相差很小的时候,才有优势。
方案二:
每个指针结点域的个数等于该结点的度,专门取一个位置来存储结点指针域的个数:
其中data为数据域,degree为度域,也就是存储该结点的孩子结点的个数,child1到childd为指针域,指向该结点的各个孩子的结点。
缺点:虽然对空间的利用率提高了,但是由于各个结点的链表是不相同的结构,加上要维护结点的度的数值,在运算上就会带来时间上的损耗。
孩子表示法:把每个结点的孩子结点排列起来,以单链表作存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空,然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中
分而治之?
孩子链表的孩子结点:
其中child是数据域,用来存放结点在表头数组中的下标;next是指针域,存放指向某结点的下一个孩子结点的指针。
表头数组的表头结点:
其中data是数据域,存储某结点的数据信息;firstchild是头指针域,存储该结点的孩子链表的头指针。
孩子表示法结构定义:
#define MAX_TREE_SIZE 100
typedef struct CTNode
{
int child; // 表头下标
struct CTNode *next;
} *ChildPtr;
typedef struct CTBox // 表头结构
{
TElemType data;
ChildPtr firstchild;
};
typedef struct CTree // 树结构
{
CTBox nodes[MAX_TREE_SIZE]; // 表头结构数组,就是结点数组
int r, n; // 根的位置和结点数
};
优点:对于查找某个结点的某个孩子,或者找某个结点的兄弟,遍历等比较方便
感觉兄弟不好找呀,比如找A的兄弟,要遍历找到包含A的子链表,才能找到A的兄弟
缺点:无法找到双亲,找双亲需要遍历。
所以可以把双亲表示法和孩子表示法综合一下,得到双亲孩子表示法:
孩子兄弟表示法
理论基础:任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的,因此,设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟。
其中data是数据域;firstchild为指针域,存第一个孩子的地址;righsib也是指针域,存储右兄弟结点的地址。
结构定义代码:
typedef struct CSNode
{
TElemType data;
struct CSNode *firstchild, *rightsib;
} CSNode, *CSTree;
如果有需要可以加parent指针域。
好处:给查找某个结点的孩子带来了方便,同时把一棵复杂的树变成了一棵二叉树
二叉树
定义:二叉树是n个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成
二叉树特点:
- 每个结点最多两棵子树,所以二叉树中不存在度大于2的结点。
- 左子树和右子树是有顺序的,次序不能任意颠倒。
- 即使某结点只有一棵子树,也要区分他是左子树还是右子树
二叉树的五种基本形态:
- 空二叉树
- 只有一个根结点
- 根结点只有左子树
- 根结点只有右子树
- 根结点既有左子树又有右子树
三个结点的二叉树有五种形态:
特殊二叉树
斜树
定义:所有的结点都只有左子树的二叉树叫左斜树,只有右子树叫右斜树,这两者统称为斜树。
左斜树:
右斜树:
特点:每一层都只有一个结点,结点的个数与二叉树的深度相同。
满二叉树
定义:在一棵二叉树中,如果所有的分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树被称为满二叉树
特点:
- 叶子只能出现在最下层
- 非叶子结点的度一定是2
- 在同深度的二叉树中,满二叉树的节点个数最多,叶子数最多
完全二叉树
定义:对一棵具有n个结点的二叉树按层序编号,如果编号为i(1<=i<=n)的结点的位置,和同深度的满二叉树中 i 结点的位置相同,那这颗树就叫完全二叉树。
没满,但是存在的结点结构和满二叉树相同
按层序编号后的结点应连贯,满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树,下面是非完全二叉树:
特点:
- 叶子结点只能出现在最下两层
- 最下层的叶子一定集中在左部连续位置
- 倒数二层,若有叶子结点,一定都在右部连续位置
- 如果结点度为1,则该结点只有左孩子,不存在只有右孩子的情况
- 同样结点树的二叉树,完全二叉树的深度最小
子树的生成以左边为主,但是不可只有左边,倒数第二层必须有右边,倒数第二层开始网上必须都完全填满,所以深度会最小
二叉树的性质
性质1:在二叉树的第 i 层上至多有 2 i − 1 2^{i-1} 2i−1个结点(i>=1)
性质2:深度为k的二叉树至多有 2 k − 1 2^{k}-1 2k−1个结点(k>=1)
性质3:对任何一棵二叉树T,如果其终端结点数位 n 0 n_0 n0,度位2的结点数为 n 2 n_2 n2,则 n 0 = n 2 + 1 n_0 = n_2 + 1