数据结构之树(三)
原文:https://blog.csdn.net/UncleMing5371/article/details/54176252
线索二叉树
二叉树可以使用两种存储结构:顺序存储和二叉链表。在使用二叉链表的存储结构的过程中,会存在大量的空指针域,为了充分利用这些空指针域,引申出了“线索二叉树”。
通过观察上面的二叉链表,存在着若干个没有指向的空指针域。对于一个有n个节点的二叉链表,每个节点有指向左右节点的2个指针域,整个二叉链表存在2n个指针域。而n个节点的二叉链表有n-1条分支线,那么空指针域的个数=2n-(n-1) = n+1个空指针域,从存储空间的角度来看,这n+1个空指针域浪费了内存资源。
从另外一个角度来分析,如果我们想知道按中序方式遍历二叉链表时B节点的前驱节点或者后继节点时,必须要按中序方式遍历二叉链表才能够知道结果,每次需要结果时都需要进行一次遍历,是否可以考虑提前存储这种前驱和后继的关系来提高时间效率呢?
综合以上两方面的分析,可以通过充分利用二叉链表中的空指针域,存放节点在某种遍历方式下的前驱和后继节点的指针。我们把这种指向前驱和后继的指针成为线索,加上线索的二叉链表成为线索链表,对应的二叉树就成为“线索二叉树(Threaded Binary Tree)”
构建线索二叉树过程
1、我们对二叉树进行中序遍历,将所有的节点右子节点为空的指针域指向它的后继节点。如下图:
通过中序遍历我们知道H的right指针为空,并且H的后继节点为D(如上图第1步),I的right指针为空,并且I的后继节点为B(如上图第2步),以此类推,知道G的后继节点为null,则G的right指针指向null。
2、接下来将这颗二叉树的所有节点左指针域为空的指针域指向它的前驱节点。如下图:
如上图,H的left指针域指向Null(如第1步),I的前驱节点是D,则I的left指针指向D,以此类推。
通过上面两步完成了整个二叉树的线索化,最后结果如下图:
通过观察上图(蓝色虚线代表后继、绿色虚线代表前驱),可以看出,线索二叉树,等于是把一棵二叉树转变成了一个“特殊的双向链表“(后面会解释为什么叫特殊的双向链表),这样对于我们的新增、删除、查找节点带来了方便。所以我们对二叉树以某种次序遍历使其变为线索二叉树的过程称做是线索化。如下图:
仔细分析上面的双向链表,与线索化之后的二叉树相比,比如节点D与后继节点I,在完成线索化之后,并没有直接线索指针,而是存在父子节点的指针;节点A与节点F,在线索化完成之后,节点A并没有直接指向后继节点F的线索指针,而是通过父子节点遍历可以找到最终的节点F,前驱节点也存在同样的问题,正因为很多节点之间不存在直接的线索,所以我将此双向链表称做“特殊的双向链表”,再使用过程中根据指针是线索指针还是子节点指针来分别处理,所以在每个节点需要标明当前的左右指针是线索指针还是子节点指针,这就需要修改节点的数据结构。修改后的数据结构如下:
class Node {
String data; //数据域
Node left; //左指针域
Node right; //右指针域
byte leftType; //左指针域类型 0:指向子节点、1:前驱或后继线索
byte rightType; //右指针域类型 0:指向子节点、1:前驱或后继线索
}
最终的二叉链表修改为如下图的样子:
线索二叉树的代码
public class ThreadBinaryTree {
private Node preNode; //线索化时记录前一个节点
//节点存储结构
static class Node {
String data; //数据域
Node left; //左指针域
Node right; //右指针域
boolean isLeftThread = false; //左指针域类型 false:指向子节点、true:前驱或后继线索
boolean isRightThread = false; //右指针域类型 false:指向子节点、true:前驱或后继线索
Node(String data) {
this.data = data;
}
}
/**
* 通过数组构造一个二叉树(完全二叉树)
* @param array
* @param index
* @return
*/
static Node createBinaryTree(String[] array, int index) {
Node node = null;
if(index < array.length) {
node = new Node(array[index]);
node.left = createBinaryTree(array, index * 2 + 1);
node.right = createBinaryTree(array, index * 2 + 2);
}
return node;
}
/**
* 中序线索化二叉树
* @param node 节点
*/
void inThreadOrder(Node node) {
if(node == null) {
return;
}
//处理左子树
inThreadOrder(node.left);
//左指针为空,将左指针指向前驱节点
if(node.left == null) {
node.left = preNode;
node.isLeftThread = true;
}
//前一个节点的后继节点指向当前节点
if(preNode != null && preNode.right == null) {
preNode.right = node;
preNode.isRightThread = true;
}
preNode = node;
//处理右子树
inThreadOrder(node.right);
}
/**
* 中序遍历线索二叉树,按照后继方式遍历(思路:找到最左子节点开始)
* @param node
*/
void inThreadList(Node node) {
//1、找中序遍历方式开始的节点
while(node != null && !node.isLeftThread) {
node = node.left;
}
while(node != null) {
System.out.print(node.data + ", ");
//如果右指针是线索
if(node.isRightThread) {
node = node.right;
} else { //如果右指针不是线索,找到右子树开始的节点
node = node.right;
while(node != null && !node.isLeftThread) {
node = node.left;
}
}
}
}
/**
* 中序遍历线索二叉树,按照前驱方式遍历(思路:找到最右子节点开始倒序遍历)
* @param node
*/
void inPreThreadList(Node node) {
//1、找最后一个节点
while(node.right != null && !node.isRightThread) {
node = node.right;
}
while(node != null) {
System.out.print(node.data + ", ");
//如果左指针是线索
if(node.isLeftThread) {
node = node.left;
} else { //如果左指针不是线索,找到左子树开始的节点
node = node.left;
while(node.right != null && !node.isRightThread) {
node = node.right;
}
}
}
}
/**
* 前序线索化二叉树
* @param node
*/
void preThreadOrder(Node node) {
if(node == null) {
return;
}
//左指针为空,将左指针指向前驱节点
if(node.left == null) {
node.left = preNode;
node.isLeftThread = true;
}
//前一个节点的后继节点指向当前节点
if(preNode != null && preNode.right == null) {
preNode.right = node;
preNode.isRightThread = true;
}
preNode = node;
//处理左子树
if(!node.isLeftThread) {
preThreadOrder(node.left);
}
//处理右子树
if(!node.isRightThread) {
preThreadOrder(node.right);
}
}
/**
* 前序遍历线索二叉树(按照后继线索遍历)
* @param node
*/
void preThreadList(Node node) {
while(node != null) {
while(!node.isLeftThread) {
System.out.print(node.data + ", ");
node = node.left;
}
System.out.print(node.data + ", ");
node = node.right;
}
}
public static void main(String[] args) {
String[] array = {"A", "B", "C", "D", "E", "F", "G", "H"};
Node root = createBinaryTree(array, 0);
ThreadBinaryTree tree = new ThreadBinaryTree();
tree.inThreadOrder(root);
System.out.println("中序按后继节点遍历线索二叉树结果:");
tree.inThreadList(root);
System.out.println("\n中序按后继节点遍历线索二叉树结果:");
tree.inPreThreadList(root);
Node root2 = createBinaryTree(array, 0);
ThreadBinaryTree tree2 = new ThreadBinaryTree();
tree2.preThreadOrder(root2);
tree2.preNode = null;
System.out.println("\n前序按后继节点遍历线索二叉树结果:");
tree.preThreadList(root2);
}
}
1.线索化的实质就是将二叉链表中的空指针改为指向前驱节点或后继节点的线索;
2.线索化的过程就是修改二叉链表中空指针的过程,可以按照前序、中序、后序的方式进行遍历,分别生成不同的线索二叉树;
3.有了线索二叉树之后,我们再次遍历时,就相当于操作一个双向链表。
4.使用场景:如果我们在使用二叉树过程中经常需要遍历二叉树或者查找节点的前驱节点和后继节点,可以考虑采用线索二叉树存储结构。
前面介绍了前序线索化二叉树、中序线索化二叉树,后面将介绍后序线索化二叉树。
图解后序线索化
下图是一棵后序线索化的二叉树,如下图:
为了更清晰、直观的表示出后继线索,在上图中忽略了前驱线索,请自行脑补。通过观察上图,节点H的后继节点是I,因此节点H的right指针指向I;节点I的后继节点是D,因此节点D的right指针指向D;节点D的后继节点是E,但是节点D的right指针指向了子节点B,因此D的right指针也就不能指向后继节点;同理节点B也没办法指向后继节点F。
对这棵二叉树完成后序线索化之后,我们在对其进行遍历时,我们知道后序遍历的顺序是:左右根,那对于上图的后序遍历结果是:HIDEBFGCA。
遍历后序线索化二叉树的思路:由于是后序线索化,那么后序遍历的开始节点一定是最左子节点,从根节点出发找到最左子节点,如何判断是否是最左子节点呢?如果是最左子节点,则其left指针一定的线索,如上图我们找到最左子节点H,H的right指针是后继线索,找到节点I,节点I的right指针是后继线索,找到节点D,节点D的right指针是子节点I,并不是后继线索指针,那么问题来了?此时我们该如何处理呢?
通过观察D的后继节点E,但是D与E没有直接线索,不过D的父节点是B,B的右字节是E,存在这样一个间接的关系,我们是否可以利用这个间接的关系呢?答案是肯定的,但是按照我们上文介绍的节点数据结构,并不存在指向父节点的指针,因此我们要对节点数据结构进行修改,修改如下:
//节点存储结构
static class Node {
String data; //数据域
Node left; //左指针域
Node right; //右指针域
Node parent; //父节点的指针(为了后序线索化使用)
boolean isLeftThread = false; //左指针域类型 false:指向子节点、true:前驱或后继线索
boolean isRightThread = false; //右指针域类型 false:指向子节点、true:前驱或后继线索
Node(String data) {
this.data = data;
}
}
按照如上的存储结构增加了parent指针之后,D节点存在了指向父节点B的指针。当遍历到D节点时找到D节点的父节点B,B的right指针指向了子节点E,E的right指针又指向了B,这里又出现了另一个问题,就是进入了两次B,如果按照前面的方式则进入了一个死循环。以节点B为例,我们什么时候去找B的父节点,什么时候去处理他的右节点呢。我们分析下两次进入节点B,第一次是通过B的左节点进入,第二次是通过右子节点进入,我们可以记录上一个处理的节点,如果上一个处理的节点是B的左节点,则接下进入B的右节点,如果上一个处理的节点是B的右节点,则说明B的左右子树都处理完成,继续处理B的父节点。
代码实现后序线索化
public class PostThreadBinaryTree {
private Node preNode; //线索化时记录前一个节点
//节点存储结构
static class Node {
String data; //数据域
Node left; //左指针域
Node right; //右指针域
Node parent; //父节点的指针(为了后序线索化使用)
boolean isLeftThread = false; //左指针域类型 false:指向子节点、true:前驱或后继线索
boolean isRightThread = false; //右指针域类型 false:指向子节点、true:前驱或后继线索
Node(String data) {
this.data = data;
}
}
/**
* 通过数组构造一个二叉树(完全二叉树)
* @param array
* @param index
* @return
*/
static Node createBinaryTree(String[] array, int index) {
Node node = null;
if(index < array.length) {
node = new Node(array[index]);
node.left = createBinaryTree(array, index * 2 + 1);
node.right = createBinaryTree(array, index * 2 + 2);
//记录节点的父节点(后序线索化遍历时使用)
if(node.left != null) {
node.left.parent = node;
}
if(node.right != null) {
node.right.parent = node;
}
}
return node;
}
/**
* 后序线索化二叉树
* @param node 节点
*/
void postThreadOrder(Node node) {
if(node == null) {
return;
}
//处理左子树
postThreadOrder(node.left);
//处理右子树
postThreadOrder(node.right);
//左指针为空,将左指针指向前驱节点
if(node.left == null) {
node.left = preNode;
node.isLeftThread = true;
}
//前一个节点的后继节点指向当前节点
if(preNode != null && preNode.right == null) {
preNode.right = node;
preNode.isRightThread = true;
}
preNode = node;
}
/**
* 后续遍历线索二叉树,按照后继方式遍历(思路:后序遍历开始节点是最左节点)
* @param node
*/
void postThreadList(Node root) {
//1、找后序遍历方式开始的节点
Node node = root;
while(node != null && !node.isLeftThread) {
node = node.left;
}
Node preNode = null;
while(node != null) {
//右节点是线索
if(node.isRightThread) {
System.out.print(node.data + ", ");
preNode = node;
node = node.right;
} else {
//如果上个处理的节点是当前节点的右节点
if(node.right == preNode) {
System.out.print(node.data + ", ");
if(node == root) {
return;
}
preNode = node;
node = node.parent;
} else { //如果从左节点的进入则找到有子树的最左节点
node = node.right;
while(node != null && !node.isLeftThread) {
node = node.left;
}
}
}
}
}
public static void main(String[] args) {
String[] array = {"A", "B", "C", "D", "E", "F", "G", "H", "I"};
Node root = createBinaryTree(array, 0);
PostThreadBinaryTree tree = new PostThreadBinaryTree();
tree.postThreadOrder(root);
System.out.println("后序按后继节点遍历线索二叉树结果:");
tree.postThreadList(root);
}
}
运行结果如下:
后序按后继节点遍历线索二叉树结果:
H, I, D, E, B, F, G, C, A,
前序、中序、后序线索化比较
1. 前序线索化二叉树遍历相对最容易理解,实现起来也比较简单。由于前序遍历的顺序是:根左右,所以从根节点开始,沿着左子树进行处理,当子节点的left指针类型是线索时,说明到了最左子节点,然后处理子节点的right指针指向的节点,可能是右子树,也可能是后继节点,无论是哪种类型继续按照上面的方式(先沿着左子树处理,找到子树的最左子节点,然后处理right指针指向),以此类推,直到节点的right指针为空,说明是最后一个,遍历完成。
2. 中序线索化二叉树的网上相关介绍最多。中序遍历的顺序是:左根右,因此第一个节点一定是最左子节点,先找到最左子节点,依次沿着right指针指向进行处理(无论是指向子节点还是指向后继节点),直到节点的right指针为空,说明是最后一个,遍历完成。
3. 后序遍历线索化二叉树最为复杂,通用的二叉树数节点存储结构不能够满足后序线索化,因此我们扩展了节点的数据结构,增加了父节点的指针。后序的遍历顺序是:左右根,先找到最左子节点,沿着right后继指针处理,当right不是后继指针时,并且上一个处理节点是当前节点的右节点,则处理当前节点的右子树,遍历终止条件是:当前节点是root节点,并且上一个处理的节点是root的right节点。