树
一、树
树是一种非线性的数据结构,用它能很好地描述有分支和层次特性的数据集合。树型结构在编译系统中,用树表示源程序的语法结构。在数据库系统中,树型结构是数据库层次模型的基础,也是各种索引和目录的主要组织形式。
在树型结构中,二叉树是最常用的结构,它的分支个数确定、又可以为空、并有良好的递归特性,特别适宜于程序设计,因此也常常将一般树转换成二叉树进行处理。
二、定义及相关概念
1、定义
树是一种数据结构,它是由n(n≤0n \le 0n≤0)个有限结点组成的一个具有层次关系的集合T(Tree)。当n = 0时,称为空树(其也是树);把它叫做叫做“树”是因为其看起来像一颗倒挂的树,也就是说它是根朝上,而叶朝下的。
当n>0n > 0n>0时,该集合满足如下条件:
1、其中必有一个称为根 (root) 的特定结点,它没有直接前驱,但有零个或多个直接后继;
2、其余 n - 1 个结点可以划分成 m (m≤0m \le 0m≤0) 个互不相交的划分集 T1,T2,...,TmT_1, T_2, ... , T_mT1,T2,...,Tm,其中 TiT_iTi 又是一棵树,称为根的子树。每棵子树的根结点有且只有一个直接前驱,但有零个或多个直接后继。
2、相关概念
结点:
包含一个数据元素及若干指向其它结点的分支信息。
结点的度:
一个结点的子树个数称为此结点的度。
叶结点:
度为的0结点,即无后继的结点,也称为终端结点。
分支结点:
度不为0的结点,也称为非终端结点。
孩子结点:
一个结点的直接后继继称为该结点的孩子结点。
双亲结点(父结点):
一个结点的真接前驱称为该结点的双亲结点。
兄弟结点:
同一双亲结点的孩子结点之间互称兄弟结点。
祖先结点:
一个结点的祖先结点是指从根结点到该结点的路径上的所有结点。在图中,结点K的祖先是A、B、E。
子孙结点:
一个结点的直接后继和间接后继称为该结点的子孙结点。在右图中,结点D的子孙是H、I、]、M。
树的度:
树中所有结点的度的最大值。
结点的层次:
从根结点开始足义,根结点的层次为1,根的直接后继的层次为2,依此类推。
树的高度(深度):
树中所有结点的层次的最大值。
有序树:
在树T中,如果各子树T土之间是有先后次序的,则称为有序树。
森林:
m(m≤0m \le 0m≤0)棵互不相交的树的集合。将一棵非空树的根结点删去,树就变成一个森林;反之,给森林增加一个统一的根结点,森林就变成一棵树。
有序树和无序树
树中任意结点的子结点之间有顺序关系,这种树称为有序树,反之则成为无序树。在有序树中,一个结点最左边的子树称为"第一个孩子",最右边的称为"最后一个如图所示,如果是其本身是一棵有序树,则以结点B为根结点的子树为整棵树的第一个孩子,以结点D为根结点的子树为整棵树的最后一个孩子。
三、树的存储结构
1、双亲表示法
说明:
以双亲作为索引的关键词的一种存储方式。每个结点只有一个双亲,所以选择顺序存储占主要,以一组连续空间存储树的结点,同时在每个结点中,附设一个指示其双亲结点位置的指针域。
结点结构:
data | parent |
---|---|
数据域 | 指针域(指向父结点) |
例子:
下标 | data | parent |
---|---|---|
0 | A | -1 |
1 | B | 0 |
2 | C | 0 |
3 | D | 0 |
4 | E | 1 |
5 | F | 1 |
6 | G | 2 |
7 | H | 3 |
8 | I | 3 |
9 | J | 3 |
10 | K | 4 |
11 | L | 4 |
12 | M | 7 |
关键代码:
struct tree {
int data, parent; //数据域,指针域
} a[];
优缺点分析:
优点:parent指针域指向数组下标,所以找双亲结点的时间复杂度为 O(1) ,向
上一直找到根结点也快。
缺点:由上向下找就十分慢,若要找结点的孩子或者兄弟,要遍历整个树。
2、孩子表示法:
说明:
孩子表示法存储普通树采用的是“顺序表+链表”的组合结构,其存储过程是:从树
的根结点开始,使用顺序表依次存储树中各个结点,各个结点配备一个链表,用于
存储各结点的孩子结点位于顺序表中的位置。
data | first child |
---|---|
数据域 | 指针域(指向孩子结点) |
其中,data是数据域,firstchild是头指针域,存储该结点的孩子链表的头指针。
data | next |
---|---|
数据域 | 指针域(指向孩子结点) |
child是数据域,用来存储某个结点在表头数组中的下标,next为指针域,存储指向该结点的下一个兄弟结点的指针。
例子:
关键代码:
struct tree {
int data, Lchild, Rchild;
} a[];
优缺点分析:
优点:查找某个结点的某个孩子,或者查找某个结点的兄弟,只需要查找这个结点的孩子单链表即可,速度快。
缺点:要找双亲结点很麻烦。
3、双亲孩子表示法
说明:
双亲孩子表示法就是在每一个节点里面除了存放数据之外,还都存放着该节点的双亲节点的位置,也存放着该节点的孩子节点的位置。
关键代码:
struct tree {
int father, data, Lchild, Rchild;
} a[];
4、孩子兄弟表示法
说明:
任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此,我们用二叉链表作树的存储结构,链表中每个结点的两个指针域分别指向第一个孩子结点和此结点的下一个兄弟结点,根结点没有兄弟,置为空
firstchild | data | nextbrother |
---|---|---|
指针域(指向第一个孩子) | 数据域 | 指针域(指向下一个兄弟结点) |
关键代码:
struct tree {
int data, Fchild, nextbother;
} a[];
优缺点分析:
优点:查找某个结点的某个孩子非常方便。此表示法的最大好处就是把一棵复杂度的树变成了一颗二叉树。
缺点:要找双亲结点很麻烦。
三、二叉树的定义
1、二叉树定义:
二叉树(binarytree)是指树中任意结点的度都不大于2的有序树。它是一种最简单且最重要的树。如下图所示,左边是二叉树,右边不是二叉树。
二叉树的递归定义为:二叉树是一棵空树,或者是一棵由一个根结点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树;左子树和右子树又同样都是二叉树。根据定义,二叉树有以下5种基本形态·
2、特殊二叉树:
满二叉树:
如果二叉树中除了叶子结点,每个结点的度都为2,则此二叉树称为满二叉树。
完全二叉树
如果二叉树中除去最后一层节点为满二叉树,且最后一层的结点依次从左到右分布,则此二叉树被称为完全二叉树。
完全二叉树
四、二叉树基本性质
性质1:在二叉树的第i(i>=1)i(i >= 1)i(i>=1)层上最多有 2i−12^{i-1}2i−1个结点。
证明:数学归纳法:
(1)第1层有 2i−1=20=12^{i - 1} = 2^0 = 12i−1=20=1 个结点。
(2)假设当 j=i−1j = i - 1j=i−1时,命题成立,即第j层最多有 2i−22^{i- 2}2i−2个结点。
(3)当j=i时,当第 i−1i - 1i−1 层结点数量最多,且第 i−1i - 1i−1 层每个结点都有
两个孩子结点时第i层的结点数量时最多的,所以带入(2)假设得第i层最
多有 $ 2 * 2^{i - 1}个结点。
即命题成立。
性质2:深度为k的二叉树至多有 2k−12^k - 12k−1 个结点。
证明:
只要保证每一层都有最大结点数时,才能保证这棵二叉树的结点数最多。由性质一可得,第一层最多有 202^020 个结点,第二层最多有 212^121 个结点,k层最多有 2(k一1)2^{(k一1)}2(k一1) 个结点。由等比数列前n项和公式可得·
20+21+...+2k−1=a1(1−qn)1−q=20(1−2k)1−2=2k−1
2^0 + 2^1 + ... + 2^{k - 1} = \frac{a_1(1 - q ^ n)}{1 - q} = \frac{2^0(1 - 2^k)}{1 - 2} = 2^k - 1
20+21+...+2k−1=1−qa1(1−qn)=1−220(1−2k)=2k−1
性质3:对于一棵非空二叉树,如果叶子结点数为 nen_ene,度为2的结点为n2n_2n2,则有 $n_e = n_2+1 $。
证明:
设二叉树的总结点数量为n,度为1的结点数量为m,则有:
n=no+n1+n2n = n_o + n_1 + n_2n=no+n1+n2(1)
在二叉树中,除根结点以外,其他结点都有父结点。度为1的结点有1个孩子,度为2的结点有2个孩子,所以结点总数也可记为:
n=n1+2∗n2+1n = n_1 + 2 * n_2 + 1n=n1+2∗n2+1(2)
由1和2可得:
n0=n2+1n_0 = n_2+ 1n0=n2+1
性质4:具有n个结点的完全二叉树的深度k为[log2n]+1[log_2n] + 1[log2n]+1
证明:
假设一棵完全二叉树的深度为k,结点个数为n时,有:
2k−1<n≤2k=12^{k - 1} < n \le 2^k = 12k−1<n≤2k=1
对两边同时加1,得·
2k−1≤n≤2k2^{k - 1} \le n \le 2^k2k−1≤n≤2k
对两边同时取log2log_2log2,得:
k−1≤[log2n]<kk - 1 \le [log_2n] < kk−1≤[log2n]<k
又因为层数为整数,所以[log2n]=k−1[log_2n] = k - 1[log2n]=k−1,即k=[log2n]+1k = [log_2n] + 1k=[log2n]+1
性质5:对于具有n个结点的完全二叉树,如果按照从上到下和总做往右的顺序对二叉树的所有结点编号为1,2,3,...,n1,2,3,...,n1,2,3,...,n对于编号为i的结点,有:
1、如果i=1,则结点i为根,无父结点;如果i>1i > 1i>1则其父结点编号为⌊i2⌋\lfloor\frac{i}{2}\rfloor⌊2i⌋。
证明:
假设二叉树有n个结点,k层。那么第k层结点数量为 n−(2k−1−1)n - (2^{k - 1} - 1)n−(2k−1−1),第1~k - 2层的结点总数为 2k−2−12^{k - 2} - 12k−2−1
(1)当n为偶数时,n的父结点在第k - 1层为从左往右数第 ⌊n−(2k−1−1)2⌋+1\lfloor\frac{n - (2^{k - 1} - 1)}{2}\rfloor + 1⌊2n−(2k−1−1)⌋+1所以父结点编号为⌊n−(2k−1−1)2⌋+1+2k−1−1=⌊n+12⌋=⌊n2⌋\lfloor\frac{n - (2^{k - 1} - 1)}{2}\rfloor + 1 + 2^{k - 1} - 1 = \lfloor\frac{n + 1}{2}\rfloor = \lfloor\frac{n}{2}\rfloor⌊2n−(2k−1−1)⌋+1+2k−1−1=⌊2n+1⌋=⌊2n⌋
(2)当n为奇数时,n的父结点在第k一1层为从左往右数 n−(2k−1−1)2\frac{n - (2^{k - 1} - 1)}{2}2n−(2k−1−1),所以父结点编号为n−(2k−1−1)2+2k−1−1=n−12=⌊n2⌋\frac{n - (2^{k - 1} - 1)}{2} + 2^{k - 1} - 1 = \frac{n - 1}{2} = \lfloor\frac{n}{2}\rfloor2n−(2k−1−1)+2k−1−1=2n−1=⌊2n⌋
2、如果2∗i≤n2 * i \le n2∗i≤n,则结点的左孩子编号为2否则结点无左孩子。
证明:如右图所示,要计算i结点的左孩子编号,首先要计算出(1)和(2)部
分结点数量。(1)结点数量由公式可得·
(1)=(2k−2−1)−1(1) = (2^{k - 2} - 1) - 1(1)=(2k−2−1)−1
(2)结点数量可以由(3)结点数量*2得到:
(3)=i−(2k−2−1)−1(3) = i - (2^{k - 2} - 1) - 1(3)=i−(2k−2−1)−1
(1)+(2)=(1)+(2)∗(3)=i(1) + (2) = (1) + (2) * (3) = i(1)+(2)=(1)+(2)∗(3)=i
所以结点i的左孩子编号为i+i=2∗ii + i = 2 * ii+i=2∗i
3、如果2∗i≤n2 * i \le n2∗i≤n,则结点的右孩子编号为2+1;否则结点无右孩子。
五、二叉树的存储结构
顺序存储:
完全二叉树的顺序存储,仅需从根结点开始,按照层次依次将树中结点存储
到数组即可。
例子:
data | 1 | 2 | 0 | 3 | 0 | 0 | 0 |
---|---|---|---|---|---|---|---|
数组下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
我们知道,完全二叉树具有这样的性质,将树中结点按照层次并从左到右依
次标号(1,2,3…),若结点i有左右孩子,则其左孩子结点为2* i,孩子结点为2*i+1。此性质可用于还原数组中存储的完全二叉树。
二叉树并不适合用数组存储,因为并不是每个二叉树都是完全二叉树,普通二叉树使用顺序表存储或多或多会存在空间浪费的现象。
链式存储
关键代码:
struct Tree{
int data, Lchild, Rchild, father;
} a[];
在某些实际场景中,可能会做“查找某结点的父结点”的操作,这时可以在结点结构中再添加一个指针域,用于各个结点指向其父亲结点。这样的链表结构,通常称为三叉链表.
六、二叉树的遍历
1、前序遍历:
思想:
(1)访问根结点;
(2)遍历当前节点的左子树;
(3)遍历当前节点的右子树;
关键代码:
void print(int i) {
if (i == 0) return;
printf("%d", &x);
print(a[x].Lchild);
print(a[x].Rchild);
}
2、中序遍历:
思想:
(1)遍历当前节点的左子树;
(2)访问根结点;
(3)遍历当前节点的右子树;
关键代码:
void print(int i) {
if (i == 0) return;
print(a[x].Lchild);
printf("%d", &x);
print(a[x].Rchild);
}
3、后序遍历:
思想:
(1)遍历当前节点的左子树;
(2)遍历当前节点的右子树;
(3)访问根结点;
关键代码:
void print(int i) {
if (i == 0) return;
print(a[x].Lchild);
print(a[x].Rchild);
printf("%d", &x);
}
七、例题
扩展二叉树
题目描述
由于先序、中序和后序序列中的任一个都不能唯一确定一棵二叉树,所以对二叉树做如下处理,将二叉树的空结点用·补齐,如图所示。我们把这样处理后的二叉树称为原二叉树的扩展二叉树,扩展二叉树的先序和后序序列能唯一确定其二叉树。
现给出扩展二叉树的先序序列,要求输出其中序和后序序列。
输入格式
扩展二叉树的先序序列。
输出格式
思路:
同过先序序列,将图用孩子表示法存入结构体数组中,搜索时传入当前遍历的点,定义全局变量q表示下一个遍历的点:
a[t].Lchild = dfs(++q);
a[t].Rchild = dfs(++q);
存入后,按照中序(即左,中,右)与后序(即左、右、中)递归输出;
代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
char s[1000005];
int n, q = 1;
struct tree {
char data;
int Lchild, Rchild;
} a[10000005];
int dfs(int t) {
if (s[t] == '.') return 0;
a[t].data = s[t];
a[t].Lchild = dfs(++q);
a[t].Rchild = dfs(++q);
return t;
}
void print1(int t) {
if (t == 0) return ;
print1(a[t].Lchild);
cout << a[t].data;
print1(a[t].Rchild);
}
void print2(int t) {
if (t == 0) return ;
print2(a[t].Lchild);
print2(a[t].Rchild);
cout << a[t].data;
}
int main() {
cin >> s + 1;
n = strlen(s + 1);
dfs(1);
print1(1);
cout << endl;
print2(1);
return 0;
}