CC++ & Algorithm

树的定义与基本概念

困难3
语言版本:通用
概述:用文件夹和家族树的例子,带你认识树这种非线性结构,理解节点、根、叶子、父子关系等核心术语,并给出C++和Python的节点创建代码。

认识“树”:从文件夹结构到家族谱的计算机思维

你有没有想过,为什么电脑里的文件能一层层打开?为什么你家人的关系可以画成一张树形图?在计算机科学中,我们专门用一种叫做 树(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 是一个列表,要往里面添加元素,必须用 appendextend

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
      剪刀
      胶水

你看,树结构非常直观地展现了我们书桌物品的从属关系。


总结要点

  1. 树的本质:描述一对多层次关系的数据结构,由节点和边组成。
  2. 核心概念:根节点(唯一)、父/子节点、兄弟节点、叶子节点、内部节点、度、深度/高度、子树。
  3. 常见实现方式:孩子表示法——每个节点用一个列表或数组存储所有孩子指针/引用。适合多叉树。
  4. 两种语言对比:C++ 需要手动管理内存,Python 自动垃圾回收。
  5. 应用无处不在:文件系统、网页 DOM、家谱、组织架构、编译器语法树、游戏中的场景管理。
  6. 常见错误:混淆树与链表、忘记根唯一、Python 列表使用不当、C++ 指针未初始化。

掌握了树的基本概念,下一步就可以学习 二叉树——一种每个节点最多有两个孩子的特殊树。二叉树是许多高效算法的基础(如二叉搜索树、堆、红黑树)。你也可以尝试思考:如果用树来组织一本书的目录(章、节、小节),该怎么表示?试试用代码实现一下吧!

例题精讲

1单选题

在计算机科学中,树是一种非线性数据结构。以下哪个选项最准确地描述了树结构的特点?

A树中每个节点只能有一个前驱节点和一个后继节点
B树中任意两个节点之间都有唯一路径相连,且不含回路
C树中所有节点都必须按顺序排列,类似于数组
D树中每个节点必须有两个子节点
2判断题

在一棵非空的树中,根节点没有父节点,叶子节点没有子节点。

3单选题

下面关于树中“父子关系”的描述,正确的是?

A一个节点可以有多个父节点
B一个节点可以有多个子节点,但只有一个父节点
C根节点也有一个父节点
D叶子节点可以有多个父节点
4判断题

在树结构中,每个节点都必须至少有一个子节点。

5填空题
以下是用Python定义一个简单树节点的代码框架,请补充空缺的部分,使得每个节点包含一个整数值和一个子节点列表。

class TreeNode:
    def __init__(self, val):
        self.value = ___
        self.children = ___