1. 基本概念
在链式存储中,发现二叉链表中存在大量的空指针,如果利用这些空指针指向其直接前驱或后继的指针,则可以更方便地运用某些二叉树操作算法。二叉树的线索化,是为了加快查找结点前驱和后继的速度。
在有N个结点的二叉树中,存在N+1个空指针。每个叶结点有2个空指针,度为1的结点有1个空指针,总的空指针为2N0+N1,又有N0=N2+1,所以总的空指针为N0+N1+N2+1=N+1。
二叉树线索化规则:若无左子树,令lchild指向其前驱结点;若无右子树,令rchild指向其后继结点。如图,增加两个标志域,用来表明当前指针指向左右结点还是前驱后继结点。
其中标志位含义为:
线索二叉树的结点:
private class Node{ int data; Node lchild, rchild; int ltag = 0, rtag = 0;}
以这种结点构成的二叉链表作为二叉树的存储结构,叫做线索链表,其中指向前驱和后继的指针,叫做线索,加上线索的二叉树叫做线索二叉树,对二叉树以某种次序遍历使其变为线索二叉树的过程叫做线索化。
2. 二叉树的构造及线索化
构造:直接使用完全二叉树序列递归创建二叉树,不存在的使用0,这里不再赘述。直接给出代码。
public class ThreadTree { private Integer[] nodes; // 存储完全二叉树序列 private int n; // 树结点数 Node root; // 根结点 Node pre; // 前一个访问的结点 private class Node{ int data; Node lchild, rchild; int ltag = 0, rtag = 0; } public ThreadTree(){ System.out.println("输入一个完全二叉树序列,不存在的结点用0代替,使用逗号隔开:");// String[] ins = StdIn.readString().split(","); String[] ins = "1,2,3,0,4,0,5".split(","); nodes = new Integer[ins.length]; for (int i = 0; i < ins.length; i++) { nodes[i] = Integer.valueOf(ins[i]); } n = ins.length; root = build(1); TreeUtil.print(depth(root), n, nodes); } /** * 递归创建一棵二叉树 ** 使用完全二叉树序列 */ public Node build(int index){ if(index > n) { return null; } if(nodes[index-1]==0){ return null; } Node node = new Node(); node.data = nodes[index-1]; node.lchild = build(2 * index); node.rchild = build(2 * index + 1); return node; }}
线索化一个二叉树其实就是遍历一次二叉树,遍历过程中,检查当前结点左右指针是否为空,若为空,将它们改为指向前驱或后继结点。
以中序遍历为例,那么结点的前驱和后继的结点就是二叉树中序遍历序列中的前后结点。
算法描述:node指向当前结点,pre指向前一个访问的结点
(1)若node的左孩子为空,则修改左指针指向pre,置ltag为1
(2)若pre不为空,且不存在右孩子,则修改右指针指向node,置rtag为1
(3)使pre指向刚刚访问过的结点node,即pre = node
public void inThreaded(){ inThreaded(root); pre.rchild = null; // 单独处理下最后一个结点 pre.rtag = 1;}/** * 中序线索化一个二叉树 */public void inThreaded(Node node){ if (node != null) { inThreaded(node.lchild); // 线索化左子树,找到左侧一个没有左孩子的结点 if(node.lchild == null){ // 左孩子为空 node.lchild = pre; // 修改指向其前驱结点 node.ltag = 1; // 修改标志位 } if(pre != null && pre.rchild == null){ // 如果前一个访问的结点不为空,且没有右孩子 pre.rchild = node; // 修改指向其后继结点 pre.rtag = 1; // 修改标志位 } pre = node; inThreaded(node.rchild); // 线索化右子树 }}
3. 线索化的遍历
中序线索化二叉树主要目的就是加快访问前驱和后继的速度,这种遍历就不需要借助栈,因为结点中隐含了前驱和后继结点的信息。不含头结点的线索二叉树遍历算法如下:
(1)首先找到中序线索二叉树中的第一个结点,左侧没有左孩子的结点,不一定是叶结点,根据ltag标志为判断
(2)找结点的后继结点,根据rtag判断
/** * 中序线索二叉树遍历,非递归 * @param node */public void inOrder(Node node){ Node tmp = firstNode(node); while(tmp != null){ System.out.print(tmp.data+" "); tmp = nextNode(tmp); }}/** * 求第一个结点 * @param node * @return */public Node firstNode(Node node){ while(node.ltag == 0){ // 最左下结点,不一定是叶子结点 node = node.lchild; } return node;}/** * 求后继结点 * @param node * @return */public Node nextNode(Node node){ if(node.rtag == 0){ // 存在右孩子 return firstNode(node.rchild); // 以此结点为根找最左下结点 } return node.rchild; // rtag = 1 直接返回后继}/** * 倒序非递归遍历 * @param node */public void inOrderO(Node node){ Node tmp = lastNode(node); while(tmp != null){ System.out.print(tmp.data+" "); tmp = preNode(tmp); }}/** * 求最后一个结点 * @return */public Node lastNode(Node node){ while(node.rtag == 0){ node = node.rchild; } return node;}/** * 求前驱结点 * @return */public Node preNode(Node node){ if(node.ltag == 0){ return lastNode(node.lchild); } return node.lchild;}
4. 测试
public static void main(String[] args) { ThreadTree tree = new ThreadTree(); System.out.print("二叉树的结点总数:" + tree.nodes(tree.root)); System.out.print("\n中序遍历递归:"); tree.inOrderRecur(tree.root); tree.inThreaded(); System.out.print("\n线索化...\n中序线索化二叉树遍历非递归:"); tree.inOrder(tree.root); System.out.print("\n非递归遍历(倒序):"); tree.inOrderO(tree.root);}
4.1 输出结果