中序线索化二叉树
1. 简介
对于二叉树来说,只能是很直观地知道某个节点及其左右子节点,但是如果想知道此节点按照某种方式遍历时的前一个节点(前继节点)和后一个节点(后继节点)的话,就不得不按照此种遍历方式对二叉树遍历一次,只有这样才能知道前继或后继节点。因此,为了避免这种不得不遍历的麻烦,是否可以寻找一种简便的方式从而更快地获取某节点的前继或后继节点? 我认为这是对二叉树线索化的主要原因。
另一方面是涉及到资源利用的问题。如上图所示,二叉树共有6(n,n=6)个节点,每个节点共有左、右两个指针域,因此整个二叉树共有12(2n,n=6)个指针域。但是,实际上只有5(n-1,n=6)个指针域被赋值,而另外7(n+1,n=6)个则是空指针,这造成了一定程度上的资源浪费,但指针域必须存在,也就是说这种浪费如果不加以处理的话,是无法避免的。因此,这也是对二叉树做线索化的第二个原因,即对资源的充分利用。
2. 中序线索化的方法
将上面的二叉树简单化,如下所示:
如果对此二叉树做中序遍历的话,输出应是:8、3、10、1、14、6。按此顺序,就8节点来看,因为8是第一个,所以8前面是没有节点,也就是说它前面指向的应当是null。8节点后面输出的3节点,也就是说8节点后面指向的是3节点。
那么,用何种方式来表达这种指向关系? 上面所提到的资源利用问题中指出,指针域并没有完全利用,而8节点的左右指针域均为空,因此,可以利用左指针域指向前面一个节点,利用右指针域指向后面一个节点(下文中分别成为前继节点和后继节点)。同理,10节点、14节点、6节点均有指针域未利用,可以利用这些空域来指向前后的节点。比如,10节点的前继节点是3节点,后继节点是1节点;14节点的前继节点是1节点,后继节点是6节点;6节点的后继节点是null。如下图所示:
到目前为止,存在一个问题,当所有指针域全部利用之后,有的指针域指向的是前继节点或者后继节点,但是有的指针域指向的是左子节点或者右子节点,如何区分指向的节点是属于何种类别呢?因此,这需要对每个节点的构造做一些改动,在原有基础上加上两个标签:
在修改过后的Node类中,isLeftChild和isRightChild都是boolean型的,当指针域指向的是孩子节点时,为true,若指向的是前继或者后继节点时,为false。 例如,6节点的isLeftChild为true,因为左指针域指向的是14节点,是6节点的左子节点,而isRightChild则为false,因为右指针域指向的是null,代表的是后继节点。
节点类修改过后的二叉树示意图如下:
以上是二叉树的视角,不过,若是分别从前继和后继的角度来看,其实可以看成一个双向链表:
红色代表前继,绿色代表后继。这实际上是代表了一种关系,可以说是先后关系,当然,也可以成为“线索”。根据这种线索,可以比较方便地查找到在中序遍历的方式中某个节点的前继节点或后继节点。
小问题:蓝色方框里的1节点和14节点实际上是没有直接的后继关系的(当然,不仅仅是这两个节点之间)。虽然14节点的前继节点是1节点,这种关系很明确,但是1节点却不存在后继节点,只有右子节点6节点,而6节点又不存在前继节点,只有左子节点14节点。因此,从1节点到14节点并不是直接靠前继或者后继得到的,而是需要进一步的查找。这种方法在下面的遍历函数中有提及。由此可知,上述的双向链表实际上是一种逻辑结构,并不真正的是物理结构,这一点需要注意。
3. 代码详细
首先,构造节点类Node:
// 节点类
class Node{
private int number;
// 左右指针域
private Node left;
private Node right;
// 左右指针域的标签
// 标签设置默认为true,因为对于暂未线索化的二叉树而言
// 左右指针域无论是否为空,都是指向孩子节点的
// 当然,如果默认为false,我认为也行,稍加改动即可
private boolean isLeftChild = true;
private boolean isRightChild = true;
}
其次,构造中序线索化二叉树类InOrderThreadedBinaryTree:
class InOrderThreadedBinaryTree{
// 二叉树的根节点
private Node root;
// 这里定义一个节点变量,用以暂存线索化时的上一个处理节点
private Node preNode = null;
// 构造器
public InOrderThreadedBinaryTree(Node root) {
this.root = root;
}
// 对二叉树做中序线索化的方法(重载)
public void in_order_threaded(){
in_order_threaded(root);
}
public void in_order_threaded(Node node){
}
// 中序线索化二叉树的遍历
public void list(){
}
}
下面,主要是详细看看中序线索化的方法及其遍历方法。
中序线索化的方法:
public void in_order_threaded(Node node){
// 如果节点为空,无法线索化,这是函数递归停止的条件
if (node == null){
return;
}
// 首先对左子树做中序线索化
in_order_threaded(node.getLeft());
// 再对当前节点做中序线索化,处理的是当前节点的前继关系
// 因为preNode指向的是当前节点的前一个结点
// 因此,当当前节点Node的左子节点为null时,就将左指针域赋值为preNode
// 并设置标签为false,表明左指针域指向的是前继节点
if (node.getLeft() == null){
node.setLeft(preNode);
node.setLeftChild(false);
}
// 处理上一个节点的后继关系
// 在这个方法中,并没有直接处理Node节点的后继关系,因为此时并不会遍历到Node的后继节点
// 于是,Node节点的后继关系处理放在了其下一个节点的处理中
// 将Node赋值给preNode,preNode的右指针域指向Node(此处的Node为后继节点)
if (preNode != null && preNode.getRight() == null){
preNode.setRight(node);
preNode.setRightChild(false);
}
// 当然,对当前节点处理完之后,preNode被赋值为Node
preNode = node;
// 最后对右子树做中序线索化
in_order_threaded(node.getRight());
}
此处,要明确的是线索化的顺序!因为既然是中序线索化,那就应该首先是对左子树线索化,再对根节点做线索化,最后对右子树做线索化,这就和中序遍历的顺序是一样的。
中序线索化的遍历方法:(此处是按照后继关系遍历的)
public void list(){
// 定义一个节点,表示遍历从根开始
Node node = root;
while (node != null){
// 寻找左子树的遍历起始节点
while (node.isLeftChild()){
node = node.getLeft();
}
// 首先输出起始节点
System.out.print(node.getNumber() + " ");
// 按照后继关系,一直输出,知道右孩子节点位置
while (!node.isRightChild()){
node = node.getRight();
System.out.print(node.getNumber() + " ");
}
// 下一个循环从右孩子结点开始
node = node.getRight();
}
}
将以上遍历方法画图表示:
4. 测试
换其他的二叉树看看输出结果是否正确:
输出结果:
输出结果:
输出结果: