二叉树:从定义到性质,一次说透
各位同学,今天我们来聊聊二叉树。说实话,二叉树是我在数据结构里用得最多的结构之一。无论是做编译器里的语法树,还是写网络协议里的路由表,二叉树的身影无处不在。我个人习惯把二叉树看作是「链表的分叉进化版」——每个节点不再只有一个 next,而是有了 left 和 right 两个指针。
二叉树的定义
先给个严谨的定义:二叉树是每个节点最多有两个子树的树结构。注意这个「最多」,也就是说,一个节点可以有 0 个、1 个或 2 个子节点。
用 C 语言表示一个二叉树节点,我通常会这样写:
typedef struct TreeNode {
int data; // 数据域
struct TreeNode *left; // 左孩子指针
struct TreeNode *right; // 右孩子指针
} TreeNode;
嗯,就这么简单。每个节点存一个数据,再加两个指针。你想想看,这和双向链表是不是有点像?只不过双向链表是前后两个指针,二叉树是左右两个指针。但它们的逻辑结构完全不同——链表是线性的,二叉树是非线性的。
特殊二叉树:斜树、满二叉树、完全二叉树
二叉树有很多种形态,但有三种特殊的二叉树,你必须记住。为什么?因为它们的性质直接决定了算法的效率。
1. 斜树
说白了,斜树就是所有节点都偏向一边的二叉树。如果所有节点只有左孩子,叫左斜树;只有右孩子,叫右斜树。
你想想看,斜树长什么样?其实就是个链表。每个节点只有一个孩子,深度等于节点数。我在早期写代码时犯过一个错误——把一个二叉搜索树插成了斜树,结果查找效率从 O(log n) 直接掉到了 O(n)。
2. 满二叉树
满二叉树,也叫完美二叉树。它的定义是:除了叶子节点外,每个节点都有两个子节点,并且所有叶子节点都在同一层。
举个例子,深度为 3 的满二叉树,节点总数是 1 + 2 + 4 = 7 个。每一层的节点数都是满的,没有空缺。
满二叉树有什么好处?它的空间利用率最高,没有浪费的指针。我在做堆排序时,底层就是用满二叉树(实际上是完全二叉树)来实现的。
3. 完全二叉树
完全二叉树比满二叉树稍微宽松一些。它的定义是:除了最后一层外,其他层都是满的,并且最后一层的节点都靠左排列。
你想想看,满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树。比如深度为 3 的完全二叉树,最后一层可以只有 1 个节点,但必须靠左放。
为什么完全二叉树这么重要?因为它可以用数组来存储,不需要指针。堆排序、优先队列的底层都是完全二叉树。我在项目中用数组实现完全二叉树时,索引计算特别方便:
// 用数组存储完全二叉树
// 父节点下标为 i,则:
// 左孩子下标 = 2*i + 1
// 右孩子下标 = 2*i + 2
// 父节点下标 = (i-1)/2
二叉树的性质
二叉树有 5 条重要性质,我建议你背下来。为什么?因为很多算法题和面试题都直接考这些性质。
| 编号 | 性质内容 | 说明 |
|---|---|---|
| 性质1 | 第 i 层最多有 2^(i-1) 个节点 | i ≥ 1,根节点在第 1 层 |
| 性质2 | 深度为 k 的二叉树最多有 2^k - 1 个节点 | k ≥ 1,满二叉树时达到最大值 |
| 性质3 | 叶子节点数 = 度为 2 的节点数 + 1 | 记作 n0 = n2 + 1 |
| 性质4 | 完全二叉树的节点编号规律 | 父节点 i,左孩子 2i,右孩子 2i+1 |
| 性质5 | 完全二叉树深度为 ⌊log₂n⌋ + 1 | n 为节点总数 |
我个人觉得性质3是最容易被忽略的。有一次我在做树的序列化时,需要判断一棵树是不是合法的二叉树,就用到了 n0 = n2 + 1 这个关系。你想想看,如果一棵树有 10 个叶子节点,那度为 2 的节点一定是 9 个,加起来就是 19 个节点。这个性质在验证树结构时特别有用。
知识体系总览
下面我用一张图来总结本章的核心内容。这张图展示了二叉树的分类体系以及它们之间的关系:
这张图把二叉树的知识点串起来了。你从根节点往下看,左边是定义和存储方式,中间是三种特殊形态,右边是五大性质。最下面那条总结是我特别想强调的——完全二叉树是连接「树」和「数组」的桥梁,理解了这一点,堆排序、优先队列这些高级内容你就能轻松拿下。
好了,关于二叉树的定义、特殊形态和性质,我们就聊到这里。记住,斜树是退化形态要避免,满二叉树是理想形态,完全二叉树是工程中最实用的。下一节我们会深入二叉树的遍历,到时候我会分享一些我在实际项目中踩过的坑。