对应章节:数据结构 → 树与二叉树 → 线索二叉树
这两题真正考的是三个点:
二叉树为什么有
n+1个空指针;线索化时,空指针分别如何改造成"前驱线索"和"后继线索";
"有线索"不等于"每个结点都能通过一根线索直接找到前驱和后继"。
一、做题前先建立统一模型
设结点结构为:
[lchild] [data] [rchild]
在线索二叉树中,通常再增加两个标志位:
ltag = 0:lchild 指向真正的左孩子
ltag = 1:lchild 是前驱线索
rtag = 0:rchild 指向真正的右孩子
rtag = 1:rchild 是后继线索
也就是说:
原来为空的左指针 → 可以改成"前驱线索"
原来为空的右指针 → 可以改成"后继线索"
注意:只有原来为空的指针域才能拿来做线索。
如果某个结点本来就有左孩子或右孩子,那么对应的指针域仍然必须指向孩子,不能同时拿来存线索。
二、为什么 n 个结点的二叉树有 n+1 个空指针?
每个结点有两个孩子指针,因此共有:
2n 个指针域
而一棵有 n 个结点的二叉树共有:
n - 1 条边
每条边对应一个非空孩子指针,因此非空指针有 n-1 个。
所以空指针数为:
2n - (n - 1)
= n + 1
因此:
n 个结点的二叉树,一共有 n+1 个空链域。
线索二叉树正是利用这些空链域来保存遍历序列中的前驱、后继信息。
第 25 题
题目
一棵左子树为空 的二叉树在先序线索化后,其中空的链域的个数是( )。
选项:
A. 不确定
B. 0 个
C. 1 个
D. 2 个
答案
D:2 个。
1. 先抓住"先序遍历"的第一个结点
先序遍历顺序:
根 → 左 → 右
所以整棵树先序遍历的第一个结点一定是根结点。
题目又告诉我们:
根的左子树为空
因此根结点的左指针本来就是空的。
画出来:
A
/ \
NULL 右子树
由于 A 是先序遍历的第一个结点,所以:
A 没有前驱
而 A 的左指针又恰好是空指针,本来应该用来存"前驱线索",但它没有前驱,因此:
A.lchild = NULL
所以这里已经确定有 1 个空链域。
2. 再看先序遍历的最后一个结点
设先序遍历序列为:
A → ... → X
其中 X 是最后访问的结点。
X 不可能还有孩子。
因为如果 X 还有左孩子或右孩子,那么按照先序遍历,访问完 X 之后还必须继续访问它的孩子,X 就不可能是最后一个结点。
所以:
最后一个结点 X 一定是叶子结点
也就是:
X
/ \
NULL NULL
X 是整个先序序列的最后一个结点,因此:
X 没有后继
X 的右指针本来为空,本应改造成"后继线索",但它没有后继,所以:
X.rchild = NULL
这又产生 1 个空链域。
3. 所以一共是两个
第一个结点(根):
左指针 → 没有前驱 → NULL
最后一个结点:
右指针 → 没有后继 → NULL
因此:
空链域总数 = 1 + 1 = 2
答案:
D. 2 个
4. 用具体例子看最直观
假设原树是:
A
/ \
NULL B
/ \
C D
先序遍历:
A → B → C → D
线索化之后:
A:
左边原本为空
A 又没有前驱
所以 A.left = NULL
C:
C 没有左右孩子
先序前驱是 B,后继是 D
C.left → B
C.right → D
D:
D 是先序最后一个结点
D 没有后继
所以 D.right = NULL
最终仍然为空的是:
A.left = NULL
D.right = NULL
正好 2 个。
5. 一个特殊情况也不影响答案
如果整棵树只有一个根结点:
A
/ \
NULL NULL
先序序列只有:
A
A 同时:
没有前驱
没有后继
于是:
A.left = NULL
A.right = NULL
仍然是 2 个空链域。
第 26 题
题目
在线索二叉树中,下列说法不正确的是( )。
选项:
A. 在中序线索树中,若某结点有右孩子,
则其后继结点是它的右子树的最左下结点。
B. 在中序线索树中,若某结点有左孩子,
则其前驱结点是它的左子树的最右下结点。
C. 线索二叉树是利用二叉树的 n+1 个空指针
来存放结点的前驱和后继信息的。
D. 每个结点通过线索都可以直接找到它的前驱和后继。
答案
D 错误。
A 为什么正确?
中序遍历顺序:
左子树 → 根 → 右子树
假设当前结点为 A,而且 A 有右孩子:
A
\
B
/
C
/
D
访问完 A 之后,下一步进入 A 的右子树。
但右子树中第一个被中序访问的结点不是一定是 B,而是:
右子树中最左边的结点
在上图中:
A 的中序后继 = D
所以:
若某结点有右孩子,它的中序后继是右子树中最左下的结点。
因此 A 正确。
B 为什么正确?
同理,中序遍历中:
左子树 → 根 → 右子树
如果 A 有左孩子,那么在访问 A 之前,必须先访问完整个左子树。
左子树中最后被中序访问的结点就是:
左子树中最右边的结点
例如:
A
/
B
\
C
\
D
则:
A 的中序前驱 = D
所以:
若某结点有左孩子,它的中序前驱是左子树中最右下的结点。
因此 B 正确。
C 为什么正确?
前面已经证明:
n 个结点
→ 一共有 2n 个孩子指针域
实际边数
→ n - 1
空指针数
→ 2n - (n - 1)
→ n + 1
这些空指针正好可以利用起来,存放遍历序列中的前驱和后继信息。
因此 C 正确。
D 为什么错误?
D 的说法是:
每个结点通过线索都可以直接找到前驱和后继
问题出在两个字:
"每个"
"直接"
线索只存在于原本为空的孩子指针域中。
如果某个结点本来有左孩子:
ltag = 0
那么:
lchild 指向左孩子
它就不是前驱线索。
此时想找这个结点的中序前驱,不能直接顺着 lchild 就得到答案,而是要:
先进入左子树
再不断向右
找到左子树中最右边的结点
同理,如果某结点本来有右孩子:
rtag = 0
那么 rchild 是真正的右孩子,不是后继线索。
要寻找中序后继,需要:
先进入右子树
再不断向左
找到右子树中最左边的结点
所以并不是每个结点都可以靠"一根线索"直接找到前驱和后继。
第 26 题最关键的图
对于中序线索树,找前驱和后继可以这样记:
当前结点 P
/ \
/ \
有左孩子 有右孩子
| |
v v
左子树一直向右 右子树一直向左
| |
v v
中序前驱 中序后继
如果本来没有左孩子:
ltag = 1
lchild 直接就是前驱线索
如果本来没有右孩子:
rtag = 1
rchild 直接就是后继线索
所以完整模型是:
找中序前驱:
① ltag = 1
→ lchild 就是前驱,直接得到
② ltag = 0
→ 有左子树
→ 进入左子树
→ 一直向右
→ 最右结点是前驱
找中序后继:
① rtag = 1
→ rchild 就是后继,直接得到
② rtag = 0
→ 有右子树
→ 进入右子树
→ 一直向左
→ 最左结点是后继
这正是 D 错误的原因。
两题放在一起总结
必须记住的 4 句话
1. n 个结点的二叉树有 n+1 个空指针。
2. 空左指针可改造成"前驱线索"。
3. 空右指针可改造成"后继线索"。
4. 有孩子时,对应指针仍然是孩子指针,
因此不一定能通过线索直接得到前驱/后继。
中序前驱 / 后继口诀
有左孩子:
前驱 = 左子树最右
有右孩子:
后继 = 右子树最左
无左孩子且已线索化:
左线索 = 前驱
无右孩子且已线索化:
右线索 = 后继
第 25 题识别信号
题目出现:
先序线索化
+
根的左子树为空
立即想到:
根是先序第一个结点
→ 没有前驱
→ 左指针又正好为空
→ 留下一个 NULL
最后一个先序结点
→ 没有后继
→ 右指针留下一个 NULL
总共 2 个
复盘点
复盘点 1:线索只能占用"空指针"
不要把线索理解成给每个结点额外增加两个前驱/后继指针。
线索化本质是:
废物利用:把原本为空的孩子指针拿来存前驱/后继。
复盘点 2:"有线索二叉树"不代表前驱后继都能 O(1) 直接顺指针得到
如果对应方向本来有孩子,那么那个指针就是孩子指针,不是线索。
因此中序线索树中:
有左孩子 → 找前驱要去左子树最右边
有右孩子 → 找后继要去右子树最左边
复盘点 3:第一个结点没有前驱,最后一个结点没有后继
任何遍历序列都可以先从这个角度判断:
第一个结点:
前驱不存在
最后一个结点:
后继不存在
但"前驱/后继不存在"是否会形成一个真正的空链域,还要看这个结点对应的孩子指针本来是不是空的。
这就是第 25 题专门强调"左子树为空"的原因。