相对于顺序存储结构而言,利用链式存储结构的二叉树已经有了很高的存储效率,单是还是有空间上未利用到的地方,比如说叶子结点的左右孩子是空的,指向左右孩子的指针就是空闲的,没有被利用到;而且,有时候给定一个结点,我们需要查找该结点的前驱结点和后继结点,如果按照中序遍历的做法去查找的话,对于一个非叶子结点,其前驱和后继结点查找可以以下算法:
1、preNode=node.left;//前去结点就是该结点的左孩子
2、subNode=search(node.right);//后继结点是该结点的右子树的最左边的叶子结点
但是,对于给定一个叶子结点,查找其前驱结点和后继结点就没那么容易了,但是思想也很容易,可以对树进行遍历,然后存储下遍历结果,排在它前面的结点就是前驱结点,排在其后面的就是后继结点。这种方法代价还是挺大的,无论是从空间上,还是从时间上说,代价都不小,故而还可以优化。这时候,可以从树的结构下手,进行优化,因为叶子结点中的左右指针是空的,我们何不利用起来呢?A.J.Perlis和C.Thornton二人设计出了一个精巧的方法,已利用这些空闲指针,那就是为二叉树建立线索,先用一张图描述一下,如下:
这里,从1开始以下的二叉树是我们初始创建的二叉树,而结点0是在对二叉树进行线索化的过程中添加的。线索化二叉树可以在遍历二叉树的过程中,更改指针的指向来完成,这里使用中序遍历法线索化二叉树,由于中序遍历时,第一个叶子结点没有前驱,最后一个叶子没有后继,这里使它们指向添加的头结点0.线索二叉树中的结点可用如下的类进行定义:
class TreeNode{ public int value=-1; public TreeNode(int value){ this.value=value; } public TreeNode left=null; public TreeNode right=null; //默认都是有孩子结点的 public int leftTag=0;//如果是0,表示left指向左孩子,否则指向前驱 public int rightTag=0;//如果是0,表示right指向右孩子,否则指向后继 public String toString(){ return " "+this.value; } }
其中,有两个标识,leftTag和rightTag,代码中已做了解释。对于给定的一棵二叉树,我们用root代表其根节点,在中序遍历二叉树的过程中,对其进行线索化的算法描述如下:
inOrderTraverse(root)
1、 head = new TreeNode(0);//创建头节点
2、 head.left = root; head.leftTag = 0;
3、 head.right = head; //头结点回指
4、 if(root == null) head.left = head;//二叉树为空
5、 else{
6、 head.left = root; //头节点的左指针指向root
7、 pre=head; las=head//pre记录前一个结点, las记录最后一个结点,也就是二叉树的最右边的叶子结点
8、 inThreading(root,pre,las);//线索化二叉树, 进行中序遍历
9、 las.right = head; las.rightTag=1; //中序遍历后,pre指向二叉树的最右边的叶子结点
10、 head.right = las; //头节点的右指针指向二叉树最右端的叶子结点
11、 }
12、 return head;//返回头节点
中序遍历二叉树并对其进行线索化的算法描述如下:
inThreading(root,pre,las)
1、if(root!=null)
2、 inThreading( root.left, pre, las);
3、 if(root.left = null){//判断是叶子结点
4、 root.leftTag=1; root.left = pre;//左指针指向前一个结点
5、 }
6、 if(pre.right = null){//该节点为叶子结点
7、 pre.rightTag=1; pre.right=root;// 该结点的右指针指向它的下一个结点
8、 }
9、 pre=root;
10、 las=root.right; //经过反复的递归调用,las最终指向二叉树的最右边的叶子结点
11、 inThreading( root.right,pre las);
因为java函数不支持地址传递,所以,我们在另一个函数中记录一个结点的地址(也就是引用)时,我们可以创建一个辅助对象,将待记录结点的地址赋值给该对象的变量,然后,我们再取出该对象的变量值即可。可得完整代码如下:
import java.util.*; class TreeNode{ public int value=-1; public TreeNode(int value){ this.value=value; } public TreeNode left=null; public TreeNode right=null; //默认都是有孩子结点的 public int leftTag=0;//如果是0,表示left指向左孩子,否则指向前驱 public int rightTag=0;//如果是0,表示right指向右孩子,否则指向后继 public String toString(){ return " "+this.value; } } public class Test{ public static void main(String[] args)throws InterruptedException{ int[] array={1,2,3,4,5,6,7,8,9}; TreeNode root=createTree(array); TreeNode head=inOrderTraverse(root);//建立线索二叉树 inOrderTrave(head); } //中序遍历线索二叉树 static void inOrderTrave(TreeNode head){ if(head.leftTag==1 && head.rightTag==1){//叶结点 System.out.println("叶结点 "+head.value+" 的前驱结点是 "+head.left.value+" 后继结点是 "+head.right.value); }else{ inOrderTrave(head.left); System.out.println("------------------------------------------结点 "+head.value+" 的leftTag是 "+head.leftTag+" rightTag是 "+head.rightTag); inOrderTrave(head.right); } } //线索化二叉树,注意,java不支持传递地址,所以为了记录所调用的其他函数中的对象,需要创建一个对象,用 //对象中的值进行记录 static TreeNode inOrderTraverse(TreeNode root){ TreeNode head=new TreeNode(-1); //这是创建的辅助对象!!! TreeNode prenode=new TreeNode(-1);//利用prenode.left记录当前结点的前一个结点 TreeNode lastnode=new TreeNode(-10); //leftTag为0表示指向左孩子,为1表示指向先驱 head.leftTag=0;head.rightTag=1; head.right=head;//头节点的右指针回指 if(root==null){ head.left=head;//树为空,头节点的左指针回指 }else{ head.left=root; prenode.left=head;//pre记录前一个结点 inThreading(root,prenode,lastnode);//中序遍历进行线索化 //利用lastnode的左孩子记录遍历二叉树的最后一个结点,这是因为java不能传递地址,所以出此下策 lastnode.left.right=head; lastnode.left.rightTag=1; head.right=lastnode.left; } return head; } //中序遍历遍历二叉树,同时对其进行线索化 static void inThreading(TreeNode p,TreeNode prenode,TreeNode lastnode){ if(p!=null){ inThreading(p.left,prenode,lastnode); if(p.left==null){//发现是叶子结点,将其左指针指向它前一个结点,作为"线索" p.leftTag=1;//线索,指向前驱 p.left=prenode.left; } if(prenode.left.right==null){//前一个结点是叶子结点,将其右指针指向它后一个结点,作为“线索” prenode.left.rightTag=1;//线索,指向后继 prenode.left.right=p; } prenode.left=p;//记录前一个结点 lastnode.left=prenode.left;//lastnode.left作用就是记录最后一个结点,在本函数中没有实质用处 inThreading(p.right,prenode,lastnode); } } //按层创建二叉树 static TreeNode createTree(int[] array){ TreeNode root=new TreeNode(array[0]); List<TreeNode> contain=new ArrayList<TreeNode>(); contain.add(root); TreeNode temp=null; int i=0,len=array.length; for(i=1;i<len;){ temp=contain.remove(0); if(i<len){ temp.left=new TreeNode(array[i]); i++; contain.add(temp.left); } if(i<len){ temp.right=new TreeNode(array[i]); i++; contain.add(temp.right); } } return root; } }
最后的输出结果如下:
叶结点 8 的前驱结点是 -1 后继结点是 4
------------------------------------------结点 4 的leftTag是 0 rightTag是 0
叶结点 9 的前驱结点是 4 后继结点是 2
------------------------------------------结点 2 的leftTag是 0 rightTag是 0
叶结点 5 的前驱结点是 2 后继结点是 1
------------------------------------------结点 1 的leftTag是 0 rightTag是 0
叶结点 6 的前驱结点是 1 后继结点是 3
------------------------------------------结点 3 的leftTag是 0 rightTag是 0
叶结点 7 的前驱结点是 3 后继结点是 -1
------------------------------------------结点 -1 的leftTag是 0 rightTag是 1
叶结点 7 的前驱结点是 3 后继结点是 -1
从结果可知,对于给定的叶子结点,通过线索二叉树寻找其前驱和后继结点是十分方便的。相对于全树遍历而言,具有线索的二叉树查找前驱结点和后继结点的不需要额外的辅助空间,同时,时间复杂度也很低。