树的定义与基本概念
困难3认识“树”:从文件夹结构到家族谱的计算机思维
你有没有想过,为什么电脑里的文件能一层层打开?为什么你家人的关系可以画成一张树形图?在计算机科学中,我们专门用一种叫做 树(Tree) 的数据结构来组织这种“一层套一层”的层次关系。树就像一棵倒着长的大树——最上面的根(文件夹)分出很多枝干(子文件夹),枝干上再长小枝(文件),最后才是叶子(具体的文件或数据)。学会树,你就能理解文件系统、网页的 DOM 结构、甚至你的家族谱是怎么存储在电脑里的了。
为什么需要“树”?而不是数组或链表?
你已经学过数组和链表,它们像一条直线(一对一关系)。比如排队买零食,第一个人后面跟着第二个人……但生活中的很多数据是“一对多”的:一个老师教多个学生、一个文件夹里装多个子文件夹、一个爸爸可以有多个孩子。用线性的结构来表示这种关系会非常别扭(比如要用二维数组或复杂的链表),而树结构天生就适合描述这种层次关系。所以,树是计算机处理“分支”、“分类”、“包含”这些场景的利器。
关键概念:从节点到整棵树
1. 节点(Node)—— 树的基本单元
每个圆圈就是一个节点,它存储一个数据(如文件名、人名、字母)。节点之间用线(叫边)连接。
生活中的例子:
- 你的名字是一个节点,你爸爸的名字是一个节点。
- 电脑里“我的文档”文件夹是一个节点,里面的“作业”文件夹也是一个节点。
2. 根节点(Root)—— 没有“爸爸”的节点
整棵树只有一个节点没有父节点,它就是根。好比一个家族里最早的祖先,或者一个文件夹的最顶层目录(如C盘)。
常见错误:有同学会认为根节点就是最上面的节点,但有时候树可能没有根?不对!树必须有且只有一个根节点,否则就不是树(可能是森林)。比如家族谱里只能有一个最早的祖先吗?实际上如果追溯人类起源,可能有很多祖先,但在计算机里我们只建一棵“树”,所以要选一个起点。
3. 父节点与子节点(Parent & Child)
如果节点 X 直接连着节点 Y,并且 X 在 Y 的上面,那么 X 是 Y 的爸爸(父节点),Y 是 X 的孩子(子节点)。注意:每个节点只能有一个爸爸(除了根),但可以有多个孩子。
例子:在你的家族树中,你的爸爸是父节点,你是子节点。你爸爸也只能有一个爸爸(你的爷爷)。
4. 兄弟节点(Sibling)
有同一个爸爸的节点之间叫兄弟。比如你和你的亲兄弟姐妹就是兄弟节点。在一个文件夹里,两个并列的子文件夹就是兄弟。
5. 叶子节点(Leaf)—— 没有孩子的节点
像树叶一样长在最末端,没有子节点。在文件系统中,叶子节点通常是一个具体的文件(比如 homework.docx),而不是文件夹。
6. 内部节点(Internal Node)—— 既有爸爸又有孩子
除了根和叶子之外的那些节点。比如你爸爸(他既有爷爷,又有你)。
7. 度(Degree)—— 孩子数量
一个节点有几个孩子,度就是几。比如你有3个孩子,你的度就是3。叶子节点的度为0。树的度是整棵树中节点度的最大值。
小测验:假设一棵树根节点有2个孩子,其中一个孩子有3个孩子,另一个孩子没有孩子。树的度是多少?答案是3(因为最大的度是3)。
8. 深度(Depth)与高度(Height)
- 深度:从根节点往下数到该节点走了几步。根节点深度为0,它的孩子深度为1,孙子深度为2……
- 高度:从该节点往下到最远的叶子节点有几步。叶子本身高度为0。树的高度就是根节点的高度(相当于树总共几层)。
例子:想一想你的年级?如果一年级是根,那么六年级就是深度6。高度反过来,从六年级往上到一年级是5?不对,高度是某节点到最远叶子的路径长度。对于根来说,高度就是最深的叶子深度。所以深度和高度数值上可能相等(这里需要区分定义,通常我们约定深度从根算起,根深度0;高度从叶子算起,叶子高度0)。不同教材定义可能不同,但重要的是理解“层次”概念。
9. 子树(Subtree)
任意节点和它所有的后代构成的树,就是原树的一棵子树。你可以把树想象成“大娃娃套小娃娃”,每个节点都是一个小娃娃的头,带着自己的一串孩子。
动手实现:用代码创建一棵树
我们先用生活中常见的“班级小组”来举例:
- 老师(根)是王老师。
- 她手下有两个组长:小明和小红。
- 小明组里有阿花、阿强;小红组里有小丽、小刚。
这种关系就是一棵树。
C++ 实现(孩子表示法)
#include <iostream>
#include <vector>
#include <string>
using namespace std;
// 定义树节点结构体
struct TreeNode {
string data; // 节点存储的数据,比如人名
vector<TreeNode*> children; // 子节点指针列表
// 构造函数:初始化数据,孩子列表为空
TreeNode(string val) : data(val) {}
};
int main() {
// 创建节点(用名字初始化)
TreeNode* teacher = new TreeNode("王老师");
TreeNode* ming = new TreeNode("小明");
TreeNode* hong = new TreeNode("小红");
TreeNode* hua = new TreeNode("阿花");
TreeNode* qiang = new TreeNode("阿强");
TreeNode* li = new TreeNode("小丽");
TreeNode* gang = new TreeNode("小刚");
// 建立关系:王老师下面有小明和小红两个组长
teacher->children.push_back(ming);
teacher->children.push_back(hong);
// 小明组下有两个学生
ming->children.push_back(hua);
ming->children.push_back(qiang);
// 小红组下有两个学生
hong->children.push_back(li);
hong->children.push_back(gang);
// 简单验证:输出王老师的第一个孩子(小明)和第二个孩子(小红)
cout << "王老师的第一个组长: " << teacher->children[0]->data << endl;
cout << "王老师的第二个组长: " << teacher->children[1]->data << endl;
// 再输出小明组的第一个学生
cout << "小明组的第一个学生: " << ming->children[0]->data << endl;
// 释放内存(养成好习惯)
delete hua; delete qiang; delete ming;
delete li; delete gang; delete hong;
delete teacher;
return 0;
}
代码解释:
TreeNode结构体有两个成员:data存名字,children存孩子指针列表。- 用
push_back把子节点指针加入父节点的children列表,就建立了父子关系。 - 注意:C++ 中手动
new出来的内存要自己delete,否则会内存泄漏。释放顺序一般可以从叶子往根释放,但这里简写了。 - 常见错误:忘记释放内存,或者重复释放。还有不要在释放后继续使用指针(野指针)。
Python 实现(更简洁)
class TreeNode:
def __init__(self, data):
self.data = data # 节点数据
self.children = [] # 子节点列表
# 创建节点
teacher = TreeNode("王老师")
ming = TreeNode("小明")
hong = TreeNode("小红")
hua = TreeNode("阿花")
qiang = TreeNode("阿强")
li = TreeNode("小丽")
gang = TreeNode("小刚")
# 建立关系
teacher.children.append(ming)
teacher.children.append(hong)
ming.children.append(hua)
ming.children.append(qiang)
hong.children.append(li)
hong.children.append(gang)
# 验证
print("王老师的第一个组长:", teacher.children[0].data)
print("小明组的第一个学生:", ming.children[0].data)
Python 说明:
- Python 用列表作为
children,不用手动管理内存。 - 注意:这里所有节点都是对象引用,不涉及深拷贝的问题。
常见错误与避坑指南
1. 混淆“树”和“链表”
链表是线性结构,一个节点只有一个后继;而树是一个节点可以有多个孩子。小学生容易把树看作“有分支的链表”,但其实树的核心是层次关系,而不是顺序。
2. 认为每个节点必须有俩孩子(混淆了二叉树)
我们这里讲的是一般树(多叉树),节点可以有0个、1个、2个或更多孩子。不要一上来就以为树只有二叉树。
3. 忘记根节点唯一性
有些同学画树时可能画两个没有父节点的节点,那其实是“森林”(多棵树)。树必须只有一个根。
4. 在 Python 中修改列表时误操作
比如 teacher.children = ming 这样赋值(而不是 append),导致 ming 变成唯一的子节点,后面再 append 就会报错或覆盖。记住:children 是一个列表,要往里面添加元素,必须用 append 或 extend。
5. C++ 中指针未初始化就用
如果忘记 new 节点,直接 TreeNode* p; p->data = ... 会访问非法内存。一定要先 new。
完整示例:用树表示你的书桌整理
想象你的书桌有多个抽屉,每个抽屉里有几个盒子,盒子里有笔、橡皮等文具。我们可以用树模型:
- 根节点:书桌
- 第一层:抽屉 A、抽屉 B
- 第二层:A 中有盒子1、盒子2;B 中有盒子3
- 第三层:盒子1里有铅笔、橡皮;盒子2里有尺子;盒子3里有剪刀、胶水
下面用 Python 创建这棵树,并写一个简单函数打印所有内容(深度优先遍历,后面会专门学)。
class TreeNode:
def __init__(self, name):
self.data = name
self.children = []
def print_tree(node, level=0):
"""递归打印树,level表示当前缩进层次"""
print(" " * level + node.data)
for child in node.children:
print_tree(child, level + 1)
# 创建节点
desk = TreeNode("书桌")
drawerA = TreeNode("抽屉A")
drawerB = TreeNode("抽屉B")
box1 = TreeNode("盒子1")
box2 = TreeNode("盒子2")
box3 = TreeNode("盒子3")
pencil = TreeNode("铅笔")
eraser = TreeNode("橡皮")
ruler = TreeNode("尺子")
scissors = TreeNode("剪刀")
glue = TreeNode("胶水")
# 构造关系
desk.children.append(drawerA)
desk.children.append(drawerB)
drawerA.children.append(box1)
drawerA.children.append(box2)
drawerB.children.append(box3)
box1.children.append(pencil)
box1.children.append(eraser)
box2.children.append(ruler)
box3.children.append(scissors)
box3.children.append(glue)
# 打印整棵树
print("我的书桌物品树:")
print_tree(desk)
输出结果:
书桌
抽屉A
盒子1
铅笔
橡皮
盒子2
尺子
抽屉B
盒子3
剪刀
胶水
你看,树结构非常直观地展现了我们书桌物品的从属关系。
总结要点
- 树的本质:描述一对多层次关系的数据结构,由节点和边组成。
- 核心概念:根节点(唯一)、父/子节点、兄弟节点、叶子节点、内部节点、度、深度/高度、子树。
- 常见实现方式:孩子表示法——每个节点用一个列表或数组存储所有孩子指针/引用。适合多叉树。
- 两种语言对比:C++ 需要手动管理内存,Python 自动垃圾回收。
- 应用无处不在:文件系统、网页 DOM、家谱、组织架构、编译器语法树、游戏中的场景管理。
- 常见错误:混淆树与链表、忘记根唯一、Python 列表使用不当、C++ 指针未初始化。
掌握了树的基本概念,下一步就可以学习 二叉树——一种每个节点最多有两个孩子的特殊树。二叉树是许多高效算法的基础(如二叉搜索树、堆、红黑树)。你也可以尝试思考:如果用树来组织一本书的目录(章、节、小节),该怎么表示?试试用代码实现一下吧!
例题精讲
在计算机科学中,树是一种非线性数据结构。以下哪个选项最准确地描述了树结构的特点?
在一棵非空的树中,根节点没有父节点,叶子节点没有子节点。
下面关于树中“父子关系”的描述,正确的是?
在树结构中,每个节点都必须至少有一个子节点。
以下是用Python定义一个简单树节点的代码框架,请补充空缺的部分,使得每个节点包含一个整数值和一个子节点列表。
class TreeNode:
def __init__(self, val):
self.value = ___
self.children = ___