数据结构:树(Tree)
目录
我们为什么需要“树”?
树的专业术语 (Terminology)
二叉树 (Binary Tree) 的一步步推导
二叉树的严格定义
如何用代码表示一个二叉树节点?
我们为什么需要“树”?
在学习“树”之前,你最熟悉的数据结构应该是数组 (Array) 和 链表 (Linked List)。我们先分析一下它们的优缺点,看看能不能找到一个“两全其美”的方案。
1. 数组 (Array):
优点: 访问速度极快。
因为内存是连续的,我们可以通过索引(下标)直接计算出元素的地址,所以查询任何一个元素的时间复杂度是 O(1)。
缺点: 插入和删除操作非常慢。
假设你有一个很大的、排好序的数组,要在中间插入一个元素,你需要把这个元素之后的所有元素都向后移动一位。删除同理。
这个过程的时间复杂度是 O(n),其中 n 是元素的数量。此外,数组的大小通常是固定的,扩容很麻烦。
2. 链表 (Linked List)
优点: 插入和删除操作非常快。
我们只需要改变目标位置前后节点的指针指向即可,时间复杂度是 O(1)(前提是已经找到了要操作的位置)。
缺点: 访问和搜索速度很慢。因为内存是分散的,你没办法直接跳到第 i
个元素,必须从头节点开始,一个一个地“遍历”过去,时间复杂度是 O(n)。
矛盾出现了:
我们想要一个数据结构,既能像数组一样快速搜索,又能像链表一样快速增删。
推导开始:
-
链表的慢,根源在于它是一个线性结构,只有一个“下一个”(
next
)的方向。要找一个元素,最坏情况要走遍整条链。 -
怎么加速呢?如果我们从一个节点出发,能有好几个“下一个”选项,是不是就能更快地“缩小范围”?
-
比如说,我们站在一个节点,左边指过去,都是比它小的数;右边指过去,都是比它大的数。这样一来,每次比较,我们都能排除掉大约一半的元素,搜索速度会大大提升。
这个“一个节点指向多个节点”的想法,就打破了“线性”的束缚。
这种一个节点(父)连接到一个或多个节点(子)的结构,看起来就像现实世界里倒挂的树。
“树”结构,就是为了解决数组和链表在“查找效率”和“增删效率”上的根本矛盾而诞生的。它是一种非线性 (Non-linear) 的数据结构。
树的专业术语 (Terminology)
在深入学习之前,必须先掌握这些基本概念,这是我们交流的“语言”。
A(根节点 Root)/ \B(Parent) C(Parent)/ \ \D E F(Leaf) (Leaf) / \G H(Leaf) (Leaf)
对应解释(结合上图)
-
节点(Node)
-
树的基本组成单位,如 A、B、C、D、E、F、G、H 都是节点。
-
-
根节点(Root)
-
树最上层的节点,没有父节点。
-
在上图中,A 是根节点。
-
-
边(Edge)
-
连接两个节点的线,例如
(A,B)
、(B,E)
、(F,H)
等。
-
-
父节点(Parent)
-
一个节点的直接上层节点。
-
B 的父节点是 A;E 的父节点是 B。
-
-
子节点(Child)
-
父节点直接连接的下层节点。
-
B 的子节点是 D 和 E。
-
-
兄弟节点(Siblings)
-
拥有相同父节点的节点。
-
D 和 E 是兄弟节点;G 和 H 也是兄弟节点。
-
-
叶子节点 / 终端节点(Leaf Node / Terminal Node)
-
没有子节点(度为 0)的节点。
-
D、E、G、H 是叶子节点。
-
-
内部节点(Internal Node)
-
非叶子节点,即至少有一个子节点。
-
A、B、C、F 是内部节点。
-
-
子树(Subtree)
-
由任意一个节点及其所有后代节点组成的树。
-
以 C 为根节点的子树是
{C, F, G, H}
。
-
-
节点的度(Degree of a Node)
-
一个节点拥有的子节点数量。
-
B 的度是 2(D 和 E);F 的度是 2(G 和 H)。
-
-
树的度(Degree of a Tree)
-
树中所有节点度的最大值。
-
上图最大度是 2(B 和 F),所以树的度为 2。
-
-
层(Level)
-
从根开始,根为第 0 层,依次向下。
-
A 在第 0 层,B 和 C 在第 1 层,D、E、F 在第 2 层,G、H 在第 3 层。
-
-
深度(Depth)
-
从根节点到某个节点经过的边数。
-
E 的深度是 2(A→B→E)。
-
-
高度(Height)
-
从某个节点到最远叶子节点的边数。
-
树的高度是根节点 A 的高度,这里是 3(A→C→F→G/H)。
-
中文术语 | 英文术语 | 解释 |
节点 | Node | 树的基本组成部分。它包含数据和指向其他节点的指针。 |
根节点 | Root | 树顶端的节点,它没有父节点。一棵树只有一个根节点。 |
边 | Edge | 连接两个节点的线。 |
父节点 | Parent | 一个节点所连接的上层节点。 |
子节点 | Child | 一个节点所连接的下层节点。 |
兄弟节点 | Siblings | 拥有相同父节点的节点们。 |
叶子节点 / 终端节点 | Leaf Node / Terminal Node | 没有任何子节点的节点(度为0的节点)。 |
内部节点 | Internal Node | 非叶子节点,即至少有一个子节点的节点。 |
子树 | Subtree | 树中任意一个节点和它下面的所有后代节点组成的结构。 |
节点的度 | Degree of a Node | 一个节点拥有的子树数量(或子节点数量)。 |
树的度 | Degree of a Tree | 树中所有节点度的最大值。 |
层 | Level | 从根节点开始,根为第0层,根的子节点为第1层,以此类推。 |
深度 | Depth | 从根节点到某个节点所经过的边的数量。根节点的深度是0。 |
高度 | Height | 从某个节点到其最远叶子节点所经过的边的数量。树的高度是指根节点的高度。 |
二叉树 (Binary Tree) 的一步步推导
我们已经知道,树的一个节点可以有任意多个子节点。但在实际应用中,这“任意多”会带来不确定性,导致结构复杂,不方便实现。
第一性推导:如何简化“树”这个通用模型?
-
最简单的“树”是什么?每个节点最多只有一个子节点。
-
如果你画出来,会发现这其实就是一个链表。它又回到了线性的老路。
-
那么,在“一个子节点”(链表)的基础上,稍微复杂化一点,但又保持结构足够简单,该怎么办?
-
答案就是:让每个节点最多有两个子节点。
这个“最多有两个子节点”的树,就是二叉树 (Binary Tree)。
它是在通用树的复杂性和链表的简单性之间取得的完美平衡,也是实际应用中最常见、最重要的树形结构。
二叉树的严格定义
二叉树是一个有限的节点集合,这个集合:
-
要么是空集(空树)。
-
要么由一个根节点和两棵互不相交的、分别称为根节点的左子树 (Left Subtree) 和右子树 (Right Subtree) 的二叉树组成。
关键点:
-
最多两个:节点可以有0个、1个或2个子节点。
-
有序区分:左子树和右子树是严格区分的,不能颠倒。一个节点只有一个子节点时,也要明确它是左孩子还是右孩子。
如何用代码表示一个二叉树节点?
根据定义,一个节点需要:
-
一块空间来存储数据本身 (Data)。
-
一个指针指向它的左孩子。如果左孩子不存在,就指向
NULL
。 -
一个指针指向它的右孩子。如果右孩子不存在,就指向
NULL
。
所以,最自然的 C/C++ 结构就产生了:
C++ 版本:
struct TreeNode {int data; // 节点存储的数据,这里用 int 举例TreeNode* left; // 指向左子树的指针 (left child pointer)TreeNode* right; // 指向右子树的指针 (right child pointer)
};
C 版本:
typedef struct TreeNode {int data;struct TreeNode* left;struct TreeNode* right;
} TreeNode;
-
data
用来存放节点的数值。 -
left
指针指向左边的孩子节点,没有就是NULL
。 -
right
指针指向右边的孩子节点,没有就是NULL
。
一棵完整的树,就是由很多这样的TreeNode
通过left
和right
指针互相连接而成的。
我们通常会持有一个指向根节点的指针(TreeNode* root
)来代表整棵树。