408答疑
一、二叉树
二叉树的概念
二叉树的定义及其主要特性
二叉树的定义
二叉树的特点
- 每个结点至多只有两棵子树(即二叉树中不存在度大于2的结点)
- 二叉树的子树有左右之分,其次序不能任意颠倒
二叉树的形式定义
二叉树是 n n n ( n ≥ 0 n \geq 0 n≥0) 个结点的有限集合:
- 或者为空二叉树,即 n = 0 n=0 n=0。
- 或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一棵二叉树。
二叉树的有序性
- 二叉树是有序树,若将其左、右子树颠倒,则成为另一棵不同的二叉树。即使树中结点只有一棵子树,也要区分它是左子树还是右子树。
二叉树与度为2的有序树的区别
结点数量
- 度为2的树至少有3个结点,而二叉树可以为空。
子树次序
- 度为2的有序树的孩子的左右次序是相对于另一个孩子而言的,若某个结点只有一个孩子,则这个孩子就无须区分其左右次序,而二叉树无论其孩子数是否为2,均需确定其左右次序,即二叉树的结点次序不是相对于另一结点而言的,而是确定的。
几种特殊的二叉树
满二叉树
定义
- 一棵高度为 h h h,且有 2 h − 1 2^h - 1 2h−1 个结点的二叉树称为满二叉树。
- 满二叉树中的每层都含有最多的结点。
- 满二叉树的叶结点都集中在二叉树的最下一层,并且除叶结点之外的每个结点度数均为2。
编号规则
- 可以对满二叉树按层序编号:约定编号从根结点(根结点编号为1)起,自上而下,自左向右。
- 每个结点对应一个编号,对于编号为
i
i
i 的结点:
- 若有双亲,则其双亲为 ⌊ i / 2 ⌋ \lfloor i/2 \rfloor ⌊i/2⌋。
- 若有左孩子,则左孩子为 2 i 2i 2i。
- 若有右孩子,则右孩子为 2 i + 1 2i + 1 2i+1。
完全二叉树
定义
- 高度为 h h h、有 n n n 个结点的二叉树,当且仅当其每个结点都与高度为 h h h 的满二叉树中编号为 1 ∼ n 1 \sim n 1∼n 的结点一一对应时,称为完全二叉树。
二叉排序树
定义
- 左子树上所有结点的关键字均小于根结点的关键字。
- 右子树上所有结点的关键字均大于根结点的关键字。
- 左子树和右子树又各是一棵二叉排序树。
平衡二叉树
定义
- 树中任意一个结点的左子树和右子树的高度之差的绝对值不超过1。
正则二叉树
定义
- 树中每个分支结点都有2个孩子,即树中只有度为0或2的结点。
二叉树的性质
性质1:叶结点数与度为2的结点数的关系
- 非空二叉树上的叶结点数等于度为2的结点数加1,即 n 0 = n 2 + 1 n_0 = n_2 + 1 n0=n2+1。
- 证明:
- 设度为0, 1和2的结点个数分别为 n 0 , n 1 n_0, n_1 n0,n1 和 n 2 n_2 n2,结点总数 n = n 0 + n 1 + n 2 n = n_0 + n_1 + n_2 n=n0+n1+n2。
- 再看二叉树中的分支数,除根结点外,其余结点都有一个分支进入,设 B B B 为分支总数,则 n = B + 1 n = B + 1 n=B+1。
- 由于这些分支是由度为1或2的结点射出的,因此又有 B = n 1 + 2 n 2 B = n_1 + 2n_2 B=n1+2n2。
- 于是得 n 0 + n 1 + n 2 = n 1 + 2 n 2 + 1 n_0 + n_1 + n_2 = n_1 + 2n_2 + 1 n0+n1+n2=n1+2n2+1,则 n 0 = n 2 + 1 n_0 = n_2 + 1 n0=n2+1。
性质2:二叉树的结点总数
- 非空二叉树的第 k k k 层最多有 2 k − 1 2^{k-1} 2k−1 个结点( k ≥ 1 k \geq 1 k≥1)。
- 第1层最多有 2 1 − 1 = 1 2^{1-1} = 1 21−1=1 个结点(根),第2层最多有 2 2 − 1 = 2 2^{2-1} = 2 22−1=2 个结点,以此类推,可以证明其为一个公比为2的等比数列 2 k − 1 2^{k-1} 2k−1。
- 高度为 h h h 的二叉树至多有 2 h − 1 2^h - 1 2h−1 个结点( h ≥ 1 h \geq 1 h≥1)。
- 该性质利用性质2求前 h h h 项的和,即等比数列求和的结果。
性质3:完全二叉树的结点编号
- 对完全二叉树按从上到下、从左到右的顺序依次编号
1
,
2
,
⋯
,
n
1, 2, \cdots, n
1,2,⋯,n,则有以下关系:
- 最后一个分支结点的编号为 ⌊ n / 2 ⌋ \lfloor n/2 \rfloor ⌊n/2⌋,若 i ≤ ⌊ n / 2 ⌋ i \leq \lfloor n/2 \rfloor i≤⌊n/2⌋,则结点 i i i 为分支结点,否则为叶结点。
- 叶结点只可能在最后两层上出现(相当于在相同高度的满二叉树的最底层、最右边减少一些连续叶结点,当减少2个或以上叶结点时,次底层将出现叶结点)。
- 若有度为1的结点,则最多只可能有一个,且该结点只有左孩子而无右孩子(度为1的分支结点只可能是最后一个分支结点,其结点编号为 ⌊ n / 2 ⌋ \lfloor n/2 \rfloor ⌊n/2⌋)。
- 按层序编号后,一旦出现某结点(如编号 i i i)为叶结点或只有左孩子而无右孩子的情况,则编号大于 i i i 的结点均为叶结点(与结论1和结论2是相通的)。
- 若 n n n 为奇数,则每个分支结点都有左、右孩子;若 n n n 为偶数,则编号最大的分支结点(编号为 n / 2 n/2 n/2)只有左孩子,没有右孩子,其余分支结点都有左、右孩子。
- 当 i > 1 i > 1 i>1 时,结点 i i i 的双亲结点的编号为 ⌊ i / 2 ⌋ \lfloor i/2 \rfloor ⌊i/2⌋。
- 若结点 i i i 有左、右孩子,则左孩子编号为 2 i 2i 2i,右孩子编号为 2 i + 1 2i + 1 2i+1。
- 结点 i i i 所在层次(深度)为 ⌊ log 2 i ⌋ + 1 \lfloor \log_2 i \rfloor + 1 ⌊log2i⌋+1。
性质4:完全二叉树的高度
- 具有 n n n 个( n > 0 n > 0 n>0)结点的完全二叉树的高度为 ⌈ log 2 ( n + 1 ) ⌉ \lceil \log_2 (n + 1) \rceil ⌈log2(n+1)⌉ 或 ⌊ log 2 n ⌋ + 1 \lfloor \log_2 n \rfloor + 1 ⌊log2n⌋+1。
- 设高度为 h h h,根据性质3和完全二叉树的定义有 2 h − 1 − 1 < n ≤ 2 h − 1 2^{h-1} - 1 < n \leq 2^h - 1 2h−1−1<n≤2h−1 或者 2 h − 1 ≤ n < 2 h 2^{h-1} \leq n < 2^h 2h−1≤n<2h,得 2 h − 1 < n + 1 ≤ 2 h 2^{h-1} < n + 1 \leq 2^h 2h−1<n+1≤2h,即 h − 1 < log 2 ( n + 1 ) ≤ h h - 1 < \log_2 (n + 1) \leq h h−1<log2(n+1)≤h,因为 h h h 为正整数,所以 h = ⌈ log 2 ( n + 1 ) ⌉ h = \lceil \log_2 (n + 1) \rceil h=⌈log2(n+1)⌉,或者得 h − 1 ≤ log 2 n < h h - 1 \leq \log_2 n < h h−1≤log2n<h,所以 h = ⌊ log 2 n ⌋ + 1 h = \lfloor \log_2 n \rfloor + 1 h=⌊log2n⌋+1。
二叉树的存储结构
顺序存储结构
定义
- 二叉树的顺序存储是指用一组连续的存储单元依次自上而下、自左至右存储完全二叉树上的结点元素,即将完全二叉树上编号为 i i i 的结点元素存储在一维数组下标为 i − 1 i-1 i−1 的分量中。
适用性
- 依据二叉树的性质,完全二叉树和满二叉树采用顺序存储比较合适,树中结点的序号可以唯一地反映结点之间的逻辑关系,这样既能最大可能地节省存储空间,又能利用数组元素的下标值确定结点在二叉树中的位置,以及结点之间的关系。
一般二叉树的顺序存储
- 对于一般的二叉树,为了让数组下标能反映二叉树中结点之间的逻辑关系,只能添加一些并不存在的空结点,让其每个结点与完全二叉树上的结点相对照,再存储到一维数组的相应分量中。
- 在最坏情况下,一个高度为 h h h 且只有 h h h 个结点的单支树却需要占据近 2 h − 1 2^h - 1 2h−1 个存储单元。
注意事项
- 建议从数组下标 1 开始存储树中的结点,保证数组下标和结点编号一致。
链式存储结构
概述
顺序存储的空间利用率较低,因此二叉树一般都采用链式存储结构,用链表结点来存储二叉树中的每个结点。在二叉树中,结点结构通常包括若干数据域和若干指针域,二叉链表至少包含3个域:数据域 d a t a data data、左指针域 l c h i l d lchild lchild 和右指针域 r c h i l d rchild rchild。
图示说明
下图所示为一棵二叉树及其对应的二叉链表。而实际上在不同的应用中,还可以增加某些指针域,如增加指向父结点的指针后,变为三叉链表的存储结构。
二叉树的链式存储结构描述
使用不同的存储结构时,实现二叉树操作的算法也会不同,因此要根据实际应用场合(二叉树的形态和需要进行的运算)来选择合适的存储结构。
容易验证,在含有 n n n 个结点的二叉链表中,含有 n + 1 n + 1 n+1 个空链域(重要结论,经常出现在选择题中)。
二叉树的遍历
定义
二叉树的遍历是指按某条搜索路径访问树中每个结点,使得每个结点均被访问一次,而且仅被访问一次。二叉树是一种非线性结构,每个结点都可能有两棵子树,因此需要寻找一种规律,以便使二叉树上的结点能排列在一个线性队列上,进而便于遍历。
遍历方式
先序遍历(NLR)
- 若二叉树为空,则什么也不做;否则,
- 访问根结点;
- 先序遍历左子树;
- 先序遍历右子树。
- 下图的虚线表示对该二叉树进行先序遍历的路径,得到先序遍历序列为 124635。
中序遍历(LNR)
- 若二叉树为空,则什么也不做;否则,
- 中序遍历左子树;
- 访问根结点;
- 中序遍历右子树。
- 下图中的虚线表示对该二叉树进行中序遍历的路径,得到中序遍历序列为 264135。
后序遍历(LRN)
- 若二叉树为空,则什么也不做;否则,
- 后序遍历左子树;
- 后序遍历右子树;
- 访问根结点。
- 下图中的虚线表示对该二叉树进行后序遍历的路径,得到后序遍历序列为 642531。
层次遍历
定义
下图所示为二叉树的层次遍历,即按照箭头所指方向,按照 1, 2, 3, 4, 5 的层次顺序,自上而下、从左至右对二叉树中的各个结点进行逐层访问。
遍历过程
进行层次遍历时,需要借助一个队列。层次遍历的思想如下:
- 首先将根结点入队。
- 若队列非空,则队头结点出队,访问该结点,若它有左孩子,则将其左孩子入队;若它有右孩子,则将其右孩子入队。
- 重复步骤2,直至队列为空。
应用
遍历是二叉树各种操作的基础,例如对于一棵给定二叉树求结点的双亲、求结点的孩子、求二叉树的深度、求叶结点个数、判断两棵二叉树是否相同等。所有这些操作都是在遍历的过程中进行的,因此必须掌握二叉树的各种遍历过程,并能灵活运用以解决各种问题。
由遍历序列构造二叉树
分析
对于一棵给定的二叉树,其先序序列、中序序列、后序序列和层序序列都是确定的。然而,只给出四种遍历序列中的任意一种,却不能唯一地确定一棵二叉树。若已知中序序列,再给出其他三种遍历序列中的任意一种,就可以唯一地确定一棵二叉树。
由先序序列和中序序列构造二叉树
方法
在先序序列中,第一个结点一定是二叉树的根结点;而在中序遍历中,根结点必然将中序序列分割成两个子序列,前一个子序列是根的左子树的中序序列,后一个子序列是根的右子树的中序序列。左子树的中序序列和先序序列的长度是相等的,右子树的中序序列和先序序列的长度是相等的。根据这两个子序列,可以在先序序列中找到左子树的先序序列和右子树的先序序列,如此递归地分解下去,便能唯一地确定这棵二叉树。
示例
例如,求先序序列( A B C D E F G H I ABCDEFGHI ABCDEFGHI)和中序序列( B C A E D G H F I BCAEDGHFI BCAEDGHFI)所确定的二叉树。首先,由先序序列可知 A A A 为二叉树的根结点。中序序列中 A A A 之前的 B C BC BC 为左子树的中序序列, E D G H F I EDGHFI EDGHFI 为右子树的中序序列。然后,由先序序列可知 B B B 是左子树的根结点, D D D 是右子树的根结点。以此类推,就能将剩下的结点继续分解下去,最后得到的二叉树如下图所示。
由后序序列和中序序列构造二叉树
方法
由于二叉树的后序序列和中序序列也可以唯一地确定一棵二叉树,因为后序序列的最后一个结点就如同先序序列的第一个结点,可以将中序序列分割成两个子序列,然后采用类似的方法递归地进行分解,进而唯一地确定这棵二叉树。
示例
同先序 + 中序类似,可自行分析后序序列( C B E H G I F D A CBEHGIFDA CBEHGIFDA)和中序序列( B C A E D G H F I BCAEDGHFI BCAEDGHFI)所确定的二叉树。
由层次序列和中序序列构造二叉树
方法
在层次遍历中,第一个结点一定是二叉树的根结点,这样就将中序序列分割成了左子树的中序序列和右子树的中序序列。若存在左子树,则层次序列的第二个结点一定是左子树的根,可进一步划分左子树;若存在右子树,则层次序列中紧接着的下一个结点一定是右子树的根,可进一步划分右子树。
示例
可自行分析层次序列( A B D C E F G I H ABDCEFGIH ABDCEFGIH)和中序序列( B C A E D G H F I BCAEDGHFI BCAEDGHFI)所确定的二叉树。
注意事项
先序序列、后序序列和层次序列的两两组合,无法唯一确定一棵二叉树。例如,下图所示的两棵二叉树的先序序列都为 A B AB AB,后序序列都为 B A BA BA,层次序列都为 A B AB AB。
代码实操
结点定义
typedef struct BTNode
{
ElemType data; // 数据域
struct BTNode *left; // 左子树
struct BTNode *right; // 右子树
} BTNode, *BTRoot;
创建二叉树
- 通过递归的方式,根据输入的字符串序列(如ABC##DE##F##G#H##)构建二叉树。'#'表示空结点。
BTNode* creatBinTree(char *str)
{
static int i = 0; // 静态变量用于记录当前字符位置
if (str[i] == '\0' || str[i] == '#') // 如果是空字符或'#',表示空结点
return NULL;
// 创建根结点
BTNode *t = (BTNode*)malloc(sizeof(BTNode));
t->data = str[i]; // 设置结点数据
i++; // 移动到下一个字符
t->left = creatBinTree(str); // 递归创建左子树
i++;
t->right = creatBinTree(str); // 递归创建右子树
return t;
}
前序遍历
- 前序遍历的顺序是“根-左-右”。递归方式直接按照顺序访问;非递归方式通过栈模拟递归过程,确保左子树先于右子树访问。
// 递归
void preOrder(BTNode *t)
{
if (t != NULL)
{
printf("%c ", t->data); // 访问根结点
preOrder(t->left); // 遍历左子树
preOrder(t->right); // 遍历右子树
}
}
// 非递归
void preOrder_noR(BTNode *t)
{
if (t != NULL)
{
BTNode* st[MAX_STACK_SIZE] = { 0 }; // 定义一个栈
int top = 0;
st[top++] = t; // 根结点入栈
while (top != 0)
{
BTNode *p = st[--top]; // 出栈并访问结点
printf("%c ", p->data);
if (p->right != NULL) // 右子树先入栈
st[top++] = p->right;
if (p->left != NULL) // 左子树后入栈
st[top++] = p->left;
}
}
}
中序遍历
- 中序遍历的顺序是“左-根-右”。递归方式直接按照顺序访问;非递归方式通过栈模拟递归过程,确保左子树先访问。
// 递归
void inOrder(BTNode *t)
{
if (t != NULL)
{
inOrder(t->left); // 遍历左子树
printf("%c ", t->data); // 访问根结点
inOrder(t->right); // 遍历右子树
}
}
// 非递归
void inOrder_noR(BTNode *t)
{
if (t != NULL)
{
BTNode* st[MAX_STACK_SIZE] = { 0 }; // 定义一个栈
int top = 0;
do
{
while (t != NULL) // 将左子树全部入栈
{
st[top++] = t;
t = t->left;
}
BTNode *p = st[--top]; // 出栈并访问结点
printf("%c ", p->data);
if (p->right != NULL) // 转向右子树
t = p->right;
} while (top != 0 || t != NULL);
}
}
后序遍历
- 后序遍历的顺序是“左-右-根”。递归方式直接按照顺序访问;非递归方式通过栈模拟递归过程,确保右子树先于根结点访问。
// 递归
void postOrder(BTNode *t)
{
if (t != NULL)
{
postOrder(t->left); // 遍历左子树
postOrder(t->right); // 遍历右子树
printf("%c ", t->data); // 访问根结点
}
}
// 非递归
void postOrder_noR(BTNode *t)
{
if (t != NULL)
{
BTNode* st[MAX_STACK_SIZE] = { 0 }; // 定义一个栈
int top = 0;
BTNode *prev = NULL;
do
{
while (t != NULL) // 将左子树全部入栈
{
st[top++] = t;
t = t->left;
}
BTNode *p = st[top - 1];
if (p->right == NULL || p->right == prev) // 如果右子树为空或已访问
{
printf("%c ", p->data); // 访问结点
top--;
prev = p; // 更新前一个访问的结点
}
else
t = p->right; // 转向右子树
} while (top != 0);
}
}
层次遍历
- 层次遍历使用队列实现,按照从上到下、从左到右的顺序访问结点。
void levelOrder(BTNode *t)
{
if (t != NULL)
{
BTNode* Q[MAX_QUEUE_SIZE] = { 0 }; // 定义一个队列
int front = 0, rear = 0;
Q[rear++] = t; // 根结点入队
while (front != rear) // 队列不空
{
BTNode *p = Q[front++]; // 出队并访问结点
printf("%c ", p->data);
if (p->left != NULL) // 左子树入队
Q[rear++] = p->left;
if (p->right != NULL) // 右子树入队
Q[rear++] = p->right;
}
}
}
二叉树结点个数
- 递归计算左子树和右子树的大小,加上根结点。
int size(BTNode *t)
{
if (t == NULL)
return 0;
return size(t->left) + size(t->right) + 1;
}
二叉树叶子结点个数
- 递归检查每个结点是否为叶子结点。
int leafSize(BTNode *t)
{
if (t == NULL)
return 0;
if (t->left == NULL && t->right == NULL) // 叶子结点
return 1;
return leafSize(t->left) + leafSize(t->right);
}
二叉树第 K 层结点个数
- 递归向下查找,直到到达第 k 层。
int levelKSize(BTNode *t, int k)
{
if (t == NULL)
return 0;
if (k == 1) // 到达目标层
return 1;
return levelKSize(t->left, k - 1) + levelKSize(t->right, k - 1);
}
二叉树高度
- 递归计算左子树和右子树的高度,取较大值加1。
int height(BTNode *t)
{
if (t == NULL)
return 0;
int left_h = height(t->left); // 左子树高度
int right_h = height(t->right); // 右子树高度
return (left_h > right_h ? left_h : right_h) + 1;
}
查找关键值为 key 的结点
- 递归查找,先在左子树查找,如果找不到再在右子树查找。
BTNode* find(BTNode *t, ElemType key)
{
if (t == NULL || t->data == key) // 找到或为空
return t;
BTNode *p = find(t->left, key); // 在左子树查找
if (p != NULL)
return p;
return find(t->right, key); // 在右子树查找
}
查找结点 p 的父结点
- 递归查找,先在左子树查找,如果找不到再在右子树查找。
BTNode* parent(BTNode *t, BTNode *p)
{
if (t == NULL || p == t) // 空树或根结点
return NULL;
if (t->left == p || t->right == p) // 找到父结点
return t;
BTNode *q = parent(t->left, p); // 在左子树查找
if (q != NULL)
return q;
return parent(t->right, p); // 在右子树查找
}
判断两棵二叉树是否相等
- 递归比较两棵树的根结点、左子树和右子树是否相等。
bool equal(BTNode *t1, BTNode *t2)
{
if (t1 == NULL && t2 == NULL) // 都为空
return true;
if (t1 == NULL || t2 == NULL) // 一个为空,一个非空
return false;
return (t1->data == t2->data && equal(t1->left, t2->left) && equal(t1->right, t2->right));
}
释放二叉树
- 递归删除左子树和右子树,最后删除根结点。
void deleteBinTree(BTNode *t)
{
if (t != NULL)
{
deleteBinTree(t->left); // 删除左子树
deleteBinTree(t->right); // 删除右子树
free(t); // 删除根结点
}
}
判断是否是完全二叉树
- 通过层次遍历,检查空结点后是否还有非空结点。
bool isCompleteBinTree(BTNode *t)
{
if (t == NULL) // 空树是完全二叉树
return true;
BTNode *queue[MAX_QUEUE_SIZE] = { 0 }; // 定义队列
int front = 0, rear = front;
queue[rear++] = t; // 根结点入队
while (front != rear) // 队列不空
{
BTNode *p = queue[front++]; // 出队
if (p != NULL) // 非空结点,子树入队
{
queue[rear++] = p->left;
queue[rear++] = p->right;
}
else // 空结点后不应再有非空结点
{
while (front != rear)
{
if (queue[front++] != NULL)
return false;
}
}
}
return true;
}
删除树中每个值为 x 的结点
- 通过层次遍历找到值为 x 的结点并删除。
// 删除结点函数
void delBTNode(BTNode *t)
{
if (t == NULL)
return;
delBTNode(t->left);
delBTNode(t->right);
free(t);
}
// 删除值为x的结点
void delX(BTNode *t, char x)
{
if (t == NULL)
return;
if (t->data == x) // 根结点为需要删除的结点
{
delBTNode(t);
return;
}
BTNode *queue[MAX_QUEUE_SIZE] = { 0 }; // 定义队列
int front = 0, rear = front;
queue[rear++] = t; // 根结点入队
while (front != rear) // 队列不空
{
BTNode *p = queue[front++]; // 出队
if (p->left != NULL)
{
if (p->left->data == x) // 左子树为需要删除的结点
{
delBTNode(p->left);
p->left = NULL;
}
else
queue[rear++] = p->left; // 左子树入队
}
if (p->right != NULL)
{
if (p->right->data == x) // 右子树为需要删除的结点
{
delBTNode(p->right);
p->right = NULL;
}
else
queue[rear++] = p->right; // 右子树入队
}
}
}
线索二叉树
线索二叉树的基本概念
概述
线索二叉树是一种优化存储结构的方法,通过合理利用空指针,避免资源浪费。在这种结构中,当一个结点没有左右子树时,其左右指针不再单纯指向子结点,而是保存其他有用信息:
- 左指针保存前驱结点的信息
- 右指针保存后继结点的信息
这些指针相当于前驱和后继的线索信息,因此称为线索化的二叉树。
作用
通过利用空指针来存放前驱和后继指针,线索二叉树能够像遍历链表那样方便地遍历二叉树。这种结构加速了二叉树中前驱结点和后继结点的访问速度。
线索二叉树的定义和结构
定义
线索二叉树是在传统二叉树的基础上引入线索的概念。传统二叉树的存储结构仅能体现父子关系,而线索二叉树通过增加线索指针,使得每个结点(除了第一个和最后一个)都有一个直接前驱和直接后继。
结构
线索二叉树的结点结构包含以下部分:
- 数据元素
data
- 左孩子指针
lchild
- 右孩子指针
rchild
- 左线索标志
ltag
- 右线索标志
rtag
其中,线索标志的含义如下:
ltag = 0
:lchild
域指示结点的左孩子ltag = 1
:lchild
域指示结点的前驱rtag = 0
:rchild
域指示结点的右孩子rtag = 1
:rchild
域指示结点的后继
空指针的数量
在有 n n n 个结点的二叉树中,空指针的个数为 n + 1 n+1 n+1 个。
推导
- 叶子结点有两个空指针,度为1的结点有一个空指针。
- 假设叶子结点数为 n 0 n_0 n0,度为1的结点数为 n 1 n_1 n1,则空指针数为 2 n 0 + n 1 2n_0 + n_1 2n0+n1。
- 又因为 n 0 = n 2 + 1 n_0 = n_2 + 1 n0=n2+1,所以 2 n 0 + n 1 = n 0 + n 1 + n 2 + 1 = n + 1 2n_0 + n_1 = n_0 + n_1 + n_2 + 1 = n + 1 2n0+n1=n0+n1+n2+1=n+1。
中序线索二叉树的构造和遍历
线索的指向
线索二叉树的构造过程中,线索指向遵循以下规则:
- 假设指针
pre
指向刚刚访问过的结点,指针p
指向正在访问的结点,即pre
指向p
的前驱。 - 在中序遍历的过程中,检查
p
的左指针是否为空,若为空则将其指向pre
。 - 检查
pre
的右指针是否为空,若为空则将其指向p
。
构造示例
以下图为例,展示了中序线索二叉树的构造过程:
- 通过检查每个结点的左右指针,将空指针转换为指向前驱或后继的线索。
- 这种转换使得在遍历过程中能够直接获取前驱和后继的信息。
下图展示了带头结点的中序线索二叉树:
- 在线索链表上添加一个头结点,使其
lchild
域的指针指向二叉树的根结点,rchild
域的指针指向中序遍历时访问的最后一个结点。 - 这样建立了一个双向线索链表,方便从前往后或从后往前对线索二叉树进行遍历。
遍历方法
中序线索二叉树的遍历利用线索信息,简化了遍历过程:
- 从序列中的第一个结点开始,依次找到每个结点的后继,直至后继为空。
- 在中序线索二叉树中,若结点的右标志为“1”,则右链为线索,指示其后继;否则遍历右子树中第一个访问的结点(右子树中最左下的结点)为其后继。
先序线索二叉树和后序线索二叉树
先序线索二叉树的构造
先序线索二叉树的构造过程如下:
- 先序序列为
A
B
C
D
F
ABCDF
ABCDF,依次判断每个结点的左右链域,若为空,则将其改造为线索。
- 结点 A , B A, B A,B 均有左右孩子。
- 结点 C C C 无左孩子,将左链域指向前驱 B B B,无右孩子,将右链域指向后继 D D D。
- 结点 D D D 无左孩子,将左链域指向前驱 C C C,无右孩子,将右链域指向后继 F F F。
- 结点 F F F 无左孩子,将左链域指向前驱 D D D,无右孩子,也无后继,所以置空。
- 得到的先序线索二叉树如下图所示。
先序线索二叉树结点后继的查找
- 若有左孩子,则左孩子就是其后继;
- 若无左孩子但有右孩子,则右孩子就是其后继;
- 若为叶结点,则右链域直接指示了结点的后继。
后序线索二叉树的构造
后序线索二叉树的构造过程如下:
- 后序序列为
C
D
B
F
A
CDBFA
CDBFA,依次判断每个结点的左右链域,若为空,则将其改造为线索。
- 结点 C C C 无左孩子,也无前驱,所以置空,无右孩子,将右链域指向后继 D D D。
- 结点 D D D 无左孩子,将左链域指向前驱 C C C,无右孩子,将右链域指向后继 B B B。
- 结点 F F F 无左孩子,将左链域指向前驱 B B B,无右孩子,将右链域指向后继 A A A。
- 得到的后序线索二叉树如下图所示。
后序线索二叉树结点后继的查找
在后序线索二叉树中找结点的后继较为复杂,可分为三种情况:
- 若结点 x x x 是二叉树的根,则其后继为空。
- 若结点 x x x 是其双亲的右孩子,或是其双亲的左孩子且其双亲没有右子树,则其后继即双亲。
- 若结点 x x x 是其双亲的左孩子,且其双亲有右子树,则其后继为双亲的右子树上按后序遍历列出的第一个结点。
在后序线索二叉树上找后继时需知道结点双亲,即需采用带标志域的三叉链表作为存储结构。
四、参考资料
鲍鱼科技课件
b站免费王道课后题讲解:
网课全程班: