一.树的概念
在计算机科学(Computer Science,SC)中,树(Tree)是一种重要的非线性数据结构,用于表示具有层次关系的数据。它由节点(Node)组成,其中一个节点被指定为根节点(Root),其余节点被划分成若干个互不相交的子集,每个子集本身也是一颗树,称为子树(Subtree)
二.树的特点
树是一种递归定义的非线性结构,由节点组成
树
- 有且仅有一个根节点
- 其余节点被划分成若干个互不相交的子树(子树之间不能相交,否则会形成环路)
- 节点之间通过边(Edge)连接
- 没有环(Cycle):树中不存在环路(即不能从一个节点出发沿环回到自身)
- 节点个数n=0时为空树
三.树的基本术语
1.根(Root)
根为树的顶层节点,没有父节点
A
/ \
B C
如上图,A就是根节点
2.父节点(Parent)
父节点表示一个节点的直接上层节点
A
/
B
如上例子,A是B的父节点
3.子节点(Child)
表示一个节点的直接下层节点
A
/
B
B是A的子节点
4.叶子(Leaf)
没有子节点的节点
A
/ \
B C
/ \
D E
B、D、E都是叶子节点
5.兄弟(Silbing)
具有相同父节点的节点
A
/ \
B C
B和C是兄弟节点
6.深度(Depth)
表示从根到改节点的边数。
根深度通常为0或1,依定义而定
A (depth 0)
/ \
B C (depth 1)
/ \
D E (depth 2)
7.高度(Height)
表示从该节点到最远叶子的最长路径边数
整棵树的高数等于根的高度
A (height 2)
/ \
B C (height 1)
/ \
D E (height 0)
整棵树的高度是2,因为从根节点A到最远叶子节点(如D或E),需要经过两条边
8.层(Level)
层是从根节点开始自上而下对树中节点进行的水平划分
-
根节点位于第0层(虽然有些教程定义为第1层,但在CS中,通常从0开始计数)
-
层与深度的关系:一个节点的"层号"等于它的深度(前提是根节点的深度等于0)
A ← 层 0(深度 0) / \ B C ← 层 1(深度 1) / / \ D E F ← 层 2(深度 2) / G ← 层 3(深度 3)
| 节点 | 所在层(Level) | 深度(Depth) |
|---|---|---|
| A | 0 | 0 |
| B, C | 1 | 1 |
| D, E, F | 2 | 2 |
| G | 3 | 3 |
为什么需要"层"这个概念:
1.层序遍历:
按从上到下,从左到右访问节点,也叫广度优先遍历(BFS)
如上图进行BFS,顺序为:A → B → C → D → E → F → G
2.计算树的宽度(Width)
某一层中节点最多的数量,就是树的"最大宽度"
3.打印树的结构
很多可视化工具按层输出,便于理解层次关系
9.度(Degree)
节点的度:该节点拥有的子节点的数量
树的度:指整棵树所有节点的度的最大值
换句话说,一个节点有几个"孩子",它的度就是几
java
A
/ | \
B C D
/ \
E F
节点A有3个字节点(B、C、D) → 节点A的度为3
节点B有2个子节点(E、F) → 节点B的度为2
节点C、D、E、F都没有子节点 → 度都为0
整棵树的度为max(3,2,0,0,0,0) = 3
由此得出叶子节点的特性:度为0的节点为叶子节点,叶子节点的度为0
10.度为m的树和m叉树
相同点:树里面节点的度最多为m
不同点:
- 度为m的树至少要有一个节点的度等于m
- m叉树所有节点的度都可以小于m,甚至可以是空树(0节点,自然节点的度为0,树的度也为0)
- " 度为m的树 "这种说法是描述树的某一刻的状态
- m叉树是事先对树结构的一种约束、、
11.有序树和无序树
各节点从左到右有次序,不能互换,称该树为有序树;否则称为无序树
java
A
/ | \
B C D
/ \
E F
比如,如上图,假如规定所有子树上的节点(字母)要从小到大排列,比如B、C、D,那就是有序树
如果这些子树可以随意交换位置
如:
java
A
/ | \
D B C
/ \
E F
那就是无序树
12.路径(Path)
路径是指从树的一个节点到另一个节点所经过的连续边的序列
路径由一系列相邻的节点组成,且不能重复经过同一个节点(因为树中无边)
路径的特点:
- 唯一性:在树中,任意两个节点有且仅有一条路径(这是树"无环" + "联通"的直接结果)
- 方向无关:路径可以自上而下(根 → 叶),也可以自下而上(叶 → 根),或横跨子树(如左子树某节点 → 右子树某节点)
- 长度:路径的长度等于路中边的数量(不是路中的节点数!!) 如A → B → C,长度为2 。 单节点(如A → A)路径的长度为0
java
A
/ \
B C
/ / \
D E F
/
G
| 起点 → 终点 | 路径(节点序列) | 路径长度(边数) |
|---|---|---|
| A → D | A → B → D | 2 |
| D → G | D → B → A → C → F → G | 5 |
| C → E | C → E | 1 |
| F → F | F | 0 |
| B → C | B → A → C | 2 |
注意:D到G必须经过它们的共同祖先A,因为树种没有其他连接方式
| 概念 | 含义 | 是否包含多个节点? |
|---|---|---|
| 路径(Path) | 两个节点之间的连接路线 | ✅ 是(至少1个节点,通常≥2) |
| 深度(Depth) | 从根到某节点的路径长度 | ❌ 是一种特殊路径(根→某节点) |
| 高度(Height) | 从某节点到最远叶子的最长路径长度 | ❌ 是路径长度的最大值 |
| 祖先/后代 | 描述节点间的上下级关系 | 路径上的节点互为祖先/后代 |
13.直径(Diameter)
树中任意两个节点的最长路径称为直径
java
A
/ \
B C
/ \
D F
\
G
最长路径可能是:D → B → A → C → F → G,长度 = 5(6个节点,5条边)
14.森林(Forest)
森林是若干棵互不相交的树的集合
森林 = 多棵树
空森林(0棵树)也是合法的森林
这是一棵树:
java
A
/ \
B C
/ \
D E
这是一个森林(多棵树,互不相连):
java
A F X
/ \ \
B C Y
/ \
D E
四.树的性质
1.节点数 = 边数 + 1 = 所有节点度数之和 + 1
例题:
在一棵度为4的树T中,若有20个度为4的节点,10个度为3的节点,1个度为2的节点,10个度为1的节点,则树T的叶节点个数是( )。
解析:
节点数n = 度为4的节点个数 + 度为3的节点个数
+ 度为2的节点个数
+ 度为1的节点个数
+ 度为0的节点个数(叶子节点)
n = 20 + 10 + 1 + 10 +
n = 41 +
而由公式:节点数 = 所有节点度数之和 + 1
即n = 20*4 + 10*3 + 1*2 + 10 * 1 + *0 + 1
n = 123
所以123 = 41 +
所以 = 82
所以叶子节点数为82
五.二叉树(Binary Tree)
1.二叉树的概念
每个节点最多有2个分支(最多有2个孩子节点),即节点的度只可能为0、1、2的树
二叉树是一种特殊的树结构,其中每个节点最多有两个子节点,并且这两个子节点有明确的顺序区分:
左子节点(Left Child)
右子节点(Right Child)
注意:"左" 不等于"右",即使某个节点只有一个子节点,也必须说明它是左子节点还是右子节点
合法的二叉树示例:
java
1
/ \
2 3
/ / \
4 5 6
关键:有序 + 最多两个孩子
2.二叉树的常见类型
| 类型 | 特点 |
|---|---|
| 满二叉树(Full Binary Tree) | 每个节点要么有 0 个子节点,要么有 2 个(不能只有 1 个) |
| 完全二叉树(Complete Binary Tree) | 除了最后一层,其他层全满;最后一层节点靠左排列(堆常用) |
| 完美二叉树(Perfect Binary Tree) | 所有叶子在同一层,且每个非叶节点都有 2 个孩子 |
| 二叉搜索树(BST) | 左子树所有值 < 根 < 右子树所有值(用于高效查找) |
| 平衡二叉树(如 AVL) | 左右子树高度差 ≤ 1,保证 O(log n) 性能 |
完全二叉树是满二叉树的一种特殊情况,完全二叉树最后一层可以不满,但是必须完全往左一次排列
如:
java
1
/ \
2 3
/ \ /
4 5 6
而像下面这种,就不是完全二叉树
java
1
/ \
2 3
/ / \
4 5 6
如果完全二叉树最后一层全满了,此时就是满二叉树了
如:
java
1
/ \
2 3
/ \ / \
4 5 6 7
3.二叉树的共有性质
1.叶子节点树 = 双分支节点数 + 1
由树的性质:节点数 = 边数 + 1 = 所有节点度数之和 + 1
先理解这个性质:每个节点(除了根节点)都有唯一的向上的边,所以边数会比节点数少1
而度为x的节点机会向下延伸出x条边(指向x个子节点),所以边数和度数是相等的
n = 2* + 1*
+ 0 *
+ 1
= +
+
=> 2* +
+ 1 =
+
+
=> + 1 =
4.完全二叉树的性质
从上到下,从左到右给每个节点编号,编号从1开始
1
/ \
2 3
/ \ / \
4 5 6 7
/ \
8 9
这样可以利用编号之间的规律,轻松地找到某个节点的孩子或父亲
一个节点,用它的编号乘以2就可以找到它的左孩子,编号乘以2再加1就能找到它的右孩子(前提是这个节点有孩子节点)
对于一个节点,编号除以2下取整就能找到它的父亲节点
对于一棵节点数为n的完全二叉树,它的最后一个分支节点的编号是n除以2下取整,即 ,而这个最后的分支节点前面的节点都是分支节点,后面的都是叶子节点
例题:一棵完全二叉树有768个节点,则该二叉树的叶子节点个数是( )。
解析: 768 除以 2 下取整为 384 ,则384(不包括384)后面的节点都是叶子节点
则叶子节点的个数为 768 - 384 = 384
完全二叉树最多只有一个度为1的节点
如:
1
/ \
2 3
/ \ / \
4 5 6 7
/ \
8 9
这棵二叉树没有度为1的节点
1
/ \
2 3
/ \ / \
4 5 6 7
/ \ /
8 9 10
这棵二叉树有一个度为1的节点
结论:完全二叉树的节点个数为偶数则有唯一的一个度为1的节点
节点个数为奇数则没有度为1的节点
5.二叉树的存储
1.顺序存储
用数组存储完全二叉树的节点,数组索引与节点编号相对应,索引0不存节点,从索引1开始存
对于不是完全二叉树的树,也可以使用顺序存储,只不过要将树补成完全二叉树(或满二叉树)的形式,补充的节点是不存在的,如果节点值是正数那就用-1来填充不存在的节点值;如果节点值是字符串,那就用#号来填充。这样就可以利用完全二叉树的规律来索引节点了
需要注意的是,如果是接近右单支树或右单支树,采用顺序存储的方式来存储节点浪费的空间将达到指数级别
2.链式存储
六.二叉树的遍历
1.前序遍历
规则:先访问根节点,再访问左子树,最后访问右子树
记忆:根左右
1
/ \
2 3
/ / \
4 5 6
如对上述二叉树进行前序遍历,遍历的结果是:124356
2.中序遍历
规则:左根右
1
/ \
2 3
/ / \
4 5 6
中序遍历结果:421536
3.后序遍历
规则:左右根
1
/ \
2 3
/ / \
4 5 6
后序遍历结果:425631
4.深度优先遍历(DFS)
Depth Frist Seach
从根开始,沿一条路径走到最深(叶子),再回溯尝试其他路径
前序遍历、中序遍历、后序遍历都属于深度优先遍历
A
/ \
B C
/ / \
D E F
以前序为例:A → B → D → C → E → F
5.广度优先遍历(BFS)
Breadth Frist Seach
尽可能先访问根最近的节点,也称为层序遍历
A
/ \
B C
/ / \
D E F
层序遍历:A → B → C → D → E → F
6.Java代码
1.递归实现DFS
1
/ \
2 3
/ / \
4 5 6
TreeNode.java
java
package algorithm.datastructure.tree;
public class TreeNode {
public int val;
public TreeNode left;
public TreeNode right;
// 构造函数1:接受节点值
public TreeNode(int val) {
this.val = val;
}
// 构造函数2:接受节点值、左子树、右子树
public TreeNode(TreeNode left, int val, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
// 重写toString方法
// toString是Java中Object类的一个方法,所有类都默认继承Object
// 因此每个类都具备toString方法
// toString方法提供了对象的字符串表示形式,方便调试和日志输出
// 默认情况下,toString方法返回的是类名 + @ + 对象的hashCode(例如TreeNode@1b6d3586),但这通常不具备可读性
// 开发者可以通过重写toString方法来自定义对象的字符串表示,使其更直观地反应对象的状态
@Override
public String toString() {
return String.valueOf(this.val);
}
}
TreeTraversal.java
java
package algorithm.datastructure.tree;
public class TreeTraversal {
public static void main(String[] args) {
TreeNode root = new TreeNode(
new TreeNode(new TreeNode(4), 2, null),
1,
new TreeNode(new TreeNode(5), 3, new TreeNode(6))
);
//System.out.println(root); // 如果没有重新toString方法则输出algorithm.datastructure.tree.TreeNode@5b2133b1,重写了则输出1
/*Java内部设计了两种调用toString()方法的方式
* 显式调用:对象.toString();
* 隐式调用:System.out.println(对象);*/
preOrder(root); // 1 2 4 3 5 6
System.out.println();
inOrder(root); // 4 2 1 5 3 6
System.out.println();
postOrder(root); // 4 2 5 6 3 1
}
// 前序遍历
static void preOrder(TreeNode node) {
if (node == null) {
return;
}
// 根
System.out.print(node.val + "\t");
// 左
preOrder(node.left);
// 右
preOrder(node.right);
}
// 中序遍历
static void inOrder(TreeNode node) {
if (node == null) {
return;
}
// 左
inOrder(node.left);
// 根
System.out.print(node.val + "\t");
// 右
inOrder(node.right);
}
// 后序遍历
// post除了有邮件、邮递、邮政、帖子、发布的意思,还有在...之后的意思
static void postOrder(TreeNode node) {
if (node == null) {
return;
}
// 左
postOrder(node.left);
// 右
postOrder(node.right);
// 根
System.out.print(node.val + "\t");
}
}