相关笔记

概览

本节介绍如何用链式数据结构表示有根树(rooted trees)。首先讨论二叉树的标准表示方法(每个节点含 leftrightparent 指针),然后介绍left-child, right-sibling 表示法,该方法可以将任意多叉树统一转换为二叉树形式,每个节点只需固定数量的指针。

要点列表:

  • 二叉树每个节点包含 p(父指针)、left(左孩子)、right(右孩子)三个指针
  • left-child, right-sibling 表示法用两个指针表示任意多叉树,空间为
  • 树的表示方法选择取决于具体应用场景
  • 二叉堆 使用数组表示完全二叉树,是树的另一种重要表示方式

知识结构总览

flowchart TD
    A["10.3 有根树的表示"] --> B["二叉树表示"]
    A --> C["多叉树表示"]
    A --> D["其他表示方式"]

    B --> B1["节点属性: p, left, right"]
    B --> B2["T.root 指向根节点"]
    B --> B3["NIL 表示空指针"]

    C --> C1["固定子节点数 k"]
    C --> C2["left-child, right-sibling"]
    C2 --> C2a["x.left-child: 最左孩子"]
    C2 --> C2b["x.right-sibling: 右兄弟"]

    D --> D1["数组表示(完全二叉树/堆)"]
    D --> D2["仅父指针(并查集)"]
    D --> D3["其他应用特定表示"]

二叉树的表示

2.1 节点结构与属性

二叉树的链式表示

二叉树 的每个节点是一个对象,包含以下属性:

属性说明
key节点存储的键值
p指向父节点的指针
left指向左孩子的指针
right指向右孩子的指针

此外,树对象 有属性 T.root 指向根节点。

约定:

  • 如果 ,则 根节点
  • 如果 ,则 没有左孩子
  • 如果 ,则 没有右孩子
  • 如果 ,则树为空

2.2 二叉树的结构示意

        T.root → [15]
                  /    \
              [9]      [20]
             /   \       \
          [22]   [13]    [25]
                 /   \
              [12]   [28]

对应的属性表:

节点keypleftright
117689
2145NILNIL
3129NILNIL
420610NIL
533102NIL
615NIL14
7289NILNIL
8221NILNIL
913137
10254NIL5

空间复杂度

一棵 个节点的二叉树,每个节点有3个指针(pleftright),共需 个指针空间。由于 个节点的树有 条边,恰好有 个非NIL指针,其余 个指针为NIL(根的 p 为NIL,加上 个节点各自的 leftright 中为NIL的部分)。

2.3 基本操作的复杂度

操作时间复杂度说明
访问根节点T.root
访问父节点x.p
访问左/右孩子x.left / x.right
判断是否为叶节点x.left == NIL and x.right == NIL
计算树的高度需要遍历所有节点
遍历所有节点前序/中序/后序遍历

有根树的统一表示:Left-Child, Right-Sibling

3.1 问题:如何表示任意多叉树?

多叉树表示的挑战

如果每个节点最多有 个孩子,可以用 child1, child2, ..., childk 表示。但这种方法存在两个问题:

  1. 子节点数无界时不可行: 无法预先分配足够的属性
  2. 空间浪费: 即使 有界,如果大多数节点只有少量孩子,会浪费大量内存

目标: 找到一种表示方法,使得:

  • 每个节点使用固定数量的指针(不依赖于子节点数)
  • 总空间为 为节点数)
  • 能正确表示任意有根树的结构

3.2 Left-Child, Right-Sibling 表示法

Left-Child, Right-Sibling 表示法

每个节点 包含以下属性:

属性说明
key节点的键值
p指向父节点的指针
x.left-child指向 最左孩子的指针
x.right-sibling指向 紧邻右兄弟的指针

约定:

  • 如果 没有孩子,则
  • 如果 是其父节点的最右孩子,则
  • 指向树的根节点

3.3 从多叉树到二叉树的转换

Left-Child, Right-Sibling 表示法的本质是将任意多叉树转换为二叉树

原始多叉树:

          A
       /  |  \
      B   C   D
     / \       \
    E   F       G
       / \
      H   I

Left-Child, Right-Sibling 表示(二叉树形式):

          A
        /
       B ——→ C ——→ D
      /           /
     E ——→ F     G
         /
        H ——→ I

转换规则:

  1. 每个节点的第一个孩子变成 left-child
  2. 同一父节点下的兄弟从左到右通过 right-sibling 链接
  3. 原始多叉树中节点的父子关系被拆分为两条路径:
    • 父 → 最左孩子(通过 left-child
    • 最左孩子 → 其他兄弟(通过 right-sibling

3.4 对应的属性表

以上面的多叉树为例:

节点keypleft-childright-sibling
1ANILBNIL
2BAEC
3CANILD
4DAGNIL
5EBNILF
6FBHNIL
7GDNILNIL
8HFNILI
9IFNILNIL

3.5 空间复杂度证明

空间复杂度

定理: Left-Child, Right-Sibling 表示法对任意 节点有根树使用 空间。

证明:

  • 每个节点有3个指针(pleft-childright-sibling),共 个指针
  • 个节点的有根树有 条边
  • 在 left-child, right-sibling 表示中:
    • 每个非根节点恰好有一个 p 指针指向它(来自其父节点),共 个非NIL的 p 指针
    • 每个有孩子的节点恰好有一个非NIL的 left-child 指针(指向其最左孩子),非NIL的 left-child 指针数等于有孩子的节点数
    • 每个非最右孩子恰好有一个非NIL的 right-sibling 指针,非NIL的 right-sibling 指针数等于总孩子数减去节点数(因为每个父节点的孩子链中最后一个的 right-sibling 为NIL)
  • 【分类计数(三类指针分别统计)】 总非NIL指针数 =
  • 【关键简化(有孩子节点数抵消)】 由于 个节点的树有 条边,总孩子数 =
  • 【代入求值】 总非NIL指针数 =
  • 因此总空间为

3.6 操作复杂度分析

操作时间复杂度说明
访问父节点x.p
访问第一个(最左)孩子x.left-child
访问所有孩子 为孩子数,沿 right-sibling 遍历
访问右兄弟x.right-sibling
访问左兄弟需要从父节点的最左孩子开始遍历
判断是否为叶节点x.left-child == NIL
计算子节点数沿 right-sibling 计数
遍历整棵树DFS/BFS

注意:访问左兄弟的代价

Left-Child, Right-Sibling 表示法中,right-sibling 是单向的(只有向右的指针),因此:

  • 访问右兄弟
  • 访问左兄弟:需要从父节点的 left-child 开始,沿 right-sibling 遍历到当前节点的前一个,时间为 为当前节点的兄弟数)

如果应用中需要频繁访问左兄弟,可以考虑使用双链表来连接兄弟节点(增加一个 left-sibling 指针),但这会增加空间开销。


其他树的表示方法

树的多种表示方式

CLRS 指出,树的表示方式取决于具体应用:

  1. 数组表示(完全二叉树): 二叉堆 使用单个数组加上一个记录最后一个节点下标的属性来表示完全二叉树。节点 的父节点为 ,左孩子为 ,右孩子为 。空间效率极高,但仅适用于完全二叉树

  2. 仅父指针表示: 第19章(不相交集合)中的树只需要父指针,不需要指向孩子的指针。因为操作只沿从叶到根的方向进行(FIND-SET 和 UNION)

  3. 带父指针的二叉树: 第12章(二叉搜索树)中的标准表示,每个节点有 pleftright,支持向上和向下的遍历

  4. 带线程指针的二叉树: 利用NIL指针存储遍历信息(前驱/后继),可以在不用栈的情况下进行中序遍历(见习题线索化二叉树)

选择原则: 哪种表示方式最好,取决于应用的具体需求。没有”万能”的树表示方法。


树的遍历

树的遍历方法

虽然CLRS第10章未详细讨论遍历,但遍历是树的最基本操作,在第12章(二叉搜索树)中会大量使用:

二叉树的遍历:

遍历方式访问顺序典型应用
前序遍历(pre-order)根 → 左子树 → 右子树复制树结构、表达式前缀表示
中序遍历(in-order)左子树 → 根 → 右子树二叉搜索树的有序输出
后序遍历(post-order)左子树 → 右子树 → 根计算目录大小、释放树空间
层序遍历(level-order)按层从上到下、从左到右BFS在树上的应用

Left-Child, Right-Sibling 表示的遍历: 遍历一棵用 left-child, right-sibling 表示的树时,需要先访问节点的所有孩子(通过 left-childright-sibling),再递归处理每个孩子。前序遍历的伪代码如下:

PREORDER-TRAVERSE(x)
1  if x ≠ NIL
2      visit(x)                    // 访问当前节点
3      PREORDER-TRAVERSE(x.left-child)  // 遍历第一个孩子
4      PREORDER-TRAVERSE(x.right-sibling) // 遍历其余兄弟

遍历的复杂度

定理: 节点的树进行遍历的时间为

证明: 每个节点恰好被访问一次(visit 操作执行一次),每次访问执行常数时间的工作。递归调用的总次数等于节点数加上 NIL 检查的次数。因此总时间为


补充理解与拓展

Left-Child, Right-Sibling 表示法的深入分析

历史与地位: Left-Child, Right-Sibling(LCRS)表示法是CLRS推荐的标准方法,也是大多数算法教材中通用的多叉树表示方式。它最早可追溯到20世纪60年代的树结构研究。

核心优势:

  1. 统一性: 将所有有根树(包括二叉树、多叉树)统一为二叉树形式。二叉树实际上是多叉树的特例——每个节点最多有2个孩子
  2. 空间效率: 每个节点固定3个指针(pleft-childright-sibling),总空间 ,不随分支因子增长
  3. 实现简洁: 可以直接复用二叉树的代码框架,降低实现复杂度

实际应用场景:

  • 文件系统: Unix文件系统的目录结构就是一棵多叉树,每个目录可以有任意数量的子目录。LCRS表示可以高效存储这种结构
  • DOM树: HTML文档对象模型(DOM)中,每个元素可以有任意数量的子元素
  • 编译器抽象语法树(AST): 编译器将源代码解析为AST,其中运算符节点的操作数数量不固定(如函数调用的参数数量可变)
  • 组织架构图: 公司的组织架构中,每个管理者可以有任意数量的下属

与现代编程语言的对应:

  • C/C++:手动管理指针,直接实现LCRS
  • Java:class TreeNode { TreeNode firstChild; TreeNode nextSibling; }
  • Python:class Node: def __init__(self): self.first_child = None; self.next_sibling = None

树的遍历算法与递归关系

树的遍历是理解递归关系式的重要实例。每种遍历方式都自然对应一种递归模式:

递归与分治的联系:

  • 树的遍历本质上是分治策略的应用:将问题(遍历整棵树)分解为子问题(遍历子树)
  • 遍历的递归关系式为 ,其中
  • 由主定理的推广形式,

非递归遍历的实现:

  • 使用模拟递归:前序/中序/后序遍历都可以用显式栈实现,空间 为树高)
  • 使用队列实现层序遍历:这是BFS在树上的直接应用
  • Morris遍历:利用NIL指针存储遍历信息,实现 额外空间的遍历(修改树结构)

遍历的应用:

  • 二叉搜索树的中序遍历产生有序序列(第12章核心内容)
  • 后序遍历用于计算表达式树的值
  • 前序遍历用于序列化/反序列化树结构
  • 层序遍历用于求树的最短路径(无权图BFS)

易混淆点与辨析

误区:Left-Child, Right-Sibling 表示的二叉树与原始多叉树"等价"

错误理解: “LCRS表示就是将多叉树变成二叉树,两者完全一样”

正确理解: LCRS表示是一种编码方式,它保留了原始多叉树的所有结构信息,但二叉树形式下的操作语义不同

  • 在LCRS二叉树中,left 指针的含义是”第一个孩子”,right 指针的含义是”下一个兄弟”
  • 不能直接对LCRS二叉树执行标准二叉搜索树操作(如中序遍历不会产生有序序列)
  • LCRS二叉树的”左子树”是原始节点的子树,“右子树”是原始节点的兄弟链

记忆方法: LCRS中,左 = 孩子(向下),右 = 兄弟(向右)。二叉树的中序遍历在LCRS表示下对应的是”先访问子树,再访问兄弟”的特殊顺序,而非按键值排序。

误区:二叉树的数组表示总是不如链式表示

错误理解: “链式表示(指针)比数组表示更灵活,应该总是使用链式表示”

正确理解: 表示方式的选择取决于树的形状操作需求

比较维度链式表示(指针)数组表示
适用范围任意形状的树仅完全/近似完全二叉树
空间效率每节点3个指针无指针开销
父子访问(通过下标计算)
缓存友好性差(指针分散)好(连续内存)
动态增删灵活困难(需要移动元素)

二叉堆 使用数组表示的原因: 堆是完全二叉树,数组表示不会浪费空间;且堆操作(PARENT、LEFT、RIGHT)通过下标计算即可完成,无需指针;连续内存布局使缓存命中率更高。

结论: 完全二叉树用数组更优,一般二叉树/多叉树用链式表示更灵活。


习题精选

题号题目描述难度
10.3-1根据给定的属性表画出以索引6为根的二叉树
10.3-2编写 时间的递归过程,打印 节点二叉树的所有键⭐⭐
10.3-3编写 时间的非递归过程,使用栈作为辅助数据结构打印所有键⭐⭐
10.3-4编写 时间的过程,打印 left-child, right-sibling 表示的任意有根树的所有键⭐⭐
10.3-5编写 时间的非递归过程,只使用常数额外空间打印所有键(不修改树)⭐⭐⭐
10.3-6只用两个指针和一个布尔值实现 left-child, right-sibling 的功能⭐⭐⭐

视频学习指南

资源主题链接说明
MIT 6.006 Lecture 6Binary Trees, Part 1https://www.youtube.com/watch?v=6s3T08KlJlYErik Demaine 讲解二叉树的基本概念与表示
Abdul BariTree Data Structurehttps://www.youtube.com/watch?v=ZQXkLGFmGBw树的基本术语、二叉树和多叉树表示
mycodeschoolBinary Tree Traversalhttps://www.youtube.com/watch?v=1bmBwV3aSFs前序、中序、后序遍历的递归与非递归实现
WilliamFisetTree Algorithmshttps://www.youtube.com/watch?v=PHd3cCdS2Qk树算法系列,含多种树的表示方式讨论
GeeksforGeeksTree Traversalshttps://www.youtube.com/watch?v=1bmBwV3aSFs详细的遍历算法讲解与代码实现

教材原文

CLRS 第4版 10.3节原文

Linked lists work well for representing linear relationships, but not all relationships are linear. In this section, we look specifically at the problem of representing rooted trees by linked data structures. We first look at binary trees, and then we present a method for rooted trees in which nodes can have an arbitrary number of children.

We represent each node of a tree by an object. As with linked lists, we assume that each node contains a key attribute. The remaining attributes of interest are pointers to other nodes, and they vary according to the type of tree.

CLRS 第4版 10.3节原文(Left-Child, Right-Sibling)

Fortunately, there is a clever scheme to represent trees with arbitrary numbers of children. It has the advantage of using only space for any -node rooted tree. The left-child, right-sibling representation appears in Figure 10.7. As before, each node contains a parent pointer , and points to the root of tree . Instead of having a pointer to each of its children, however, each node has only two pointers:

  1. points to the leftmost child of node , and
  2. points to the sibling of immediately to its right.

If node has no children, then , and if node is the rightmost child of its parent, then .

CLRS 第4版 10.3节原文(其他表示方式)

We sometimes represent rooted trees in other ways. In Chapter 6, for example, we represented a heap, which is based on a complete binary tree, by a single array along with an attribute giving the index of the last node in the heap. The trees that appear in Chapter 19 are traversed only toward the root, and so only the parent pointers are present: there are no pointers to children. Many other schemes are possible. Which scheme is best depends on the application.


参见Wiki

第10章-基本数据结构 有根树