(3) 时间复杂度:O(n)

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

## (3) 时间复杂度:O(n)

<!-- code-audit: false_positive -->
> [!info] 已人工审读:这是正文思考题及答案说明,被错误放入代码围栏。
请大家思考,如果根结点下标为0应该怎么写?
这种情况下,根结点下标为0,i号结点左孩子下标为2×i+1,右孩子下标为2×i+2
## 习题4-3 已知由一棵二叉链表表示的二叉树,设计算法,输出二叉树的高度(只有根结点的二叉树高度为1)。二叉树结点数据类型定义如下: <!-- page 125 -->
typedef struct node{
    int data;
    struct node *lchild, *rchild;
}BTNode;
(1)给出算法的基本设计思想,关键之处给出注释。 (2) 根据设计思想,采用C或C++语言描述算法。 (3)说明所设计算法的时间复杂度。 【答案】 (1)对二叉树进行先序/中序/后序遍历,遍历时设置变量depth 保存当前子树的高度= max(左子树高度,右子树高度)+1,最后输出整棵树的高度即可。 (2) 代码如下:
int PostOrder(BTNode* p) //后序遍历
{
    if (p==NULL)
    return 0; // 该结点不存在
    int L=Postorder(p-&gt;lchild);
    int R=Postorder(p-&gt;rchild);
    int depth=max(L, R)+1; // 访问当前结点visit(p)
    return depth;
}
(3) 时间复杂度:O(n) ## 习题4-4 设树T采用孩子兄弟链表表示(数据域、孩子域、兄弟域),设计算法计算树T的度。 (1)写出树结点的数据结构定义。 (2) 给出算法的基本设计思想。 (3)根据设计思想,采用C或C++ 语言描述算法,关键之处给出注释 (4)说明所设计算法的时间复杂度。 【答案】(1)定义如下:
typedef struct node{
    int data;
    struct node *firstchild, *nextbro;
}TNode;
(2)核心思想:一个父结点的度,等于其第一个孩子结点的兄弟链表的长度。 算法通过一个后根序的递归函数 Post(p) 遍历树,该函数的主要功能是计算并返回以结点 p 开头的兄弟链表的长度。同时,用一个全局变量 deqree记录在整个计算过程中遇到的最长的兄弟链长度。 (3) 代码如下: <!-- page 126 -->
(2)根据设计思想,采用C或C++; // 语言描述算法,关键之处给出注释。
int degree=0;
int Post(TNode* p)
{
    if (p==NULL)
    return 0;
    Post(p-&gt;firstchild);
    int d=1+Post(p-&gt;nextbro);
    degree=max(degree, d);
    return d;
}
## (4) 时间复杂度:O(n) ## 树的度:递归算法可视化
原资料配图
原资料配图 · 转写内容请回源核验

## 习题4-5

设二叉树以二叉链表形式存放,根结点的深度为1,一棵二叉树的繁茂程度定义为各层结点数与该层深度的乘积之和。试设计算法,求二叉树的繁茂程度。数据类型定义如下:

typedef struct node{
    int data;
    struct node*lchild, *rchild;
}BTNode;
<!-- page 127 --> 【答案】(1)按照二叉树的层次遍历 BFS,front 保存队头元素后一个位置下标,rear 保存队尾元素下标,last 保存上一层的最后一个元素下标,当本层访问完的时候,本层元素个数为rear-last,依次算出每一层的繁茂程度,并求出最大值。(2) 代码如下:
int ans(BTNode *T)
{
    BTNode* q[MAX_SIZE];
    int front=0, rear=0, depth=1;
    int 1ast=0; // 本层的最后一个结点下标
    if (T!=NULL) q[rear++]=T;
    BTNode *p; // 当前访问的结点p
    int sum=1*1; // 第一层的繁茂程度
    while(front&lt;rear)
    {
        p=q[front++];
        if (p-&gt;lchild!=NULL)
        q[rear++]=p-&gt;lchild;
        if (p-&gt;rchild!=NULL)
        q[rear++]=p-&gt;rchild;
        if (front&gt;last) // 该层已访问完
        depth++;
        printf(&quot;%d %d %d\n&quot;, front, rear, depth);
        sum+=(rear-front)*depth;
        last=rear-1;
    }
    return sum;
}
(3) // 时间复杂度:O(n) 空间复杂度:O(n)
## 习题4-6 给定一棵二叉树,设计算法检查它是否是镜像对称的,如果是镜像对称则返回1,否则返回0。例如下图所示就是镜像对称的。 <!-- page 128 -->
原资料配图
原资料配图 · 转写内容请回源核验

(1)给出算法的基本设计思想。

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

(3)说明所设计算法的时间复杂度。

【解析】本题采用先序/中序/后序遍历都可以,设置两个指针p、q访问即可。

【答案】(1)按照先序遍历,在遍历的时候设置两个指针p、q,分别指向当前访问结点和它对称的位置,每次比较p和q的值是否相同。当p递归左(右)孩子的时候,q递归右(左)孩子,这样可以保证p和q一直是对称位置,如果发现p 和 q的值不一样则返回0。

(2) 代码如下:

int PreOrder(BTNode *p,BTNode *q)
{
    if (p==NULL &amp;&amp; q==NULL)
    return 1;
    if (p==NULL || q==NULL)
    return 0;
    if (p-&gt;data!=q-&gt;data)
    return 0;
    if (Preorder(p-&gt;lchild, q-&gt;rchild)==0)
    return 0;
    if (Preorder(p-&gt;rchild, q-&gt;lchild)==0)
    return 0;
    return 1;
}
(3)时间复杂度:O(n) ## 习题4-7 (13 分) 二叉树中两个结点p和g 的最低公共祖先是二叉树中离根结点尽可能远且同时是p 和 g 祖先的结点,结点的祖先可以是自己。给定一棵非空二叉树T,采用二叉链表存储。例如,对于下图所示的非空二叉树T,结点3和结点4的最低公共祖先是结点3,结点2和结点4的最低公共祖先是结点1。 二叉树结点定义如下: <!-- page 129 -->
typedef struct node
{
    int data;
    struct node *left, *right;
}BTree;
root 为指向T的根结点的指针,请设计算法求出p和q的最低公共祖先的地址,要求: 1)给出算法的基本设计思想。 2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。 ## 【答案】 (1) 算法设计思想: 使用递归后序遍历二叉树,对于当前访问的结点root: • 若 root == p 或 root == q ,直接返回 root ; • 递归分别在左子树、右子树查找 p 和 q ; • 若左右子树递归结果均不为 NULL,说明 p 和 q 分别在左右子树中,root 即为最近公共祖先; • 若其中一个子树递归结果为NULL,则返回另一侧的结果(说明两结点均在某一子树中)。 (2) 算法代码:
BTree* PostOrder(BTree* root, BTree* p, BTree* q) {
    if (root == NULL)
    return NULL;
    if (root == p || root == q)
    return root; // 若当前结点等于p或q,直接返回
    BTree* left = PostOrder(root-&gt;left, p, q); //在左子树查找
    BTree* right = PostOrder(root-&gt;right, p, q); // 在右子树查找
    if (left != NULL &amp;&amp; right != NULL)
    return root; //左右子树都找到,当前root即为最近公
    共祖先
    //如果只在一个子树找到则返回该子树
    if (left == NULL) return right;
    else return left;
}
(3) 时间复杂度:O(n) 空间复杂度:O(n) ## 4.6.2 真题 ## 2014年42题 (13分)二叉树的带权路径长度(WPL)是二叉树中所有叶结点的带权路径长度之和。给定一棵二叉树T,采用二叉链表存储,结点结构如下: <table><tr><td>left</td><td>weight</td><td>right</td></tr></table> <!-- page 130 --> 其中叶结点的weight域保存该结点的非负权值。设root为指向T的根结点的指针,请设计求T的WPL的算法,要求: (1)给出算法的基本设计思想。 (2)使用C或C++语言,给出二叉树结点的数据类型定义。 (3)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。 带权路径长度WPL有两种计算方法: ① WPL的定义:WPL=所有(叶结点的权值 Wi×该结点深度 Di)求和。 ② 推导。如果该树的所有非叶结点满足以下条件:它的权值等于它的孩子结点权值之和。则WPL=所有非叶结点的权值和。 a n 因此本题有两种解法,都可以拿满分。 ## 【解法1】前序/中序/后序遍历,下面给出的是先序遍历 (1)先序遍历二叉树,设置全局变量WPL,每次p访问到叶子结点时p的权值与深度相乘(根结点深度 depth=0),再累加到 WPL 中。 (2) 类型定义:
typedef struct BTNode{
    int weight;
    struct BTNode *left, *right;
}BTNode;
(3) 算法如下:
int WPL=0; // WPL是全局变量
void PreOrder(BTNode *p, int depth) //当前结点p,深度depth
{
    if (p==NULL)
    return;
    if (p-&gt;left==NULL &amp;&amp; p-&gt;right==NULL) // p是叶结点
    wPL+=depth*p-&gt;weight;
    Preorder(p-&gt;left, depth+1); //递归调用左孩子
    Preorder(p-&gt;right, depth+1); // 递归调用右孩子
}
## 【解法2】后序遍历 (1)后序遍历二叉树,设置全局变量WPL,如果访问到叶结点则把权值返回给父亲,如果是非叶结点则该结点权值等于左、右孩子的权值和,累加到WPL中,并将该权值返回给父亲。 (2) 类型定义:
typedef struct BTNode{
    int weight;
    struct BTNode *left, *right;
}BTNode;
<!-- page 131 --> (3) 算法如下: <!-- verified-by-hand --> > [!success] 已结合原文第 131 页及 Markdown 上下文人工校对 > 递归调用函数名大小写与定义不一致;合并行虽合法但应恢复清晰的两条语句。
int WPL = 0;
int PostOrder(BTNode *p)
{
    if (p == NULL) return 0;
    int L = PostOrder(p-&gt;left);
    int R = PostOrder(p-&gt;right);
    if (p-&gt;left == NULL &amp;&amp; p-&gt;right == NULL) return p-&gt;weight;
    WPL += L + R;
    return L + R;
}
## 2017年41题 (15分)请设计一个算法,将给定的表达式树(二叉树)转换为等价的中缀表达式(通过括号反映操作符的计算次序)并输出。例如,当下列两棵表达式树作为算法的输入时,输出的等价中缀表达式分别为(a+b)×(c×(-d))和(a×b)+(-(c-d))。
原资料配图
原资料配图 · 转写内容请回源核验
原资料配图
原资料配图 · 转写内容请回源核验

二叉树结点定义如下:

typedef struct node{
    char data[10];
    struct node *left, *right;
}BTree;
// 存储操作数或操作符 要求: (1)给出算法的基本设计思想。 (2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。 ## 【解法】中序遍历,递归 (1)中序遍历二叉树,对于当前访问的非空结点p,则先输出“(”,然后递归调用左子树,输出p的权值,递归调用右子树,输出“)”,如果p是根或者叶结点则不需要输出“(”或“)”。 (2) 算法如下: <!-- page 132 -->
void InOrder(BTree* p, BTree* 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;);
## 2022年41题 (13分)已知非空二叉树T的结点值均为正整数,采用顺序存储方式保存,数据结构定义如下:
typedef struct {
    int SqBiTNode[MAX_SIZE];
    int ElemNum;
} SqBiTree;
//MAX_SIZE为已定义常量
//保存二叉树结点值的数组
//实际占用的数组元素个数
T 中不存在的结点在数组 SqBiTNode 中用 -1 表示。例如,对于下图所示的两棵非空二叉树 T1 和 T2, T1 的存储结果如下:
T1.SqBiTNode
<table><tr><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
T2 的存储结果如下:
T2.SqBiTNode
<table><tr><td rowspan=1 colspan=1>40</td><td rowspan=1 colspan=1>50</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>-1</td><td rowspan=1 colspan=1>-1</td><td rowspan=1 colspan=1>-1</td><td rowspan=1 colspan=1>-1</td><td rowspan=1 colspan=1>35</td><td rowspan=1 colspan=1></td></tr></table>
T2.ElemNum = 11
请设计一个尽可能高效的算法,判定一棵采用这种方式存储的二叉树是否为二叉搜索树,若是,则返回true,否则,返回false。要求: (1)给出算法的基本设计思想。 (2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。 ## 【解法】中序遍历 (1)如果当前结点的数组下标为i则左孩子下标为 2i+1,右孩子下标为2i+2,对该树进行递归的中序遍历,pre保存当前访问结点的前驱结点的权值,如果SqBiTree[i]<pre则说明中序遍历得到的不是升序,即当前子树返回false,如果SqBiTree[i]>=pre 且左子树和右子树都确定是二叉排序树则返回 true。 (2) 算法如下:
int pre=0; //全局变量pre保存中序前驱的权值,初值0
bool InOrder(SqBiTree *T, int i)
{
    /*指针bt指向二叉树的结构体,i是当前访问的下标*/
    if (i&gt;=T-&gt;ElemNum |I T-&gt;SqBiTNode[i]==-1) // 当前结点为空
    return true;
    if (Inorder(T, 2*i+1) != true) // 递归调用左子树
    return false; // 左子树不符合定义
    if (pre&gt;T-&gt;SqBiTNode[i])
    return false;
    pre=T-&gt;SqBiTNode[i];
    if (Inorder(T, 2*i+2) != true)
    return false;
    return true;
}
<!-- page 133 --> //访问当前结点visit(p) // 前驱大于当前结点,不符合定义 //更新pre //递归调用右子树 // 右子树不符合定义 //根下标为i的子树符合定义 ## 5图 ## 5.1 图概述 ## 5.1.1 图的基本概念 想象一下,我们把图看作一张城市地图。地图上的每个城市就是一个顶点(Vertex),连接城市之间的道路就是一条边(Edge)。 • 路径:是从一个顶点到另一个顶点所经过的一系列边的集合。你从家里(起点)出发,经过几条街道(边),最终到达学校(终点)。这一整段路程就是一条路径。 • 环:也叫回路,是一条特殊的路径,它的起点和终点是同一个顶点。 • 自环:是一条从某个顶点出发,又立刻回到自身的边。 • 重边:是指在两个相同的顶点之间,存在多于一条的边。 • 无向图:图中的边(道路)没有方向,是双向的。 • 有向图:图中的边(道路)有明确的方向,是单行道。 · 无权图:所有的边都没有“权重”或“成本”的概念。我们只关心两个顶点之间有没有路,不关心路有多长。在地铁线路图上,我们通常只关心从A站到B站需要经过几站,而不关心每一站之间的具体距离。 • 有权图:每一条边都被赋予了一个数值,称为权重。连接两个城市的道路不仅存在,还标有长度(公里),这就是边的权重。 20 • 连通图:在一个无向图中,任意两个顶点之间都存在路径。 • 强连通图:在一个有向图中,任意两个顶点之间都存在路径。 • 完全图:在一个无向图中,任意两个顶点之间都存在边。 • 有向完全图:在一个有向图中,任意两个顶点之间都存在边。 • 子图:从原图中选取一部分顶点和一部分边组成的图。好比从一张完整的全国地图中,单独把“长三角地区”的地图拎出来。 van •极大连通子图:指在保持“连通”这个性质的前提下,规模最大的子图。你不能再从原图中添加任何顶点或边到这个子图里,否则它就不再是连通的了。就是一个连通分量。 连通分量:对于一个非连通的无向图,图中“抱团”的、内部任意互通的极大连通子图。 • 生成树:在一个连通图中,选取一部分边,刚好能连接起所有的顶点,并且不形成任何环。这个由“最少的边”构成的骨架,就是一棵生成树。 <!-- page 134 --> 若一个图中没有自环和重边,它被称为简单图,考试中的图默认简单图。 本章默认:图的顶点集合为V,顶点数V为n,边集合为E,边数E为e。