目录
树是一种重要数据结构,它的拓展应用很广。
基本概念
树由若干点和恰好比点少一条边组成。大概长得像这样:
树有几个特点:
- n 个点必须连在一起。
- 边的数量必须恰好比点的数量少一。
- 没有两条边连接的两个点完全相同。
- 若不重复经过点,则一点到另一点只有一条路径。
树可以分为两种:
- 无根树:所有边都是无向的(即边连接的两个点可以互相到达)。
- 有根树:有至少一条边是有向的(即只能边连接的两个点只能从一个点到另一个点,另一个点不能到达这个点)。
还可以分为另外两种:
- 带权树:所有边带有权值(长度)。
- 不带权树:没有边带有权值。
可以发现,一棵树不是无根树,就是有根树。
关于树,还有几个特殊的概念:
- 节点/结点:树中的点就是结点。
- 树根:对于无根树,任何点可作为根节点;对于有根树,从任意它以外的点出发都无法到达的点就是根。
- 父结点:一个点所连接的点中深度比该结点小的点就是父节点
- 子结点:一个点所连接的点中深度比该结点大的点就是父节点
- 叶子结点:没有子结点的结点就是叶子结点。
- 兄弟结点:拥有相同父亲的结点。
- 结点的深度:结点到根的所需经过的最少的边的数量就是该结点的深度。
- 树的深度:树中结点的最大深度就是树的深度。
- 树中两点的距离:在不带权树中,两点距离为从一点到另一点所经过的最少边的数量;在带权树中,两点距离为从一点到另一点所经过的边的最小权值和。
- 链:所有结点只有不超过 1 个儿子的树。
- 树的重心:所有点到这个点的距离最短的点。
树的代码实现
树的操作灵活多样,主要看题目要求。所以主要讲解存储。
儿子存储法
int son[100][100];
int w[100][100];
int len[100];
其中 son_{i,j}soni,j 表示结点 ii 的第 jj 个儿子,len_ileni 表示结点 ii 的儿子数量,w_{i, j}wi,j 表示结点 ii 的第 jj 个儿子连接的边的边权。
空间复杂度:O(n^2)O(n2)。
优:已知父节点向所有子结点操作快捷 (O(n)O(n))。
劣: 子结点查找父节点麻烦。(O(log_2n)O(log2n))。
父节点向指定多个子节点拓展麻烦 (O(mn)O(mn))。
占用空间
父亲存储法
int f[100];
int w[100];
其中 f_ifi 表示结点 ii 的父亲,w_iwi 表示结点 ii 的父亲连接的边的边权。
空间复杂度:O(n)O(n)。
优:已知子节点向父结点操作快捷 (O(1)O(1))。
劣: 父结点查找子节点麻烦。(O(log_2n)O(log2n))。
父节点向指定多个子节点拓展麻烦 (O(mn)O(mn))。
双亲存储法
int son[100][100],f[100];
int ws[100][100],wf[100];
int len[100];
其中 son_{i,j}soni,j 表示结点 ii 的第 jj 个儿子,len_ileni 表示结点 ii 的儿子数量,f_ifi 表示结点 ii 的父亲,,ws_{i, j}wsi,j 表示结点 ii 的第 jj 个儿子连接的边的边权,wf_iwfi 表示结点 ii 的父亲连接的边的边权。
空间复杂度:O(n^2)O(n2)。
优:向所有双亲结点操作快捷 (O(n)O(n))。
劣:向多个指定子节点拓展麻烦 (O(mn)O(mn))。
占用空间(哪都有你)
邻接矩阵
int edge[100][100];//如果是不带权树可以改成 bool;
其中 edge_{i,j}edgei,j 表示结点 ii 到达第 jj 个结点的边权,若为 0 则没有边连接。
空间复杂度:O(n^2)O(n2)。
优:对于几乎所有操作快捷。
劣:父子关系不明确(有过年父母叫脸盲的孩子认亲戚的既视感)。
占用空间(How old are you?)
邻接表
vector <int> edge[100];
vector <int> w[100];
其中 vector_{i,j}vectori,j 表示结点 ii 的第 jj 个儿子,edge_{i,j}edgei,j 表示结点 ii 到达第 jj 个结点的边权,若为 0 则没有边连接。
优:对于几乎所有操作快捷。
劣:父子关系不明确,单独查询两个点的父子关系麻烦。
二叉树
二叉树指所有结点的儿子数不超过 2 个的特殊二叉树。
二叉树又有几个特殊之处:
- 左儿子:指左边的儿子结点。
- 右儿子:指右边的儿子结点。
- 满二叉树:指深度为 nn 时,结点数为 2^n-12n−1 的树。
- 完全二叉树:指深度为 nn 时,所有的叶子结点深度都为 nn 或 n-1n−1,且除根结点和最后一个结点(不一定没有)以外所有的结点都有兄弟且若最后一个结点没有兄弟则最后一个结点不为右儿子的树。
- 左子树:指以该结点左儿子为根的子树。
- 右子树:指以该结点右儿子为根的子树。
二叉树的遍历
遍历主要靠递归实现。事实上,树的算法大多的需要递归实现。用这棵树举例子。
前序遍历(先根遍历)
即先遍历根,再遍历左子树,再遍历右子树。
代码:
int l[100],r[100];//这是基于儿子存储法的二叉树改进版本。
void erg(int x){
if(!x) return; //空子树直接返回。
printf("%d ",x);
erg(l[x]);
erg(r[x]);
}
结果为:
1 2 4 5 3 6 7
中序遍历(中根遍历)
即先遍历左子树,再遍历根,再遍历右子树。
代码:
int l[100],r[100];。
void erg(int x){
if(!x) return;
erg(l[x]);
printf("%d ",x);
erg(r[x]);
}
结果为:
4 2 5 1 6 3 7
后序遍历(后根遍历)
即先遍历左子树,再遍历右子树,再遍历根。
代码:
int l[100],r[100];。
void erg(int x){
if(!x) return;
erg(l[x]);
erg(r[x]);
printf("%d ",x);
}
结果为:
4 5 2 1 6 7 3
通过遍历顺序构造树
想要实现这个功能,就得知道中序遍历和前(后)序遍历 。
举个栗子:
1 2 4 5 3 6 7//前序遍历
4 2 5 1 6 3 7//中序遍历
- 因为前序遍历的顺序,前序遍历的第一个一定是这个树的根。
- 知道了根,在中序遍历中,根左边的结点一定在左子树中,右边的结点一定在右子树中。
- 找到左子树的前序遍历和右子树的前序遍历。
-
递归。
最近公共祖先(LCA)
指深度最大的公共祖先。
暴力法
两点同步高度后向上查找。
mm 次的时间复杂度:O(mn)O(mn)。
倍增法
同步高度后跳到深度为 2^k2k 的祖先后判断:
- 相同,向下找。
- 不同,向上跳。
m次的时间复杂度: O(log_2m+log2n)O(log2m+log2n)。