先上一张图,如图1。我们可以发现二叉树中指针域并不是被充分利用了,有很多的 ^ ,也就是空指针域的存在。
对于一个有 n 个节点的二叉树,每个节点有左右孩子两个指针域,所以一共有 2n 个指针域。而 n 个节点的二叉树有 n - 1 条连线,所以存在 2n - ( n - 1 ) = n + 1 个空指针域。如图1所示的二叉树一共有10个节点,所以一共一11个空指针域。
这意味着空间的浪费,应该想办法将这些空间利用起来。
那么,怎么利用呢?
对于图1的二叉树的中序遍历是HDIBJEAFCG,我们可以知道节点B的前驱就是节点I,节点B的后继就是节点J,我们可以很清楚的知道一个节点的前驱和后继是哪一个。但是这是建立在二叉树被遍历的基础上。在二叉链表中,我们只知道每个节点指向其左右孩子的地址,并不知道某个节点的前驱和后继是谁。如果以后需要知道时,都必须先遍历一次。那为什么不在第一次遍历的时候就记住这些前驱和后继呢,这样将会节约大把的时间。线索二叉树就是这样诞生的。
综上所述
- 利用二叉链表的空指针域,存放指向该节点在某种次序下前驱和后继节点的指针(这种指针叫做线索)。
- 这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树。根据线索性质的不同,线索二叉树可以分为前序线索二叉树、中序线索二叉树、后序线索二叉树三种。
- 对二叉树以某种次序遍历使其变为线索二叉树的过程称作是线索化
问题来了?
我们怎么知道节点的指针域是指向它的孩子还是指向它的前驱和后继呢?比如图4中的E节点,lchild是指向左孩子J,而rchild是指向后继A。所以我们必须要区分lchild是指向左孩子还是前驱,rchild是指向右孩子还是后继。解决办法就是每个节点再增加两个标志域ltag和rtag,ltag和rtag存放布尔型变量。此时二叉树的节点如图5所示。
ltag为false时,lchild指向左孩子;ltag为true时,lchild指向前驱。
rtag为false时,rchild指向右孩子;rtag为true时,rchild指向后继。
注意:当二叉树被线索化后,原来直接递归遍历的那种方式就不能用了,否则会出现死循环。所以当线二叉树被线索化后,也需要重写遍历方法。另外,按前序、中序、后序的方式线索化和遍历二叉树实现是不同的。
代码实现
/**
* 线索二叉树
*
* @author chenzhiyuan
* @date 2019-10-04 10:49
*/
public class BinaryThreadTree {
private static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
private BinaryThreadNode root = null;
// 定义一个全局变量,在线索化的时候始终指向当前被遍历节点的前一个节点
private BinaryThreadNode pre = null;
public static void main(String[] args) throws IOException {
BinaryThreadTree binaryThreadTree = new BinaryThreadTree();
binaryThreadTree.buildBiTree();
// 先前序线索化,再遍历