一、相关介绍
1.基本介绍
线索二叉树:对节点加上线索线索后的二叉树称为线索二叉树;
建立线索二叉树:通过某种遍历方式对二叉树进行遍历,并在遍历过程中对其节点添加线索,使其变为线索二叉树;
2.相关概念
1)对于n个结点的二叉树,在二叉链存储结构中有n+1个空链域(理解:n个节点共有2n个指针域,而n个节点共消耗n-1个指针域,所以空指针域共有2n-n+1=n+1个),利用这些空链域存放在某种遍历次序下该结点的前驱结点和后继结点的指针,这些指针称为线索,加上线索的二叉树称为线索二叉树;
2)这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种;
3)前驱结点:固定遍历方式下,当前节点的前一个节点;后继结点:固定遍历方式下,当前节点的下一个节点;
下图是普通二叉树和线索二叉树的对比(其中紫线指向前驱结点、红线指向后继节点):
图1:
线索化二叉树很好地避免了二叉树中节点指针域的浪费,且对二叉树进行线索化后,每个节点都可以很方便地找到自己的前驱结点和后继节点(其中第一个节点没有前驱结点,最后一个节点没有后继节点)。
二、建立线索二叉树
我们要知道对二叉树进行线索化是通过某种遍历方式进行的,所以根据遍历方式的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。比如,中序线索二叉树是二叉树通过中序遍历的方式去进行线索化,其过程是按照中序遍历的顺序为节点添加其前驱结点和后继节点。如图1中,我简单展示如何对左面的二叉树进行中序线索化:按照中序遍历的顺序,找到的第一个节点应该是4,然后是节点2,依次类推,当遍历到该节点时我们需要确定他的前驱结点和后继节点。
大家知道二叉树的每个节点有两个指针,分别指向其左、右子节点,节点结构如下:
但是大家很明显可以从图1中线索二叉树中发现,某些节点(比如1节点)的左右指针不一定指向的是前驱节点或后继节点,而是他本身指向的左右子节点,所以为了区分当前节点的左右指针具体指向,需要为节点添加两个属性:left_type、right_type,left_type=0表示左指针指向左子节点,left_type=1表示左指针指向他的前驱结点,对于right_type设置也一样。
此时我们就可以开始观察二叉树实现线索化的过程:
1.前序线索二叉树
//前序线索化
public void preThreadedNode(){
this.preThreadedNode(root);
}
public void preThreadedNode(Employee_Node node){
// System.out.println(node);
if(node==null){
return;
}
// 处理当前节点
// 前驱节点
if(node.getLeft()==null){
node.setLeft(preNode);
node.setLeftType(1);
}
// 后继节点
if(preNode!=null&&preNode.getRight()==null&&preNode.getLeft()!=node){
preNode.setRight(node);
preNode.setRightType(1);
}
preNode=node;
// 左子树线索化
if(node.getLeftType()==0) {
preThreadedNode(node.getLeft());
}
// 右子树线索化
preThreadedNode(node.getRight());
}
2.中序线索二叉树
中序线索化是按照中序遍历二叉树的顺序为节点添加线索,过程如下:
- 1)判断当前节点是否为空,如果是直接返回;
- 2)否则,对当前节点左子树进行中序线索化;
- 3)对当前节点进行线索化(确定他的前驱结点和后继节点);
- 4)对当前节点右子树进行中序线索化;
在代码实现过程中,有两个点需要注意:
- 从一开始设置一个preNode节点,记录当前节点的前驱结点,最开始默认为null;
- 当前节点的后继节点通过下一个节点设置,比如节点4为当前节点,其前驱节点preNode=null,要想确定其后继节点为2,我们需要先移动使得当前节点为2,其preNode=4,此时preNode=4的后继节点不就正好是2吗?
// 中序线索化
public void middleThreadedNode(){ //重载
this.middleThreadedNode(root);
}
public void middleThreadedNode(Employee_Node node){ // 线索化当前节点
if(node==null){
return;
}
middleThreadedNode(node.getLeft()); //左子树线索化
//当前节点线索化
// 处理前驱结点
if(node.getLeft()==null){
node.setLeft(preNode);
node.setLeftType(1); //说明left指向前驱结点
}
//处理后继节点:当前节点的后继节点通过下一个节点设置,因为下一个节点的前驱结点就是当前节点
if(preNode!=null&&preNode.getRight()==null){
preNode.setRight(node);
preNode.setRightType(1); //说明right指向前驱结点
}
//移动preNode,令其为当前节点
preNode=node;
middleThreadedNode(node.getRight());//右子树线索化
}
线索化过程如下:
第一步:
第二步:
依次类推…
3.后序线索二叉树
// 后续线索化
public void postThreadedNode(){
postThreadedNode(root);
}
public void postThreadedNode(Employee_Node node){
if(node==null){
return;
}
// 左子树线索化
postThreadedNode(node.getLeft());
// 右子树线索化
postThreadedNode(node.getRight());
// 当前节点线索化
// 前驱节点
if(node.getLeft()==null){
node.setLeft(preNode);
node.setLeftType(1);
}
if(preNode!=null&&preNode.getRight()==null){
preNode.setRight(node);
preNode.setRightType(1);
}
preNode=node;
}
三、线索二叉树遍历
这里需要明确的是1)对线索二叉树进行遍历时,其遍历方式要和线索化方式对应一致,比如中序线索二叉树就要使用中序遍历。2)线索二叉树的遍历不能使用普通二叉树的遍历算法(普通二叉树的遍历通过判断某个节点的左右子节点是否为null来做递归终止条件,而线索二叉树的节点不存在空指针域),要对其进行一定的修改,因为每个节点都可以找到对应的前驱结点和后继节点,我们需要通过一种“线性”的方式去遍历二叉树。
1.中序线索二叉树遍历
// 中序遍历:不能使用原来的遍历方式,
// 非递归方式
public void middleThreadedOrder(){
Employee_Node tempNode=root; // 临时存储当前节点
while (tempNode!=null){
// 第一次肯定找到的是中序遍历顺序的第一个节点8
// 寻找经历线索化的第一个节点
while (tempNode.getLeftType()!=1){
tempNode=tempNode.getLeft();
}
// 输出当前节点
System.out.println(tempNode);
//处理后继节点,直到某个节点的right指向的是右子树为止
while (tempNode.getRightType()==1){
tempNode=tempNode.getRight();
System.out.println(tempNode);
}
tempNode=tempNode.getRight(); //移动当前节点
}
}
四、完整代码
package com.north.tree;
public class ThreadedBinaryTreeDemo {
public static void main(String[] args){
System.out.println("线索化二叉树...");
Employee_Node root=new Employee_Node(1,"杨俊荣");
Employee_Node node2=new Employee_Node(2,"周杰伦");
Employee_Node node3=new Employee_Node(3,"方文山");
Employee_Node node4=new Employee_Node(4,"派俊伟");
Employee_Node node5=new Employee_Node(5,"弹头");
Employee_Node node6=new Employee_Node(6,"黄俊郎");
Employee_Node node7=new Employee_Node(7,"昆凌");
// 构建二叉树模型
ThreadedBinaryTree tree=new ThreadedBinaryTree();
tree.setRoot(root);
root.setLeft(node2);
root.setRight(node3);
node2.setLeft(node4);
node2.setRight(node5);
node3.setLeft(node6);
node3.setRight(node7);
// 中序线索化二叉树
// tree.middleThreadedNode();
// 测试
// System.out.println("node5的前驱结点为:"+node5.getLeft());
// System.out.println("node5的后继结点为:"+node5.getRight());
// 使用线索化二叉树进行中序遍历
// tree.middleThreadedOrder();
// 前序线索化
// tree.preThreadedNode();
//后续线索化
tree.postThreadedNode();
System.out.println("node5的前驱结点为:"+node5.getLeft());
System.out.println("node5的后继结点为:"+node5.getRight());
System.out.println("node6的前驱结点为:"+node6.getLeft());
System.out.println("node6的后继结点为:"+node6.getRight());
System.out.println("node7的前驱结点为:"+node7.getLeft());
System.out.println("node7的后继结点为:"+node7.getRight());
}
}
// 构建二叉树
class ThreadedBinaryTree{
private Employee_Node root; // 根节点
private Employee_Node preNode; //记录当前节点的前一个节点(就中序遍历的结果中的顺序而言)
public void setRoot(Employee_Node root) {
this.root = root;
}
//前序线索化
public void preThreadedNode(){
this.preThreadedNode(root);
}
public void preThreadedNode(Employee_Node node){
// System.out.println(node);
if(node==null){
return;
}
// 处理当前节点
// 前驱节点
if(node.getLeft()==null){
node.setLeft(preNode);
node.setLeftType(1);
}
// 后继节点
if(preNode!=null&&preNode.getRight()==null&&preNode.getLeft()!=node){
preNode.setRight(node);
preNode.setRightType(1);
}
preNode=node;
// 左子树线索化
if(node.getLeftType()==0) {
preThreadedNode(node.getLeft());
}
// 右子树线索化
preThreadedNode(node.getRight());
}
// 中序线索化
public void middleThreadedNode(){
this.middleThreadedNode(root);
}
public void middleThreadedNode(Employee_Node node){ // 线索化当前节点
if(node==null){
return;
}
middleThreadedNode(node.getLeft()); //左子树线索化
//当前节点线索化
// 处理前驱结点
if(node.getLeft()==null){
node.setLeft(preNode);
node.setLeftType(1); //说明left指向前驱结点
}
//处理后继节点:当前节点的后继节点通过下一个节点设置,因为下一个节点的前驱结点就是当前节点
if(preNode!=null&&preNode.getRight()==null){
preNode.setRight(node);
preNode.setRightType(1); //说明right指向前驱结点
}
//移动preNode,令其为当前节点
preNode=node;
middleThreadedNode(node.getRight());//右子树线索化
}
// 后续线索化
public void postThreadedNode(){
postThreadedNode(root);
}
public void postThreadedNode(Employee_Node node){
if(node==null){
return;
}
// 左子树线索化
postThreadedNode(node.getLeft());
// 右子树线索化
postThreadedNode(node.getRight());
// 当前节点线索化
// 前驱节点
if(node.getLeft()==null){
node.setLeft(preNode);
node.setLeftType(1);
}
if(preNode!=null&&preNode.getRight()==null){
preNode.setRight(node);
preNode.setRightType(1);
}
preNode=node;
}
// 中序遍历:不能使用原来的遍历方式,
// 非递归方式
public void middleThreadedOrder(){
Employee_Node tempNode=root; // 临时存储当前节点
while (tempNode!=null){
// 第一次肯定找到的是中序遍历顺序的第一个节点8
// 寻找经历线索化的第一个节点
while (tempNode.getLeftType()!=1){
tempNode=tempNode.getLeft();
}
// 输出当前节点
System.out.println(tempNode);
//处理后继节点,直到某个节点的right指向的是右子树为止
while (tempNode.getRightType()==1){
tempNode=tempNode.getRight();
System.out.println(tempNode);
}
tempNode=tempNode.getRight(); //移动当前节点
}
}
}
// 构建节点
class Employee_Node{
private int number;
private String name;
private Employee_Node left; //左子节点
private Employee_Node right; //右子节点
private int leftType=0; // 0表示left指向左子树,1表示指向前驱节点
private int rightType=0; // 0表示right指向右子树,1表示指向后继节点
public Employee_Node(int number, String name) {
this.number = number;
this.name = name;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Employee_Node getLeft() {
return left;
}
public void setLeft(Employee_Node left) {
this.left = left;
}
public Employee_Node getRight() {
return right;
}
public void setRight(Employee_Node right) {
this.right = right;
}
public int getLeftType() {
return leftType;
}
public void setLeftType(int leftType) {
this.leftType = leftType;
}
public int getRightType() {
return rightType;
}
public void setRightType(int rightType) {
this.rightType = rightType;
}
@Override
public String toString() {
return "Employee_Node{" +
"number=" + number +
", name='" + name+"}";
}
}