1,线索化二叉树基本介绍
- 线索化二叉树是对普通二叉树的扩展,对于普通二叉树而言,一个节点会包含一个数据域以及指向左右子节点的位置索引;但是对于叶子节点来讲,左右子节点的位置索引并没有被利用到,线索二叉树充分利用该部分节点,普通二叉树如下:
- 在n个节点的二叉树中总共含有
n - 1
(类似5指向2表示一个位置指向)个位置指向,即2n
(每一个节点有左右两个指针域)个指针域,这样对于一个二叉树来讲,共有2n - (n - 1) = n + 1
个空指针域。利用二叉树的空指针域,根据前/中/后序不同遍历方式存储前驱节点(左子节点)和后继节点(右子节点),这种附加的指针称为线索,二叉树转线索化二叉树,后续会分别具体分析: - 根据线索性质的不同,线索二叉树可以分为前序线索二叉树,中序线索二叉树,后续线索二叉树;分别对应前序遍历方式,中序遍历方式,后续遍历方式,部分方式的线索二叉树必须对应不同的遍历方式,不然会遍历失败,甚至栈溢出
- 二叉树转换为线索化二叉树后,
leftNode
指向的可能是左子树,也可能是前驱节点;同样,rightNode
指向的可能是右子树,也可能是后继节点;具体区分,会在Node
节点对象中分别维护leftFlag
和rightFlag
属性,对应不同的标志位标识是线索节点还是真实节点
2,二叉树 — 线索化二叉树转换规则
- 线索化二叉树是在原二叉树的基础上,将每一个节点抽象为存在上下指针的节点
- 在将二叉树转换为线索化二叉树时,
- 如果节点的左右节点存在,则该节点不变,
- 如果节点的左侧节点为空, 则将左侧节点填充为前驱节点;中序方式的遍历方式为:左中右,可以通过不同位置对应前驱节点
- 如果节点的右侧节点为空,则将右侧节点填充为后继节点,选择同上
- 另外,在Node节点中除过
leftNode
,rightNode
左右节点数据外,另外维护leftFlag
,rightFlag
两个标志位,说明当前左/右侧数据指示的是树数据,还是后继节点数据 - 对于前序/中序/后序方式生成的线索化二叉树,必须对应的使用前序/中序/后序方式进行遍历
- 遍历结果的第一个元素和最后一个元素,分别没有前驱节点和后继节点,所以在线索化二叉树中对应的指向Node为空,但Flag指示为前驱/后继节点
3,中序线索化二叉树
3.1,转换规则
- 中序输出规则为先输出左侧节点,再输出当前节点,最后输出右侧节点
- 如果左侧节点为空,递归左侧进行处理
- 处理当前节点
- 如果左侧节点为空,填充为前驱节点,即 preNode,此时如果该节点是第一个需要遍历的节点,则
preNode
为空,单leftFlag
会被修改为前驱节点 - 如果右侧节点为空,填充为后继节点,同样,如果该节点是最后一个节点,则值为空,标志位为后继节点
- 因为当前节点拿不到下一个树节点,所以填充右侧后继节点需要遍历到下一个节点后进行处理,等到遍历到下一个节点时, 此时当前节点为 preNode
- 当前节点(preNode)的后继节点即下一个节点,也就是当前遍历到的节点,此时设置后继节点, 即把当前遍历到的节点设置为preNode的右侧节点
- 如果右侧节点不为空,递归右侧进行处理
3.2,示意图
- 上图表示二叉树转换为线索化二叉树后的节点关系,即前驱后继节点关联关系
- 其中蓝色数字表示数节点,红色数组表示前驱/后继节点
- 实线表示左右节点的连线关系,虚线表示节点的前驱后继关联关系
- 9节点旁边的8颜色标错了,应该是红色
3.3,遍历规则
- 首先一直向左循环拿到
leftFlag
标志为前驱节点的节点,表示最左侧节点,也就是中序遍历需要遍历到的第一个节点 - 中序遍历方式的节点打印顺序为:左中右
- 此时先打印该节点,表示遍历到的第一个节点
- 打印完成后,循环向右找后继节点的右侧节点并顺序打印
- 直接循环到有效树节点,则以该节点为顶层节点,继续第一步处理
3.4,代码示例
package com.self.datastructure.tree.binarytree;
import lombok.Data;
import lombok.ToString;
public class ClueBinaryTree {
public static void main(String[] args) {
MyBinaryTree binaryTree = new MyBinaryTree();
binaryTree.addNode(5);
binaryTree.addNode(2);
binaryTree.addNode(1);
binaryTree.addNode(4);
binaryTree.addNode(3);
binaryTree.addNode(8);
binaryTree.addNode(6);
binaryTree.addNode(9);
binaryTree.addNode(7);
System.out.println("中序生成线索化二叉树...");
binaryTree.middleClueBinaryTree();
System.out.println("\r\n中序遍历线索化二叉树...");
binaryTree.middleShowDetails();
binaryTree.postShowDetails();
}
static class MyBinaryTree {
private Node node;
private Node preNode;
public void middleClueBinaryTree() {
doMiddleClueBinaryTree(node);
}
public void doMiddleClueBinaryTree(Node node) {
if (null == node) {
return;
}
if (node.getLeftFlag() == 0) {
doMiddleClueBinaryTree(node.getLeftNode());
}
System.out.print(node.getData() + " ");
if (null == node.getLeftNode()) {
node.setLeftNode(preNode);
node.setLeftFlag(1);
}
if (null != preNode && null == preNode.getRightNode()) {
preNode.setRightNode(node);
preNode.setRightFlag(1);
}
preNode = node;
if (node.getRightFlag() == 0) {
doMiddleClueBinaryTree(node.getRightNode());
}
}
public void middleShowDetails() {
doMiddleShowDetails(node);
}
public void doMiddleShowDetails(Node node) {
for (;null != node;) {
for (;node.getLeftFlag() == 0;) {
node = node.getLeftNode();
}
System.out.print(node.getData() + " ");
for (;node.getRightFlag() == 1;) {
node = node.getRightNode();
System.out.print(node.getData() + " ");
}
node = node.getRightNode();
}
}
}
@Data
@ToString
static class Node {
private Integer data;
private Node leftNode;
private Node rightNode;
private int leftFlag;
private int rightFlag;
public Node() {}
public Node(Integer data) {
this(data, null, null);
}
public Node(Integer data, Node leftNode, Node rightNode) {
this.data = data;
this.leftNode = leftNode;
this.rightNode = rightNode;
}
}
}
4,前序线索化二叉树
4.1,转换规则
- 前序相对于中序来讲相对比较简单
- 前序输出规则为:先输出当前节点,再输出左侧节点,最后输出右侧节点
- 处理当前节点
- 填充规则与中序完全一致
- 左侧节点为空,填充
preNode
节点为前驱节点 - 右侧节点为空,填充下一个遍历到的节点为后继节点
- 再递归处理左侧节点,此时注意左侧节点如果为前驱节点则不处理
- 最后递归处理右侧节点,右侧节点为后继节点则不处理
4.2,示意图
4.3,便利规则
- 前序遍历相对于中序遍历稍微简单
- 前序遍历首先从顶层节点向左遍历,如果左侧节点为有效树节点,则输出后继续向左遍历
- 最终遍历到一个节点的左侧节点为前驱节点,则获取该节点的右侧节点继续进行遍历
- 此时右侧节点可能为后继节点,也可能为有效的树节点,但在前序中区分的意义不大,可以直接遍历打印,直到遍历到最后一个节点,其右侧几点即后继节点为null
4.4,代码示例
package com.self.datastructure.tree.binarytree;
import lombok.Data;
import lombok.ToString;
public class ClueBinaryTree {
public static void main(String[] args) {
MyBinaryTree binaryTree = new MyBinaryTree();
binaryTree.addNode(5);
binaryTree.addNode(2);
binaryTree.addNode(1);
binaryTree.addNode(4);
binaryTree.addNode(3);
binaryTree.addNode(8);
binaryTree.addNode(6);
binaryTree.addNode(9);
binaryTree.addNode(7);
System.out.println("\r\n前序生成线索化二叉树");
binaryTree.preClueBinaryTree();
System.out.println("\r\n前序遍历线索化二叉树");
binaryTree.preShowDetails();
}
static class MyBinaryTree {
private Node node;
private Node preNode;
public void preClueBinaryTree() {
doPreClueBinaryTree(node);
}
public void doPreClueBinaryTree(Node node) {
if (null == node) {
return;
}
System.out.print(node.getData() + " ");
if (null == node.getLeftNode()) {
node.setLeftNode(preNode);
node.setLeftFlag(1);
}
if (null != preNode && null == preNode.getRightNode()) {
preNode.setRightNode(node);
preNode.setRightFlag(1);
}
preNode = node;
if (node.getLeftFlag() == 0) {
doPreClueBinaryTree(node.getLeftNode());
}
if (node.getRightFlag() == 0) {
doPreClueBinaryTree(node.getRightNode());
}
}
public void preShowDetails() {
doPreShowDetails(node);
}
public void doPreShowDetails(Node node) {
for (;null != node;) {
for (;0 == node.getLeftFlag();) {
System.out.print(node.getData() + " ");
node = node.getLeftNode();
}
System.out.print(node.getData() + " ");
node = node.getRightNode();
}
}
public void addNode(Integer data) {
if (null == node) {
node = new Node(data);
} else {
addNode(data, node);
}
}
private void addNode(Integer data, Node node) {
if (null == node) {
throw new RuntimeException("Node 节点为空");
}
if (data > node.getData()) {
Node rightNode = node.getRightNode();
if (null == rightNode) {
node.setRightNode(new Node(data));
} else {
addNode(data, node.getRightNode());
}
} else if (data < node.getData()) {
Node leftNode = node.getLeftNode();
if (null == leftNode) {
node.setLeftNode(new Node(data));
} else {
addNode(data, node.getLeftNode());
}
} else {
System.out.println("数据节点已经存在");
}
}
}
@Data
@ToString
static class Node {
private Integer data;
private Node leftNode;
private Node rightNode;
private int leftFlag;
private int rightFlag;
public Node() {}
public Node(Integer data) {
this(data, null, null);
}
public Node(Integer data, Node leftNode, Node rightNode) {
this.data = data;
this.leftNode = leftNode;
this.rightNode = rightNode;
}
}
}
5,后序线索化二叉树
5.1,转换规则
- 后续输出的顺序为:先输出左侧节点,再输出右侧节点,最后输出当前节点
- 所以在后续转换时,先递归处理左侧节点
- 再递归处理右侧节点
- 最后处理当前节点,当前节点的前驱和后继节点填充与之前完成一致
- 重点:后续输出顺序为左右中,所以对于一个完整子树来说,左右侧输出完成后,右侧的后继节点为它的父节点,即中间节点;此时中间节点如果为左侧节点,输出后需要再次输出其对应的右侧节点,也就是
parentNode.rightNode
,以之前前序中序的转换和遍历法则肯定不足以满足,需要在Node
实体类中添加parentNode
属性 - 添加完
parentNode
属性后,在二叉树转换线索化二叉树时,可直接对该属性进行维护
5.2,示意图
5.3,遍历规则
- 后续遍历与前序和中序有所不同,并且相对复杂,后续遍历注意添加了一个新属性:
parentNode
- 因为后续遍历的打印顺序为:左右中,所以首先还是获取最左侧节点,即
leftFlag
为0的最左侧节点, - 首先判断右侧节点是否为后继节点,如果右侧节点为后继节点,则打印该节点,并循环继续判断下一个右侧节点,直接非后继节点为止
- 获取到非后继节点后,判断当前节点的右侧节点与上一个处理节点是否一致,或者在当前节点的右侧节点为后继节点时,当前节点的左侧节点与上一个处理节点是否一致
- 如果上一步能匹配到,说明以当前节点为顶层节点的子树已经遍历完成, 继续以该节点的父节点进行上一步判断,并以此类推
- 如果上一步没有匹配到,则说明上一个处理节点为当前节点的左侧节点,需要继续遍历左树进行处理,则对
node
重新赋值左树处理
5.4,代码示例
package com.self.datastructure.tree.binarytree;
import lombok.Data;
import lombok.ToString;
public class ClueBinaryTree {
public static void main(String[] args) {
MyBinaryTree binaryTree = new MyBinaryTree();
binaryTree.addNode(5);
binaryTree.addNode(2);
binaryTree.addNode(1);
binaryTree.addNode(4);
binaryTree.addNode(3);
binaryTree.addNode(8);
binaryTree.addNode(6);
binaryTree.addNode(9);
binaryTree.addNode(7);
System.out.println("\r\n后续生成线索化二叉树");
binaryTree.postClueBinaryTree();
System.out.println("\r\n后续遍历线索化二叉树");
binaryTree.postShowDetails();
}
static class MyBinaryTree {
private Node node;
private Node preNode;
public void postClueBinaryTree() {
doPostClueBinaryTree(node, null);
}
public void doPostClueBinaryTree(Node node, Node parentNode) {
if (null == node) {
return;
}
doPostClueBinaryTree(node.getLeftNode(), node);
doPostClueBinaryTree(node.getRightNode(), node);
System.out.print(node.getData() + " ");
if (null == node.getLeftNode()) {
node.setLeftNode(preNode);
node.setLeftFlag(1);
}
if (null != preNode && null == preNode.getRightNode()) {
preNode.setRightNode(node);
preNode.setRightFlag(1);
}
node.setParentNode(parentNode);
preNode = node;
}
public void postShowDetails() {
doPostShowDetails(node);
}
public void doPostShowDetails(Node node) {
Node preNode = null;
for (;null != node;) {
for (;0 == node.getLeftFlag();) {
node = node.getLeftNode();
}
for (;1 == node.getRightFlag();) {
System.out.print(node.getData() + " ");
preNode = node;
node = node.getRightNode();
}
for (;preNode == node.getRightNode() || (1 == node.getRightFlag() && preNode == node.getLeftNode());) {
System.out.print(node.getData() + " ");
if (this.node == node) {
return;
}
preNode = node;
node = node.getParentNode();
}
node = node.getRightNode();
}
}
public void addNode(Integer data) {
if (null == node) {
node = new Node(data);
} else {
addNode(data, node);
}
}
private void addNode(Integer data, Node node) {
if (null == node) {
throw new RuntimeException("Node 节点为空");
}
if (data > node.getData()) {
Node rightNode = node.getRightNode();
if (null == rightNode) {
node.setRightNode(new Node(data));
} else {
addNode(data, node.getRightNode());
}
} else if (data < node.getData()) {
Node leftNode = node.getLeftNode();
if (null == leftNode) {
node.setLeftNode(new Node(data));
} else {
addNode(data, node.getLeftNode());
}
} else {
System.out.println("数据节点已经存在");
}
}
}
@Data
@ToString
static class Node {
private Integer data;
private Node leftNode;
private Node rightNode;
private Node parentNode;
private int leftFlag;
private int rightFlag;
public Node() {}
public Node(Integer data) {
this(data, null, null);
}
public Node(Integer data, Node leftNode, Node rightNode) {
this.data = data;
this.leftNode = leftNode;
this.rightNode = rightNode;
}
}
}