算法通关村第六关——理解树的结构(青铜)
1. 树的常见概念
树是一种非线性的数据结构,它由若干个节点组成,并且这些节点之间存在特定的关系。在树结构中,每个节点都有一个父节点和零个或多个子节点。
常见概念有如下:
- 节点(Node):树中的基本单元,节点通常包含存储数据的数据域和指向其他节点的引用域。
- 根节点(Root Node):树的顶层节点,它没有父节点,是整棵树的起点。
- 子节点(Child Node):某节点下属于它的直接后继节点。
- 父节点(Parent Node):某节点所指向的直接前驱节点。
- 叶子节点(Leaf Node):没有任何子节点的节点。
- 兄弟节点(Sibling Node):拥有共同父节点的节点。
- 子树(Subtree):由一个节点及其所有后代节点组成的树。
- 深度(Depth):从根节点到某节点的路径上经过的边数。
- 高度(Height):从某节点到它的最远叶子节点的路径上经过的边数。
- 层级(Level):根节点的深度为0,它的子节点的深度为1,依次类推。
- 祖先节点(Ancestor Node):从根节点到某节点路径上的所有节点。
- 后代节点(Descendant Node):某节点下属于它的所有子节点。
- 节点的度:一个结点含有的子节点的个数称为该节点的度。
- 树的度(Degree):一棵数中,最大的节点的度称为树的度,注意与节点度的区别;
- 有序树(Ordered Tree):树中的子节点具有顺序。
- 无序树(Unordered Tree):树中的子节点没有特定的顺序。
- 森林(Forest):由若干颗互不相交的树组成的集合。
通过一个例子来讲解树的常见概念:
高度 深度 层
A 2 0 1
/ \
B C 1 1 2
/ \ / \
D E F G 0 2 3
在这个例子中,A是根节点,B和C是A的子节点,D、E、F和G分别是B和C的子节点。叶子节点包括D、E、F和G,它们没有子节点。
以下是树的常见概念的详细解释:
- 节点:每个节点都有一个存储数据的数据域(比如字母A、B等)和指向其他节点的引用域。
- 根节点:顶层节点A,它没有父节点。
- 子节点:B和C是A的子节点,D、E、F和G是B和C的子节点。
- 父节点:A是B和C的父节点,B是D和E的父节点,C是F和G的父节点。
- 叶子节点:D、E、F和G没有任何子节点。
- 兄弟节点:B和C是兄弟节点,也就是拥有共同父节点A的节点。
- 深度:根节点A的深度为0,B和C的深度为1,D、E、F和G的深度为2。
- 高度:叶子节点D、E、F和G的高度为0,B和C的高度为1,根节点A的高度为2。
- 层级:根节点A所在的层级为0,B和C所在的层级为1,D、E、F和G所在的层级为2。
- 祖先节点:比如,节点B的祖先节点为A,节点E的祖先节点为B和A。
- 后代节点:比如,节点A的后代节点包括B、C、D、E、F和G。
2. 树的性质
树具有以下几个重要的性质:
**性质1:**在二叉树的第i层上至多有2^(i - 1)个节点(i > 0)
**性质2:**深度为k的二叉树至多有2^k - 1 个结点(k>0)
**性质3:**对于任意一棵二叉树,如果其叶结点数为N0,而度数为2的结点总数为N2,则N0=N2+1
**性质4:**具有n个结点的完全二叉树的深度必为log2(n+1)
**性质5:**对完全二叉树,若从上至下、从左至右编号,则编号为i的结点,其左孩子编号必为2i,其右孩子编号必为2i+1;其双亲的编号必为i/2(i = 1 时为根,除外)
让我们以一个家谱树为例来说明这些性质:
A
/ \
B C
/ \ \
D E F
\
G
在这个家谱树中,A是根节点,B和C是A的子节点,D、E和F分别是B和C的子节点,G是E的子节点。
-
性质1:在二叉树的第i层上至多有2^(i - 1)个节点(i > 0)
- 第1层上最多有2^(1-1)=1个节点。
- 第2层上最多有2^(2-1)=2个节点。
- 第3层上最多有2^(3-1)=4个节点。
-
性质2:深度为k的二叉树至多有2^k - 1个结点(k>0)
- 这棵树的深度为3,所以至多有2^3-1=7个节点。
-
性质3:对于任意一棵二叉树,如果其叶结点数为N0,而度数为2的结点总数为N2,则N0=N2+1
- 叶节点是没有子节点的节点。
- 度数为2的节点是指具有两个子节点的节点
- 在这棵树中,叶节点数N0为4(D、G、F),度数为2的节点数N2为2(A、B),满足N0 = N2 + 1。
-
性质4:具有n个结点的完全二叉树的深度必为log2(n+1)
- 这棵树有7个节点,所以它是具有7个节点的完全二叉树,深度为log2(7+1)=3。
-
性质5:对完全二叉树,若从上至下、从左至右编号,则编号为i的结点,
其左孩子编号必为2i,其右孩子编号必为2i+1;其双亲的编号必为i/2(i = 1 时为根,除外)- 编号为1的节点A,左孩子编号为2,右孩子编号为3。
- 编号为2的节点B,左孩子编号为4,右孩子编号为5,双亲编号为1。
- 编号为3的节点C,右孩子编号为6,双亲编号为1。
- 编号为4的节点D,没有左孩子和右孩子,双亲编号为2。
- 编号为5的节点E,左孩子编号为10,右孩子编号为11,双亲编号为2。
- 编号为6的节点F,没有左孩子和右孩子,双亲编号为3。
- 编号为7的节点G,没有左孩子和右孩子,双亲编号为5。
3.树的定义与存储方式
定义二叉树
定义树的原理与前面讲的链表本质上是一样的,只不过多了一个指针,如果是二叉树,只要在链表的定义上增加一个指针就可以了
public class BinaryTreeNode {
private int data; // 节点的数据
private BinaryTreeNode leftChild; // 左子节点
private BinaryTreeNode rightChild; // 右子节点
}
本质上就是有两个引用,分别指向两个位置,为了便于理解,我们分别命名为左孩子和右孩子。如果是N叉树该如何定义呢?其实就是每个节点最多可以有N个指针指向其他地方,这是不用left和right,使用一个List就可以了,也就是:
public class NaryTreeNode {
private int data; // 节点的数据
private List<NaryTreeNode> children; // 子节点
}
下面给出一个通用的树节点类。它包含了以下属性:
id
:节点ID,用来唯一标识每个节点。parentId
:父节点ID,标识节点的父节点。顶级节点的父节点ID一般为0。label
:节点的名称或标签,用来描述节点的内容。children
:子节点列表,存储该节点的所有子节点。
通过这个通用的树节点类,可以构建各种类型的树结构,如二叉树、N叉树等。这样的设计可以方便地表示树形结构,并进行相关的操作和遍历。
public class TreeNode {
/** 节点ID */
private Integer id;
/** 父节点ID:顶级节点为0 */
private Integer parentId;
/** 节点名称 */
private String label;
/** 子节点 */
private List<TreeNode> children;
public TreeNode(Integer id, Integer parentId, String label) {
this.id = id;
this.parentId = parentId;
this.label = label;
}
}
4.树的遍历
二叉树的遍历方式有层次遍历和深度优先遍历两种:
- 深度优先遍历:先往深走,遇到叶子节点再往回走。
- 广度优先遍历:一层一层的去遍历,一层访问完再访问下一层。
这两种遍历方式不仅仅是二叉树,N叉树也有这两种方式,图结构也有。
深度优先算法又有前中后序三种。
树的遍历方式主要有三种:前序遍历、中序遍历和后序遍历。下面将结合一个例子来详细讲解这三种遍历方式。
考虑以下二叉树作为例子:
A
/ \
B C
/ \ \
D E F
\
G
- 前序遍历(Preorder Traversal):按照 根-左-右 的顺序遍历树。
- 遍历顺序:A -> B -> D -> E -> G -> C -> F
- 中序遍历(Inorder Traversal):按照 左-根-右 的顺序遍历树。
- 遍历顺序:D -> B -> G -> E -> A -> C -> F
- 后序遍历(Postorder Traversal):按照 左-右-根 的顺序遍历树。
- 遍历顺序:D -> G -> E -> B -> F -> C -> A
现在,让我们一步一步地说明如何进行这三种遍历:
- 前序遍历:
- 首先访问根节点A。
- 然后递归地对左子树进行前序遍历,访问节点B。
- 继续递归地对左子树进行前序遍历,访问节点D。
- 左子树遍历完成后,回到B节点,递归地对右子树进行前序遍历,访问节点E。
- 继续递归地对右子树进行前序遍历,访问节点G。
- 回到A节点,递归地对右子树进行前序遍历,访问节点C。
- 最后递归地对右子树进行前序遍历,访问节点F。
- 中序遍历:
- 首先递归地对左子树进行中序遍历,访问节点D。
- 然后访问根节点B。
- 继续递归地对左子树进行中序遍历,访问节点E。
- 继续递归地对右子树进行中序遍历,访问节点G。
- 回到根节点A,访问它。
- 递归地对右子树进行中序遍历,访问节点C。
- 最后递归地对右子树进行中序遍历,访问节点F。
- 后序遍历:
- 首先递归地对左子树进行后序遍历,访问节点D。
- 继续递归地对左子树进行后序遍历,访问节点G。
- 然后回到根节点B,递归地对右子树进行后序遍历,访问节点E。
- 回到根节点A,递归地对右子树进行后序遍历,访问节点C。
- 继续递归地对右子树进行后序遍历,访问节点F。
- 最后访问根节点A。