一、为什么要用线索二叉树
普通二叉链表:
-
n 个结点,一共2n 个指针域
-
真正指向孩子的指针只有 n-1 个
-
剩余 n+1 个空指针,空间浪费
解决办法:
利用空左、空右指针,存放中序遍历的前驱、后继结点
加上标记位区分,就是中序线索二叉树
二、线索结点结构
数据 data
左指针 lchild + 左标记 ltag
右指针 rchild + 右标记 rtag
-
`ltag = 0`:lchild 指向左孩子
-
`ltag = 1`:lchild 是前驱线索
-
`rtag = 0`:rchild 指向右孩子
-
`rtag = 1`:rchild 是后继线索
三、构造核心规则(中序:左→根→右)
-
按照中序遍历顺序递归遍历整棵树
-
定义全局指针 `pre`,永远记录上一个访问过的结点
-
当前结点左孩子为空
→ 左指针指向 `pre`,`ltag=1`
- 前驱结点 `pre` 右孩子为空
→ pre 右指针指向当前结点,`pre.rtag=1`
- 遍历完当前结点,更新 `pre = 当前结点`
四、一步步构造流程(画图逻辑)
-
初始化 `pre = null`
-
递归线索化左子树
-
处理当前根结点:绑定前驱线索
-
绑定前驱结点到当前结点的后继线索
-
更新前驱 pre
-
递归线索化右子树
五、Java 标准构造代码
1.线索结点类
class InThreadNode {
int data;
InThreadNode left;
InThreadNode right;
int ltag; //0=左孩子 1=前驱线索
int rtag; //0=右孩子 1=后继线索
public InThreadNode(int data) {
this.data = data;
left = null;
right = null;
ltag = 0;
rtag = 0;
}
}
2.中序线索化核心递归代码
public class ThreadTree {
static InThreadNode pre = null; //记录前驱结点
//中序遍历实现二叉树线索化
public void createInThread(InThreadNode root) {
if (root == null) return;
//1.递归线索化左子树
createInThread(root.left);
//2.处理当前结点:左空 → 指向前驱
if (root.left == null) {
root.left = pre;
root.ltag = 1;
}
//3.处理前驱结点:右空 → 指向当前结点(后继)
if (pre != null && pre.right == null) {
pre.right = root;
pre.rtag = 1;
}
//前驱后移
pre = root;
//4.递归线索化右子树
createInThread(root.right);
}
}
六、中序线索二叉树遍历(不用栈、不用递归)
//线索树遍历
public void threadTraverse(InThreadNode root){
InThreadNode p = root;
//找到中序第一个结点(最左下)
while(p != null && p.ltag == 0){
p = p.left;
}
while(p != null){
System.out.print(p.data + " ");
//右是线索,直接走后继
if(p.rtag == 1){
p = p.right;
}
//右是孩子,找右子树最左下结点
else{
p = p.right;
while(p != null && p.ltag == 0){
p = p.left;
}
}
}
}
七、考点
-
线索化本质 = 一次完整中序遍历
-
`pre` 是连接前驱后继的关键
-
叶子结点:左右指针全部是线索
-
线索二叉树:无需递归、无需栈即可遍历
-
n 个结点二叉链表,固定空指针数量:n+1 个
-
中序线索树找前驱后继最简单,先序、后序很复杂
八、优缺点
优点
-
充分利用空闲指针,不额外占用空间
-
非递归快速遍历
-
快速查询结点遍历前驱、后继
缺点
-
插入、删除结点麻烦,需要重新修改所有线索
-
树结构改动代价大