4树

原始讲义 · 非增强校订版 · 原书检索 · 文字用于定位,公式与图表请回原页核验 · 本次导入快照

## 4树

由2014年真题引入

【2014年真题,15分】二叉树的带权路径长度(WPL)是二叉树中所有叶结点的带权路径长度之和。给定一棵二叉树T,采用二叉链表存储,结点结构如下:

<table><tr><td>left 看伽</td><td>weight</td><td>right</td></tr></table>

其中叶结点的 weight 域保存该结点的非负权值。设 root 为指向T的根结点的指针,请设计求T的 WPL的算法,要求:1)给出算法的基本设计思想。

2) 使用C或C++语言,给出二叉树结点的数据类型定义。

3)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。

<!-- page 107 -->

<!-- code-audit: false_positive -->
> [!info] 已人工审读:这是存储结构示意图标题,不是 C 代码。
二叉树(二叉链表)
{
<!-- verified-by-hand --> > [!success] 已结合原文第 107 页及 Markdown 上下文人工校对 > 结构体定义缺少左花括号。
typedef struct BTNode
{
    ElemType data;
    struct BTNode *lchild, *rchild;
} BTNode;
## 4.1 树概述 ## 4.1.1 思考流程 树和图的题目一般不会特别要求复杂度,如果没有特别要求的话,考试中只要做出来就可以直接写,不用考虑优化问题。 一般都会考察树的遍历,除非特别说明,否则遍历就用递归写,不要写非递归。 ## 4.1.2 考察结构 树和森林 <!-- code-audit: false_positive --> > [!info] 已人工审读:这是存储结构示意图标题,不是 C 代码。
树和森林(孩子兄弟表示法)
{
firstchild 是指向该结点第一个儿子结点的指针,nextbro是指向下一个兄弟结点的指针 <!-- verified-by-hand --> > [!success] 已结合原文第 107 页及 Markdown 上下文人工校对 > 孩子兄弟结点结构体缺少左花括号。
typedef struct TNode
{
    ElemType data;
    struct TNode *firstchild, *nextbro;
} TNode;
树和森林(结点用双亲表示法存储,常用于并查集) 一般采用数组表示,father数组保存每个结点的父亲结点在数组中的下标:
int father[MAX_SIZE];
或者
typedef struct TNode
{
    Elemtype data;
    struct TNode *father;
}TNode;
//保存每个结点的父亲结点在数组中的下标

//每个结点的权值
//father是指向该结点父亲结点的指针
//树结点
二叉树 <!-- code-audit: false_positive --> > [!info] 已人工审读:这是存储结构示意图标题,不是 C 代码。
二叉树(顺序)
{
<!-- page 108 --> <!-- verified-by-hand --> > [!success] 已结合原文第 108 页及 Markdown 上下文人工校对 > 顺序二叉树结构体缺少左花括号。
typedef struct
{
    int SqBiTNode[MAX_SIZE];
    int ElemNum;
} SqBiTree;
对于完全二叉树,只要从根起按层序存储即可,依次自上而下、自左至右存储结点元素,即将完全二叉树上编号为i的结点元素存储在如上定义的一维数组中下标为i-1的位置。可以用-1或者0表示对应结点不存在。 <table><tr><td rowspan=1 colspan=1>T1.SqBiTNode</td><td rowspan=1 colspan=1>40</td><td rowspan=1 colspan=1>25</td><td rowspan=1 colspan=1>60</td><td rowspan=1 colspan=1>-1</td><td rowspan=1 colspan=1>30</td><td rowspan=1 colspan=1>-1</td><td rowspan=1 colspan=1>80</td><td rowspan=1 colspan=1>-1</td><td rowspan=1 colspan=1>-1</td><td rowspan=1 colspan=1>27</td><td rowspan=1 colspan=1></td><td rowspan=1 colspan=1></td></tr></table> T1.ElemNum = 10
原资料配图
原资料配图 · 转写内容请回源核验

## 4.2 树的递归遍历

树的遍历是408代码题的常考点,包括2014、2017、2022年,只要考察树必然需要用到遍历,一般是考察树的遍历+某种树的定义(如二叉搜索树),而二叉树的遍历包括先序中序后序遍历,他们又分为递归和非递归两种方式,每一种遍历的非递归逻辑都完全不一样,复习起来很痛苦也没有必要,而递归的代码逻辑非常简单,所以考试中除了特殊情况,树的遍历请一定使用递归(递归简单好用)。

<!-- page 109 -->

## 4.2.1 二叉树的递归遍历

递归,从本质上讲,是分治思想的一种完美体现。分治策略就是“分而治之”,它将一个复杂的大问题分解成若干个规模更小、但结构与原问题相同的子问题,然后逐个解决。 W

在二叉树的遍历中,分:将“遍历整棵树”这个大问题,分解为三个更小的问题:

• 访问根结点本身。

• 遍历左子树(一个规模更小的相同问题)。

• 遍历右子树(另一个规模更小的相同问题)。

因此,我们可以把递归想象成一个“任务分包”的过程。你作为“总指挥”,只需要应用分治思想,关注当前结点,并把子树的任务“外包”出去。 m

处理二叉树我们总共需要完成三个工作:

• 处理好你当前手头的工作(即访问结点 p 本身)。

• 把遍历左子树的整个任务,完全甩给你的“左下属”(处理左子树 order(p->lchild))。

• 把遍历右子树的整个任务,完全甩给你的“右下属”(处理右子树 order(p->rchild))。

核心信念:当你调用 order(p->lchild) 时,你不需要关心它内部是如何一步步执行的。你只需要完全信任这个调用,相信它一旦执行,就一定能帮你把整个左子树完美地遍历一遍。同理,order(p->rchild)也一样。

有了这个“信任”,你的工作就变得极其简单:你只需要考虑三件事(访问自己、调用左下属、调用右下属)的执行顺序即可。把复杂的大问题,分解成了在当前结点的一个极小问题。

<!-- code-audit: false_positive -->
> [!info] 已人工审读:整块是讲解先中后序步骤的中文提纲,不是可编译代码。
遍历 order
• 先序遍历 PreOrder:
1. 访问自己 visit(p);
2. 调用左 PreOrder(p-&gt;lchild);
3. 调用右 PreOrder(p-&gt;rchild);
• 中序遍历 InOrder:
1. 调用左 InOrder(p-&gt;Ichild);
2. 访问自己 visit(p);
3. 调用右 InOrder(p-&gt;rchild);
• 后序遍历 PostOrder:
1. 调用左 PostOrder(p-&gt;Ichild);
2. 调用右 PostOrder(p-&gt;rchild);
3. 访问自己 visit(p);
先访问自己是先序,在中间访问自己是中序,最后访问自己是后序
先中后序的递归遍历时间复杂度为 O(n), 空间复杂度为 O(h),n 是总结点数,h 是树高,如果不确定树高则空间复杂度为O(n)。
//先序(根左右)
void PreOrder(BTNode *p)
{
    if (p==NULL) return;
    visit(p);
    Preorder(p-&gt;lchild);
    Preorder(p-&gt;rchild);
}
<!-- page 110 --> <!-- verified-by-hand --> > [!success] 已结合原文第 110 页及 Markdown 上下文人工校对 > 多出闭括号且递归函数名大小写不一致。
void TreePreOrder(TNode *T)
{
    if (T == NULL) return;
    visit(T);
    TreePreOrder(T-&gt;firstchild);
    TreePreOrder(T-&gt;nextbro);
}
void InOrder(BTNode *p)
{
    if (p == NULL) return;
    InOrder(p-&gt;lchild);
    visit(p);
    InOrder(p-&gt;rchild);
}
void PostOrder(BTNode *p)
{
    if (p == NULL) return;
    PostOrder(p-&gt;lchild);
    PostOrder(p-&gt;rchild);
    visit(p);
}
// 对p结点的访问等
// 对p结点的访问等
## 4.2.2 树和森林的递归遍历 树和森林的递归遍历本质上是对孩子兄弟表示法结构的递归操作,重点在于掌握: • firstchild 表示第一个孩子(相当于“左子树”)。 • nextbro 表示兄弟结点(相当于“右子树”)。 即“左孩子右兄弟”。 也就是说,这种结构可以转化为“二叉树”模型来理解递归逻辑。 • 树和森林的先根遍历等价于二叉树的先序遍历。 • 树和森林的后根遍历等价于二叉树的中序遍历。 先根遍历(等价于先序遍历): 后根遍历(孩子都访问完再访问根,等价于中序遍历): <!-- page 111 -->
void TreePostorder(TNode *T)
{
    if (T == NULL) return;
    TreePostorder(T-&gt;firstchi1d); //先访问所有孩子
    visit(T); //最后访问根
    TreePostorder(T-&gt;nextbro); // 然后访问兄弟结点
}
关键还是掌握二叉树的三种遍历,在二叉树遍历的基础上稍加修改就得到树和森林的遍历 ## 4.3 树的做题方法 408中树算法考察的核心是树的遍历,所以在做题应确定遍历,以对应遍历作为框架做题。建议分成四步:审题一一写框架一一填充一一调整,其中第一步是最重要的。 请阅读2017年41题 (15分)请设计一个算法,将给定的表达式树(二叉树)转换为等价的中缀表达式(通过括号反映操作符的计算次序)并输出。例如,当下列两棵表达式树作为算法的输入时,输出的等价中缀表达式分别为(a+b)×(c×(-d))和(a×b)+(-(c-d))。
原资料配图
原资料配图 · 转写内容请回源核验
原资料配图
原资料配图 · 转写内容请回源核验

二叉树结点定义如下:

typedef struct node{
    char data[10];
    struct node *left, *right;
}BTree;
## 4.3.1 审题并分析 根据题目的要求,从根结点开始想一下按递归的方式需要当前结点做什么?然后根据访问顺序确定×序遍历。确定思路以后再回头看题检查是否满足了题目要求。 选择遍历方式的依据: 这个选择取决于处理当前结点p时,你是否需要其子树(左子树和右子树)的处理结果。 • 先序遍历 (PreOrder): 根 -> 左 -> 右 <!-- page 112 --> 适用场景:当对结点p的处理不依赖于其左、右子树的处理结果。 <!-- code-audit: false_positive --> > [!info] 已人工审读:这是遍历方式说明正文,不是 C 代码。
• 中序遍历 (InOrder): 左 -&gt; 根 -&gt; 右
适用场景:与中序遍历序列、中缀表达式有关时,最经典的用途是处理二叉排序树(BST)。因为中序遍历一个BST会得到一个有序的序列。 Y • 后序遍历 (PostOrder): 左 -> 右 -> 根 适用场景:当对结点p 的处理必须依赖于其左、右子树的处理结果时。 • 层序遍历(LevelOrder): 适用场景:当处理问题的逻辑和“层”或“广度”相关时。比如求最短路径、判断完全二叉树等。它通常需要借助一个队列来实现。 om 2017年41题分析:本题需要输出中缀表达式,因此考虑使用中序遍历。在访问每一个结点的时候需要输出该结点的data域。对于所有的不为原树也不为叶结点的子树,这种子树对应的式子两边都需要加括号。 ## 4.3.2 草稿纸上书写框架 写法1:只写递归过程部分(递归实现遍历,确定是什么序遍历),输出靠递归返回的参数。框架以中序为例: <!-- code-audit: false_positive --> > [!info] 已人工审读:这是同时展示“有返回值/无返回值”两种写法的教学模板,含返回值、xx 等占位符,刻意不作为完整可编译程序。
//需要返回值
int InOrder(BTNode *p) // 返回类型不一定是int
{
    if (p==NULL) return 返回值;
    Inorder(p-&gt;lchild); // 调用根据题目修改
    visit(p); //是最重要的部分,根据题目要求决定
    Inorder(p-&gt;rchild);
    return xx;

    // 不需要返回值
    void InOrder(BTNode *p)

    {
        if (p==NULL) return;
        Inorder(p-&gt;lchild); // 调用根据题目修改
        visit(p); //是最重要的部分,根据题目要求决定
        Inorder(p-&gt;rchild);
    }
}
如果递归过程即可完成所有功能则使用写法1,大部分问题都可以使用,建议优先使用。 写法2:程序分为两个部分:本题主过程部分ans(负责调用递归过程以及输入输出)、和对应的递归过程部分(递归实现遍历,确定是什么序遍历)。框架以中序为例: <!-- page 113 -->
printf(&quot;%s&quot;, p-&gt;data); //输出操作数
<!-- code-audit: false_positive --> > [!info] 已人工审读:这是带中文占位符的递归框架模板,意在让读者按题目填充,不是完整实现。
int InOrder(BTNode *p)
{
    if (p==NULL) return 返回值;
    Inorder(p-&gt;lchild); // 调用根据题目修改
    visit(p); // 是最重要的部分,根据题目要求决定
    Inorder(p-&gt;rchild);
    return  // 返回值;
}
void ans(BTNode *T)
{
    输出InOrder(T);  om
    完成其他工作
}
当写法1无法完全实现功能时,就需要补充主体过程部分,即此处的ans()。 2017年41题分析:本题不需要返回参数,框架如下:
void InOrder(BTNode *p)
{
    if (p==NULL) return; // p是空结点
    Inorder(p-&gt;lchild); // 递归调用左子树
    visit(p);
    Inorder(p-&gt;rchild); // 递归调用右子树
}
## 4.3.3 填充 visit(p) 确定对于p需要处理什么,并补充 visit(p)部分 2017年41题分析:visit(p)需要输出该结点的 data 域(注意是输出字符数组)。 或 <!-- code-audit: false_positive --> > [!info] 已人工审读:这是 visit 操作的伪代码说明,不是 C 代码。
输出 p-&gt;data
<!-- page 114 --> ## 4.3.4 再次读题并调整 把前面几步得到的基本代码写出来,并比对题目要求,补充或修改对应的语句。
void InOrder(BTNode *p)
{
    if (p==NULL) return; // p是空结点
    Inorder(p-&gt;lchild); //递归调用左子树
    printf(&quot;%s&quot;, p-&gt;data); //输出操作数
    Inorder(p-&gt;right, root); //递归调用右子树
} 专研
对于括号还未处理,需要判断以p为根的树是否为原树或者叶结点。如果满足要求则在开头输出左括号,在末尾输出右括号。 ka • 不为叶结点:if (p->left!=NULL∥ p->right!=NULL)。 • 不为根:需要主要过程调用时增加一个参数——根 root。if(p!=root)。 调整后:
void InOrder(BTree* p, BTree* root) // root是原二叉树的根
{
    if (p==NULL) return; // p是空结点
    if ((p-&gt;left!=NULL || p-&gt;right!=NULL) &amp;&amp; (p!=root))
    printf(&quot;(&quot;);
    Inorder(p-&gt;left, root); // 递归调用左子树
    printf(&quot;%s&quot;, p-&gt;data); //输出操作数
    Inorder(p-&gt;right, root); // 递归调用右子树
    if ((p-&gt;left!=NULL || p-&gt;right!=NULL) &amp;&amp; (p!=root))
    printf(&quot;)&quot;);
}
## 4.3.5 返回值或全局变量 很多时候题目需要我们统计一些信息,可以采用函数返回值或者全局变量的方式来统计,有时候两种方法都可以。 以计算二叉树的带权路径长度WPL为例:WPL=所有(叶结点的权值Wi×该结点深度 Di)求和。 ## 全局变量 这种方式更像是在遍历过程中“收集”信息。 • 核心思想:定义一个在所有递归调用中都共享的变量,在遍历的任何一步,只要满足条件,就直接修改这个共享变量。Y va ·适用场景:全局变量是一个全局的累加、计数或状态标记。 <!-- page 115 --> <!-- verified-by-hand --> > [!success] 已结合原文第 115 页及 Markdown 上下文人工校对 > 函数缺少闭括号,WPL 大小写和递归函数名不一致。
int WPL = 0;
void PreOrder(BTNode *p, int depth)
{
    if (p == NULL) return;
    if (p-&gt;left == NULL &amp;&amp; p-&gt;right == NULL)
        WPL += depth * p-&gt;weight;
    PreOrder(p-&gt;left, depth + 1);
    PreOrder(p-&gt;right, depth + 1);
}
## 函数返回值 这种方式是“自底向上”传递信息的核心。 • 核心思想:子问题的解(即对子树的递归调用)通过 return 语句返回给父问题,父问题利用这些返回的结果来计算自己的解。 • 适用场景:某过程的返回值表示的是以该子树为核心的二叉树的对应值。 ```c int postOrder(BTNode *p, int depth) //当前结点p,深度depth { if (p == NULL) return 0; // p是空结点 int L_wPL = postorder(p->left, depth+1); int R_wPL = postorder(p->right, depth+1); // visit(p); if (p->left==NULL && p->right==NULL) // p是叶结点 return depth*p->weight; else return L_WPL + R_WPL; } ```