- 线索二叉树相关由来和总结
- 中序线索二叉树构造(带头结点)
两点由来:
- 空间的浪费 : 在使用二叉链表的存储结构的过程中,会存在大量的空指针域,为了充分利用这些空指针域,引申出了“线索二叉树”。对于一个有
n
个节点的二叉链表,每个节点有指向左右节点的2
个指针域,整个二叉链表存在2n
个指针域。而n
个节点的二叉链表有n-1
条分支线,那么空指针域的个数=2n-(n-1) = n+1
个空指针域,从存储空间的角度来看,这n+1
个空指针域浪费了内存资源。 - 想知道前驱和后继:如果我们想知道按中序方式遍历二叉链表时某一个节点的前驱节点或者后继节点时,必须要按中序方式遍历二叉链表才能够知道结果,每次需要结果时都需要进行一次遍历,所以可以考虑提前存储这种前驱和后继的关系来提高时间效率。
综合上面两点分析,我们可以通过充分利用二叉链表中的空指针域,存放节点在某种遍历方式下的前驱和后继节点的指针。我们把这种指向前驱和后继的指针称为线索(Thread
),加上线索的二叉链表成为线索链表,对应的二叉树就成为“线索二叉树(Threaded Binary Tree
)”。
- 我们按照上面的规定,若一个结点有左子树,则其
lchild
域指示其左孩子,否则令lchild
域指示其前驱;若结点有右子树,则其rchild
域指示其右孩子,否则令rchild
域指示其后继。 - 为了避免混淆,需要改变普通的二叉树结构,增加两个标记域
ltag
,rtag
,当ltag
为true
时,表示左边是前驱,为false
时,表示是左儿子;同理,当rtag
为true
时,表示右边是后驱,为false
时,表示是右儿子;
以这种结点结构构成的二叉链表作为二叉树的存储结构,叫做线索链表,其中指向结点前驱和后继的指针,叫做线索,加上线索的二叉树成为线索二叉树。例如下面的树进行线索化:
这里给出结点结构:
public class BiThrTree<E>{
private static final boolean LINK = false; //指针
private static final boolean THREAD = true; //线索
private class Node{
public E e;
public Node left,right;
public boolean ltag; //false表示left指向左孩子 true表示left指向前驱
public boolean rtag; //false表示right指向右孩子 true表示right指向后继
public Node(E e, Node left, Node right, boolean ltag, boolean rtag) {
this.e = e;
this.left = left;
this.right = right;
this.ltag = ltag;
this.rtag = rtag;
}
public Node(E e) {
this(e,null,null,LINK,LINK); //默认指向左右孩子
}
public Node() {
this(null,null,null,LINK,LINK); //默认指向左右孩子
}
}
private Node dummyHead; //虚拟头结点
private Node root; //根结点
private Node pre; //pre指针 指向前一个结点
}
我们这里先讨论中序线索二叉树,看下图的构建过程,实现为指向左右儿子,虚线为线索(指向前驱和后继),对二叉树以某种次序遍历使得其变为线索二叉树的过程叫做线索化。
遍历的方法 :
- 寻找一个后继的方法是:若其右标志是
true(THREAD)
,则右边为线索,指示其后继;否则,遍历右子树最先访问的结点(也就是右子树最左下的结点); - 同理,所以寻找一个前驱的方法是:若其左标志是
true
,则左边为线索,指示其前驱;否则,遍历左子树最后访问的结点(也就是左子树最右下的结点)。
然后就是注意设置双向链表头结点的问题:
线索化的过程:
实现代码如下,其中测试用例就是用的上面图的例子,建树使用下标对应,不懂可以看看这篇博客:
import java.util.LinkedList;
import java.util.Queue;
public class BiThrTree<E>{
private static final boolean LINK = false; //指针
private static final boolean THREAD = true; //线索
private class Node{
public E e;
public Node left,right;
public boolean ltag; //false表示left指向左孩子 true表示left指向前驱
public boolean rtag; //false表示right指向右孩子 true表示right指向后继
public Node(E e, Node left, Node right, boolean ltag, boolean rtag) {
this.e = e;
this.left = left;
this.right = right;
this.ltag = ltag;
this.rtag = rtag;
}
public Node(E e) {
this(e,null,null,LINK,LINK); //默认指向左右孩子
}
public Node() {
this(null,null,null,LINK,LINK); //默认指向左右孩子
}
}
private Node dummyHead; //虚拟头结点
private Node root; //根结点
private Node pre; //pre指针 指向前一个结点
public BiThrTree(E[] arr){
this.dummyHead = null;
this.pre = null;
this.root = createTree(arr, 0); // 创建二叉树,返回创建的结点作为自己的根
}
public Node createTree(E arr[],int i) {
if(i >= arr.length || arr[i] == null)
return null;
Node T = new Node(arr[i]);
T.left = createTree(arr,2*i + 1);
T.right = createTree(arr,2*i + 2);
return T;
}
/** 中序线索化二叉树 */
public void inThreading(){
initPreNode();
}
//先初始化pre结点 并开始线索化
public void initPreNode(){
dummyHead = new Node(null); //一开始是虚拟的头结点
dummyHead.ltag = LINK; //左边是连着 root
dummyHead.rtag = THREAD; //右边当做线索
dummyHead.right = dummyHead; //右指针指向自己
if(root == null) //根节点为null 就自己指向自己吧 (此时就只有一个虚拟的头结点,啥也没有)
dummyHead.left = dummyHead;
else {
dummyHead.left = root; //蓝色矩形的(1)
pre = dummyHead; //初始化第一个pre 为dummyHead
/**此时pre结点已经初始化完毕,可以开始线索化了*/
process(root); //开始 中序线索化
/**此时差不多 已经线索化完毕了, 只需要 连成一个双向的链表 ,后续的一些小的操作*/
pre.right = dummyHead; //蓝色矩形的 (3)
pre.rtag = THREAD;
dummyHead.right = pre; // 虚拟头结点的 right = pre 也就是蓝色矩形的(4)
}
}
//递归线索化
public void process(Node node){
if(node == null)
return;
process(node.left); //中序 先左
/**此时已经来到 这次中序的第一个结点, 开始改*/
//改自己的前驱
if(node.left == null){ // no left child
node.ltag = THREAD;
node.left = pre; // as a thread --> 左孩子指向前序
}
//同时改pre的后继 --> 指向当前node
if(pre.right == null){
pre.rtag = THREAD;
pre.right = node;
}
pre = node; // 继续下一个
process(node.right); //递归右子树
}
// 通过 线索化 来 中序遍历
public void inOrderByThread(){
Node cur = root; //从根节点开始
while(cur != dummyHead){ //转一圈
while(cur.ltag == LINK) //如果 LINK就一直往左走 (找到最左下角的开始)
cur = cur.left;
//访问这个结点
System.out.print(cur.e + " ");
while(cur.rtag == THREAD && cur.right != dummyHead){ //说明有后继,而且不是最后一个
cur = cur.right; //指向后继
System.out.print(cur.e + " ");
}
cur = cur.right;
}
System.out.println();
}
public void leverOrder(){
if(root == null)
return;
Queue<Node> que = new LinkedList<>();
que.add(root);
while(!que.isEmpty()){
Node cur = que.poll();
System.out.print(cur.e + " ");
if(cur.left != null)
que.add(cur.left);
if(cur.right != null)
que.add(cur.right);
}
System.out.println();
}
public void inOrder(){
inOrder(root);
}
//递归中序遍历
public void inOrder(Node node ){
if(node == null)
return;
inOrder(node.left);
System.out.print(node.e + " ");
inOrder(node.right);
}
public static void main(String[] args) {
Character[] arrStr = {'-','+','/','a','*','e','f',
null,null,'b','-',null,null,null,null, //第四层
//最后一层
null,null,null,null,null,null,'c','d',
null,null,null,null, null,null,null,null,
};
BiThrTree<Character> biThrTree = new BiThrTree(arrStr);
//test
System.out.println("-------层序-------");
biThrTree.leverOrder();
System.out.println("-------递归中序-------");
biThrTree.inOrder();
System.out.println();
biThrTree.inThreading(); // 线索化
System.out.println("-------线索化中序-------");
// biThrTree.inOrder(); //注意此时不能递归中序遍历了,因为已经线索化了 更改了结构
biThrTree.inOrderByThread(); //线索中序遍历
}
}
运行结果:
另外:
没有设置头结点的前,中,后续二叉线索树构造 可以看看这个博客。