04. 【解答】

2027 · OCR 辅助 · 原书检索 · 文字用于定位,公式与图表请回原页核验 · 本次导入快照

## 04. 【解答】

根据完全二叉树的定义,具有 $n$ 个结点的完全二叉树与满二叉树中编号从 $1 \sim n$ 的结点一一对应。算法思想:采用层次遍历算法,将所有结点加入队列(包括空结点)。遇到空结点时,查看其后是否有非空结点。若有,则二叉树不是完全二叉树。

算法实现如下:

bool IsComplete(BiTree T){
//本算法判断给定二叉树是否为完全二叉树
InitQueue(Q);
if(!T)
return true; //空树为满二叉树
EnQueue(Q,T);
while(!IsEmpty(Q)) {
DeQueue(Q,p);
if(p) { //结点非空,将其左右子树入队列
EnQueue(Q,p->lchild);
EnQueue(Q,p->rchild);
}
else
//结点为空,检查其后是否有非空结点
while(!IsEmpty(Q)) {
DeQueue(Q,p);
if(p) //结点非空,则二叉树为非完全二叉树
return false;
}
}
return true;
}</rear){>

<!-- page: 172 -->

2027年数据结构考研复习指导

## 05. 【解答】

计算一棵二叉树 b 中所有双分支结点数的递归模型 f(b) 如下:

```r
f(b) = 0
若 $b = \text{NULL}$
f(b) = f(b->lchild) + f(b->rchild) + 1
若 *b 为双分支结点
f(b) = f(b->lchild) + f(b->rchild)
其他情况(*b 为单分支结点或叶结点) 具体算法实现如下:
int DsonNodes(BiTree b) {
if (b == NULL)
return 0;
else if (b->lchild != NULL && b->rchild != NULL) // 双分支结点
return DSonNodes(b->lchild) + DsonNodes(b->rchild) + 1;
else
return DSonNodes(b->lchild) + DsonNodes(b->rchild);
}
当然,本题也可以设置一个全局变量 Num,每遍历到一个结点时,判断每个结点是否为分支结点(左右结点都不为空,注意是双分支),若是,则 Num++。 ## 06. 【解答】 采用递归算法实现交换二叉树的左右子树,首先交换 b 结点的左孩子的左右子树,然后交换 b 结点的右孩子的左右子树,最后交换 b 结点的左右孩子,当结点为空时递归结束(后序遍历的思想)。算法实现如下:
void swap(BiTree b) {
//本算法递归地交换二叉树的左右子树
if (b) {
swap(b->lchild); // 递归地交换左子树
swap(b->rchild); // 递归地交换右子树
temp=b->lchild; // 交换左右孩子结点
b->lchild = b->rchild;
b->rchild = temp;
}
}
## 07. 【解答】 设置一个全局变量 i(初值为 1)来表示进行先序遍历时,当前访问的是第几个结点。然后可以借用先序遍历的代码模型,先序遍历二叉树。当二叉树 b 为空时,返回特殊字符 '#';当 $k == i$ 时,该结点即要找的结点,返回 b->data;当 $k \neq i$ 时,递归地在左子树中查找,若找到则返回该值,否则继续递归地在右子树中查找,并返回其结果。对应的递归模型如下:
f(b, k) = '#'
当 $b = \text{NULL}$ 时
f(b, k) = b->data
当 $i = k$ 时
f(b, k) = ((ch = f(b->lchild, k)) == '#' ?f(b->rchild, k) : ch)
其他情况 算法的实现如下:
int i = 1; // 遍历序号的全局变量
ElemType PreNode(BiTree b, int k) {
//本算法查找二叉树先序遍历序列中第 k 个结点的值
if (b == NULL)
return '#';
if (i == k)
return b->data;
i++;
ch = PreNode(b->lchild, k);
// 遍历序号的全局变量
// 已经查找二叉树先序遍历序列中第 k 个结点的值
// 空结点,则返回特殊字符
// 相等,则当前结点即第 k 个结点
// 下一个结点
// 左子树中递归寻找

<!-- page: 173 -->

第5章 树与二叉树
161

```javascript
if(ch != '#') //在左子树中,则返回该值
return ch;
ch=PreNode(b->rchild,k); //在右子树中递归寻找
return ch;
本题实质上就是一个遍历算法的实现,只不过用一个全局变量来记录访问的序号,求其他遍历序列的第 k 个结点也采用相似的方法。二叉树的遍历算法可以引申出大量的算法题,因此考生务必要熟练掌握二叉树的遍历算法。 ## 08. 【解答】 删除以元素值 x 为根的子树,只要能删除其左右子树,就可以释放值为 x 的根结点,因此宜采用后序遍历。算法思想:删除值为 x 的结点,意味着应将其父结点的左(右)孩子指针置空,用层次遍历易于找到某结点的父结点。本题要求删除树中每个元素值为 x 的结点的子树,因此要遍历完整棵二叉树。算法实现如下:
void DeleteXTree(BiTree &bt) { //删除以 bt 为根的子树
if (bt) {
DeleteXTree(bt->lchild);
DeleteXTree(bt->rchild); //删除 bt 的左子树、右子树
free(bt); //释放被删结点所占的存储空间
}
}
//在二叉树上查找所有以 x 为元素值的结点,并删除以其为根的子树
void Search(BiTree bt,ElemType x) {
BiTree Q[]; //Q 是存放二叉树结点指针的队列,容量足够大
if (bt) {
if (bt->data == x) { //若根结点值为 x,则删除整棵树
DeleteXTree(bt);
exit(0);
}
InitQueue(Q);
EnQueue(Q, bt);
while (!IsEmpty(Q)) {
DeQueue(Q, p);
if (p->lchild) //若左孩子非空
if (p->lchild->data == x) { //左子树符合则删除左子树
DeleteXTree(p->lchild);
p->lchild = NULL;
} //父结点的左孩子置空
else
EnQueue(Q, p->lchild); //左子树入队列
if (p->rchild) //若右孩子非空
if (p->rchild->data == x) { //右孩子符合则删除右子树
DeleteXTree(p->rchild);
p->rchild = NULL;
} //父结点的右孩子置空
else
EnQueue(Q, p->rchild); //右孩子入队列
}
}
## 09. 【解答】 算法思想:采用非递归后序遍历,最后访问根结点,访问到值为 x 的结点时,栈中所有元素均为该结点的祖先,依次出栈打印即可。算法实现如下: <!-- page: 174 --> 2027年数据结构考研复习指导
typedef struct{
BiTree t;
int tag;
}stack; //tag=0 表示左孩子被访问,tag=1 表示右孩子被访问
void Search(BiTree bt,ElemType x) {
//在二叉树 bt 中,查找值为 x 的结点,并打印其所有祖先
stack s[]; //栈容量足够大
top=0;
while (bt != NULL || top &amp;gt; 0) {
while (bt != NULL &amp;amp;&amp;amp; bt -&amp;gt; data != x) {
s [++ top].t = bt;
s [top].tag = 0;
bt = bt -&amp;gt; lchild;
} //沿左分支向下
}
if (bt != NULL &amp;amp;&amp;amp; bt -&amp;gt; data == x) {
printf(&quot;所查结点的所有祖先结点的值为: \n&quot;); //找到 x
for (i = 1; i &amp;lt;= top; i++)
printf(&quot;%d&quot;, s[i].t -&amp;gt; data); //输出祖先值后结束
exit(1);
}
while (top != 0 &amp;amp;&amp;amp; s[top].tag == 1)
top--;
if (top != 0) {
s [top].tag = 1;
bt = s[top].t -&amp;gt; rchild;
} //沿右分支向下遍历
}
} //while (bt != NULL || top &amp;gt; 0)
}
因为查找的过程就是后序遍历的过程,所以使用的栈的深度不超过树的深度。 ## 10. 【解答】 后序遍历最后访问根结点,即在递归算法中,根是压在栈底的。本题要找 p 和 q 的最近公共祖先结点 r,不失一般性,设 p 在 q 的左边。算法思想:采用后序非递归算法,栈中存放二叉树结点的指针,当访问到某结点时,栈中所有元素均为该结点的祖先。后序遍历必然先遍历到结点 p,栈中元素均为 p 的祖先。先将栈复制到另一辅助栈中。继续遍历到结点 q 时,将栈中元素从栈顶开始逐个到辅助栈中去匹配,第一个匹配(相等)的元素就是结点 p 和 q 的最近公共祖先。算法实现如下:
typedef struct{
BiTree t;
int tag; //tag=0 表示左孩子已被访问,tag=1 表示右孩子已被访问
}stack;
stack s[], s1[]; //栈,容量足够大
BiTree Ancestor(BiTree ROOT, BiTNode *p, BiTNode *q) {
//本算法求二叉树中 p 和 q 指向结点的最近公共结点
top=0; bt=ROOT;
while (bt != NULL || top &amp;gt; 0) {
while (bt != NULL) {
s [++ top].t = bt;
s [top].tag = 0;
bt = bt -&amp;gt; lchild;
} //沿左分支向下
while (top != 0 &amp;amp;&amp;amp; s[top].tag == 1) {
//假定 p 在 q 的左侧,遇到 p 时,栈中元素均为 p 的祖先
if (s[top].t == p) {
for (i = 1; i &amp;lt;= top; i++)
}

&lt;!-- page: 175 --&gt;

第5章 树与二叉树
163

```c
s1[i]=s[i];
top1=top;
}
//将栈s的元素转入辅助栈s1保存
if(s[top].t==q)
for(i=top;i&amp;gt;0;i--){//将栈中元素的树结点到s1中去匹配
for(j=top1;j&amp;gt;0;j--)
if(s1[j].t==s[i].t)
return s[i].t; //p和q的最近公共祖先已找到
}
top--;
//退栈
} //while
if(top!=0){
s[top].tag=1;
bt=s[top].t-&amp;gt;rchild;
}
//沿右分支向下遍历
} //while
return NULL; //p和q无公共祖先
## 11. 【解答】 采用层次遍历的方法求出所有结点的层次,并将所有结点和对应的层次放在一个队列中。然后通过扫描队列求出各层的结点总数,最大的层结点总数即二叉树的宽度。算法实现如下:
typedef struct{
BiTree data[MaxSize]; //保存队列中的结点指针
int level[MaxSize]; //保存data中相同下标结点的层次
int front, rear;
} Qu;
int BTWidth(BiTree b){
BiTree p;
int k, max, i, n;
Qu.front=Qu.rear=-1; //队列为空
Qu.rear++;
Qu.data[Qu.rear]=b; //根结点指针入队
Qu.level[Qu.rear]=1; //根结点层次为1
while (Qu.front&lt;qu.rear){
Qu.front++; //出队
p=Qu.data[Qu.front]; //出队结点
k=Qu.level[Qu.front]; //出队结点的层次
if (p-&amp;gt;lchild!=NULL){ //左孩子入队
Qu.rear++;
Qu.data[Qu.rear]=p-&amp;gt;lchild;
Qu.level[Qu.rear]=k+1;
}
} //while
max=0; i=0; //max保存同一层最多的结点数
k=1; //k表示从第一层开始查找
while (i&amp;lt;=Qu.rear){ //i扫描队中所有元素
n=0; //n统计第k层的结点数
while (i&amp;lt;=Qu.rear&amp;amp;&amp;amp;Qu.level[i]==k){
n++;
i++;
}
<!-- page: 176 --> 2027年数据结构考研复习指导
}
k=Qu.level[i];
if (n&amp;gt;max) max=n; //保存最大的 n
}
return max;
## 注意 本题队列中的结点,在出队后仍需要保留在队列中,以便求二叉树的宽度,所以设置的队列采用非环形队列,否则在出队后可能被其他结点覆盖,无法再求二叉树的宽度。 ## 12. 【解答】 对一般二叉树,仅根据先序或后序序列,不能确定另一个遍历序列。但对满二叉树,任意一个结点的左右子树均含有相等的结点数,同时,先序序列的第一个结点作为后序序列的最后一个结点,由此得到将先序序列 pre[11..h1] 转换为后序序列 post[12..h2] 的递归模型如下:
f(pre,11,h1,post,12,h2)=不做任何事情 h1&amp;lt;11 时
f(pre,11,h1,post,12,h2)=post[h2]=pre[11] 其他情况
取中间位置 half=(h1-11)/2:
将pre[11+1,11+half]左子树转换为post[12,12+half-1],
即f(pre,11+1,11+half,post,12,12+half-1):
将pre[11+half+1,h1]右子树转换为post[12+half,h2-1],
即f(pre,11+half+1,h1,post,12+half,h2-1)。
其中,post[h2]=pre[11]表示后序序列的最后一个结点(根结点)等于先序序列的第一个结点(根结点)。相应的算法实现如下:
void PreToPost(ElemType pre[], int 11, int h1, ElemType post[], int 12, int h2) {
int half;
if (h1 &amp;gt;= 11) {
post[h2] = pre[11];
half = (h1 - 11) / 2;
PreToPost(pre, 11 + 1, 11 + half, post, 12, 12 + half - 1); //转换左子树
PreToPost(pre, 11 + half + 1, h1, post, 12 + half, h2 - 1); //转换右子树
}
}
例如,有以下代码:
ElemType *pre=&quot;ABCDEFG&quot;;
ElemType post[MaxSize];
PreToPost(pre,0,6,post,0,6);
printf(&quot;后序序列:&quot;);
for (int i=0; i&amp;lt;=6; i++)
printf(&quot;%c&quot;,post[i]);
printf(&quot;\n&quot;);
执行结果如下:
后序序列:CDBFGEA
## 13. 【解答】 通常使用的先序、中序和后序遍历对于叶结点的访问顺序都是从左到右,这里选择中序递归遍历。算法思想:设置前驱结点指针 pre,初始为空。第一个叶结点由指针 head 指向,遍历到叶结点时,就将它前驱的 rchild 指针指向它,最后一个叶结点的 rchild 为空。算法实现如下:
LinkedList head, pre=NULL; //全局变量

&lt;!-- page: 177 --&gt;

第5章 树与二叉树
165

```cpp
LinkedList InOrder(BiTree bt){
if(bt){
InOrder(bt-&amp;gt;lchild); //中序遍历左子树
if(bt-&amp;gt;lchild==NULL&amp;amp;&amp;amp;bt-&amp;gt;rchild==NULL) //叶结点
if(pre==NULL){
head=bt;
pre=bt;
}
//处理第一个叶结点
else{
pre-&amp;gt;rchild=bt;
pre=bt;
}
//将叶结点链入链表
InOrder(bt-&amp;gt;rchild); //中序遍历右子树
pre-&amp;gt;rchild=NULL; //设置链表尾
}
return head;
}
上述算法的时间复杂度为 $O(n)$,辅助变量使用 head 和 pre,栈空间复杂度为 $O(n)$。 ## 14. 【解答】 本题采用递归的思想求解,若 $T_{1}$ 和 $T_{2}$ 都是空树,则相似;若有一个为空另一个不空,则必然不相似;否则递归地比较它们的左右子树是否相似。递归函数的定义如下: 1)f(T1,T2)=1:若 T1==T2==NULL。 2)f(T1,T2)=0:若 T1 和 T2 之一为 NULL,另一个不为 NULL。 3)f(T1,T2)=f(T1-&gt;lchild,T2-&gt;lchild)&amp;&amp;f(T1-&gt;rchild,T2-&gt;rchild):若 T1 和 T2 均不为 NULL。 因此,算法实现如下:
int similar(BiTree T1,BiTree T2){
//采用递归的算法判断两棵二叉树是否相似
int leftS,rightS;
//两棵树皆空
return 1;
else if(T1==NULL||T2==NULL) //只有一棵树为空
return 0;
else{
//递归判断
leftS=similar(T1-&amp;gt;lchild,T2-&amp;gt;lchild);
//递归判断
leftS=similar(T1-&amp;gt;rchild,T2-&amp;gt;rchild);
//return leftS&amp;amp;&amp;amp;rightS
}
}
## 15. 【解答】 二叉树的带权路径长度有两种常见的计算方法:① 根据二叉树的带权路径长度的定义,二叉树的 WPL 值 = 树中全部叶结点的带权路径长度之和。② 根据带权二叉树的性质,二叉树的 WPL 值 = 树中所有非叶结点的权值之和(记住该结论即可,不要求证明)。根据两种常见的计算方法,本题不难写出下列两种解法。 1)算法的基本设计思想。 ① 本问题可采用递归算法实现。根据定义: 二叉树的 WPL 值 = 树中全部叶结点的带权路径长度之和 = 根结点左子树中全部叶结点的带权路径长度之和 + 根结点右子树中全部叶结点的带权路径长度之和 <!-- page: 178 --> 2027年数据结构考研复习指导 叶结点的带权路径长度 = 该结点的 weight 域的值 × 该结点的深度 设根结点的深度为 0,若某结点的深度为 $d$ 时,则其孩子结点的深度为 $d + 1$。 在递归遍历二叉树结点的过程中,若遍历到叶结点,则返回该结点的带权路径长度,否则返回其左右子树的带权路径长度之和。 ② 若借用非叶结点的 weight 域保存其孩子结点中 weight 域值的和,则树的 WPL 等于树中所有非叶结点 weight 域值之和。 采用后序遍历策略,在遍历二叉树 $T$ 时递归计算每个非叶结点的 weight 域的值,则树 $T$ 的 WPL 等于根结点左子树的 WPL 加上右子树的 WPL,再加上根结点中 weight 域的值。在递归遍历二叉树结点的过程中,若遍历到叶结点,则 return 0 并且退出递归,否则递归计算其左右子树的 WPL 和自身结点的权值。 2)二叉树结点的数据类型定义如下。
typedef struct node{
int weight;
struct node *left, *right;
} BTree;
3)算法的代码如下。 ① 基于方法 1 的算法实现:
int WPL(BTree *root)  //根据WPL的定义采用递归算法实现
{ return WPL1(root, 0);
}
int WPL1(BTree *root, int d)  //d为结点深度
{ if(root-&amp;gt;left==NULL&amp;amp;&amp;amp;root-&amp;gt;right==NULL)
return(root-&amp;gt;weight*d);
else
return (WPL1(root-&amp;gt;left, d+1) + WPL1(root-&amp;gt;right, d+1));
}
② 基于方法 2 的算法实现: ```c int WPL(BTree *root) //基于递归的后序遍历算法实现 { int w_l, w_r; if (root-&gt;left==NULL&amp;&amp;root-&gt;right==NULL) return 0; else { w_l = WPL(root-&gt;left); if (root-&gt;left==NULL) return (w_l + w_r + root-&gt;weight); } } ```