我们知道二叉树的是一棵树的度小于等于2
的有序树
;那么线索二叉树又是什么呢?其实线索二叉树实际是一棵变形的二叉树。
如果有一种算法需要经常的对一棵二叉树进行遍历,那么遍历的过程就是在频繁的利用递归或栈做重复性的操作,而线索二叉树不需要如此,通过使用二叉树空闲的内存空间记录某些结点的前趋和后继结点,在遍历的时候,就可以利用好这些保存的结点信息,提高遍历效率。
那么使用这种方式(利用好二叉树空闲内存的方式)构建的二叉树就是一颗线索二叉树
普通二叉树中存在指针浪费的问题:
我们知道线索二叉树相比普通二叉树而言,就是利用了普通二叉树的空闲内存,记录了某些结点的前趋和后继元素信息。我们先来了解一个前提,这个空闲内存到底指的是什么呢?
我们看到上面的图,该树就是一颗很普通的二叉树,结点结构也很简单,就是数据域,左孩子指针,右孩子指针。该树有7
个结点,所以该树就有2*7 = 14个指针空间,这14
个指针空间就是我们所说的内存空间。
而空闲的内存空间又指的是什么呢?很简单,就是14个指针空间中指向null的空间,比如D,E,F
结点的左右孩子指针和C
结点的左孩子指针。因为这些空间并没有指向左右孩子,本质上就是一种浪费。本着有好东西就不能浪费的理念,所以就出现了线索二叉树
空闲指针的规律:
- 存在n个结点的二叉链表必定存在n + 1个空指针域
- 不要问为什么,数一下就知道了
所以线索二叉树实际上就是一棵利用普通二叉树剩下的这些空指针域去存储结点之间的前趋和后继关系的特殊二叉树
线索二叉树的结点结构:
我们知道,通常二叉树在代码实现中,是通过二叉链的形式去表达的,通常都是一个数据域,两个指针域
但我们知道,在线索二叉树中,如果结点有左孩子,那么
lchild
就会指向左孩子,否则lchild
就会指向该结点的直接前趋结点;同样,如果该结点有右孩子,那么rchild
就会指向右孩子,否则rchild
就会指向该结点的直接后继结点。
也就是说,左右孩子指针是有双重意义的,这很容易让人产生迷惑,所以为了避免指针域指向结点的意义混淆,需要在普通二叉树的结点构造上做一些小小的改变,增加两个标志位
- 我们在原有的基础上,增加了两个标志位
- ltag值为0时,表示lchild指向的是该结点的左孩子;为1的时候,表示lchild指向的是该结点的直接前趋结点
- rtag值为0时,表示rchild指向的是该结点的右孩子;为1的时候,表示rchild指向的是该结点的直接后继结点
结点代码:
public static class TreeNode<E> {
/**
* 数据域
*/
private E data;
/**
* 子结点域
*/
private TreeNode<E> lchild, rchild;
/**
* tag域
* ltag为0时,表示lchild指向的是左孩子,如果ltag为1时,表示指向的是直接前趋结点
* rtag为0时,表示rchild指向的是右孩子,如果rtag为1时,表示指向的是直接后继结点
*/
private int ltag, rtag;
}
什么是普通二叉树线索化?就是给定一棵普通的二叉树,假如有n
个结点,那么这棵树必然有2n
个指针域,n-1
个空指针域。通过中序的方式线索化就是,通过中序遍历的顺序依次将该树的空指针域指向前趋或后继结点,填充空指针域,让一棵普通二叉树变成一棵
/**
* 通过中序线索化一颗普通二叉树
* 让其从普通二叉树变成线索二叉树
*/
public void createThreadTreeByMidOrder() {
if (this.rootNode == null) {
return;
}
createThreadTreeByMidOrder(this.rootNode);
}
/**
* 通过中序线索化一颗普通二叉树
* 让其从普通二叉树变成线索二叉树
* 逻辑就跟递归实现的中序遍历差不多
*/
private void createThreadTreeByMidOrder(TreeNode<T> root) {
//递归推出条件
if (root == null) {
return;
}
/**
* 1.先递归左子树
*/
createThreadTreeByMidOrder(root.lchild);
/**
* 2. 再到相对根节点
* 一开始的前趋后继链的第一个前趋结点必然为空
*/
//如果当前结点的左孩子等于null,则代表它是空指针域
if (root.lchild == null) {
//设置前趋结点和状态,前趋结点是一个临时全局变量
root.ltag = 1;
root.lchild = preNode;
}
//如果前趋结点不为null(不是链表头),就看前趋结点的右孩子等不等于null,如果是null,则填写后继结点
if (preNode != null && preNode.rchild == null) {
//前趋结点的右孩子为后继结点,后继结点是当前结点 | 要区分前趋结点和当前结点分别是什么,且该前趋是当前结点的前趋
preNode.rtag = 1;
preNode.rchild = root;
}
//每处理完一个结点,当前root结点就是下一个结点的前趋结点
preNode = root;
/**
* 3. 递归右子树
*/
createThreadTreeByMidOrder(root.rchild);
}
相对根结点==null
preNode
, 默认初始为null最左叶子结点A
的左指针,它必然是空指针域,指向null, 这个最左子结点A
就是的当前相对根结点
,其ltag变为1,代表lchild指向前趋结点,非左孩子。同时结点A的lchild指向临时变量preNode(第一次必然为null)当前相对根结点
前趋结点preNode
是否为null, 且preNode
的右指针rchild
是否也是空指针域。为什么要判断前趋结点的右指针呢?因为每一轮遍的第1步都是先解决左指针,而过了这轮遍历,下一轮开始,上轮的当前结点就会成为本轮的前趋结点, 我们就要解决上一轮相对根结点(本轮的前趋结点)的右指针rchild
; 即本轮解决当前结点A的左指针,下一轮就会解决本轮前趋结点(结点A)的右指针什么是线索二叉树的中序遍历呢?普通二叉树的遍历,我们都知道,左->根->右
;但是线索二叉树的中序遍历还需要这样子吗?那必须有更加高效的方式啦!
/**
* 中序遍历线索二叉树 | 迭代方式
* 1. 找到最左叶子节点,然后向右开始线索遍历,找后继结点
* 2. 因为有中断结点,所以无法一直都向右遍历,因为中断结点本身有右子树,所以无法找到后继结点
* 3. 因为中断结点的影响,所以需要找到右孩子,跳到第一层while,重写开始循环,即迭代子树
*/
public void midOrderWithIterator() {
//如果是空树,直接返回
if (this.rootNode == null) {
return;
}
//从根节点开始
TreeNode<T> node = this.rootNode;
//只要当前节点不为null,就一直遍历
while (node != null) {
//找到最左叶子结点,ltag == 1就退出循环
while (node.ltag == 0) {
node = node.lchild;
}
//输出
System.out.println(node.data);
//只要当前结点不是中断结点,那么直接输出可遍历到的后继结点
while (node.rtag == 1 && node.rchild != null) {
node = node.rchild;
System.out.println(node.data);
}
//如果被打破了条件,证明链表中断,当前node结点是中断结点,直接找中断结点的右孩子,然新的结点成为根结点,重新开始迭代,遍历子树
node = node.rchild;
}
}
主要功能:
TreeNode(线索二叉树结点)
public class TreeNode<E> {
/**
* 数据域
*/
private E data;
/**
* 子结点域
*/
private TreeNode<E> lchild, rchild;
/**
* tag域
* ltag为0时,表示lchild指向的是左孩子,如果ltag为1时,表示指向的是直接前趋结点
* rtag为0时,表示rchild指向的是右孩子,如果rtag为1时,表示指向的是直接后继结点
*/
private int ltag, rtag;
public TreeNode(E data) {
this.data = data;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof TreeNode)) return false;
TreeNode<?> treeNode = (TreeNode<?>) o;
if (ltag != treeNode.ltag) return false;
if (rtag != treeNode.rtag) return false;
if (data != null ? !data.equals(treeNode.data) : treeNode.data != null) return false;
if (lchild != null ? !lchild.equals(treeNode.lchild) : treeNode.lchild != null) return false;
return rchild != null ? rchild.equals(treeNode.rchild) : treeNode.rchild == null;
}
@Override
public int hashCode() {
int result = data != null ? data.hashCode() : 0;
result = 31 * result + (lchild != null ? lchild.hashCode() : 0);
result = 31 * result + (rchild != null ? rchild.hashCode() : 0);
result = 31 * result + ltag;
result = 31 * result + rtag;
return result;
}
}
ThreadTree(线索二叉树)
/**
* 线索二叉树
*
* @author liwenjie
*/
public class ThreadTree<T> {
/**
* 根节点
*/
private TreeNode<T> rootNode;
/**
* 前驱节点 | 临时存放,方法的公共变量
*/
private TreeNode<T> preNode;
/**
* 构造只有一个根节点的树
*
* @param rootNode
*/
public ThreadTree(TreeNode<T> rootNode) {
this.rootNode = rootNode;
}
/**
* 为某个节点添加左孩子
*
* @param parent
*/
public void addLChild(TreeNode<T> parent, TreeNode<T> lchild) {
if (this.rootNode == null || parent == null || parent.lchild != null) {
return;
}
parent.lchild = lchild;
}
/**
* 为某个节点添加右孩子
*
* @param parent
*/
public void addRChild(TreeNode<T> parent, TreeNode<T> rchild) {
if (this.rootNode == null || parent == null || parent.rchild != null) {
return;
}
parent.rchild = rchild;
}
/**
* 通过中序线索化一颗普通二叉树
* 让其从普通二叉树变成线索二叉树
*/
public void createThreadTreeByMidOrder() {
if (this.rootNode == null) {
return;
}
createThreadTreeByMidOrder(this.rootNode);
}
/**
* 通过中序线索化一颗普通二叉树
* 让其从普通二叉树变成线索二叉树
* 逻辑就跟递归实现的中序遍历差不多
*/
private void createThreadTreeByMidOrder(TreeNode<T> root) {
//递归推出条件
if (root == null) {
return;
}
/**
* 1.先递归左子树
*/
createThreadTreeByMidOrder(root.lchild);
/**
* 2. 再到相对根节点
* 一开始的前趋后继链的第一个前趋结点必然为空
*/
//如果当前结点的左孩子等于null,则代表它是空指针域
if (root.lchild == null) {
//设置前趋结点和状态,前趋结点是一个临时全局变量
root.ltag = 1;
root.lchild = preNode;
}
//如果前趋结点不为null(不是链表头),就看前趋结点的右孩子等不等于null,如果是null,则填写后继结点
if (preNode != null && preNode.rchild == null) {
//前趋结点的右孩子为后继结点,后继结点是当前结点 | 要区分前趋结点和当前结点分别是什么,且该前趋是当前结点的前趋
preNode.rtag = 1;
preNode.rchild = root;
}
//每处理完一个结点,当前root结点就是下一个结点的前趋结点
preNode = root;
/**
* 3. 递归右子树
*/
createThreadTreeByMidOrder(root.rchild);
}
/**
* 中序遍历线索二叉树 | 迭代方式
* 1. 找到最左叶子节点,然后向右开始线索遍历,找后继结点
* 2. 因为有中断结点,所以无法一直都向右遍历,因为中断结点本身有右子树,所以无法找到后继结点
* 3. 因为中断结点的影响,所以需要找到右孩子,跳到第一层while,重写开始循环,即迭代子树
*/
public void midOrderWithIterator() {
//如果是空树,直接返回
if (this.rootNode == null) {
return;
}
//从根节点开始
TreeNode<T> node = this.rootNode;
//只要当前节点不为null,就一直遍历
while (node != null) {
//找到最左叶子结点,ltag == 1就退出循环
while (node.ltag == 0) {
node = node.lchild;
}
//输出
System.out.println(node.data);
//只要当前结点不是中断结点,那么直接输出可遍历到的后继结点
while (node.rtag == 1 && node.rchild != null) {
node = node.rchild;
System.out.println(node.data);
}
//如果被打破了条件,证明链表中断,当前node结点是中断结点,直接找中断结点的右孩子,然新的结点成为根结点,重新开始迭代,遍历子树
node = node.rchild;
}
}
public static void main(String[] args) {
TreeNode<Integer> node1 = new TreeNode<>(1);
TreeNode<Integer> node2 = new TreeNode<>(2);
TreeNode<Integer> node3 = new TreeNode<>(3);
TreeNode<Integer> node4 = new TreeNode<>(4);
TreeNode<Integer> node5 = new TreeNode<>(5);
TreeNode<Integer> node6 = new TreeNode<>(6);
TreeNode<Integer> node7 = new TreeNode<>(7);
ThreadTree<Integer> threadTree = new ThreadTree<>(node1);
threadTree.addLChild(node1, node2);
threadTree.addRChild(node1, node3);
threadTree.addLChild(node2, node4);
threadTree.addRChild(node2, node5);
threadTree.addLChild(node3, node6);
threadTree.addRChild(node3, node7);
threadTree.createThreadTreeByMidOrder();
threadTree.midOrderWithIterator();
}
}