K:树、二叉树与森林之间的转换及其相关代码实现
相关介绍:
二叉树是树的一种特殊形态,在二叉树中一个节点至多有左、右两个子节点,而在树中一个节点可以包含任意数目的子节点,对于森林,其是多棵树所组成的一个整体,树与树之间彼此相互独立,互不干扰,但其又是一个整体。树与二叉树之间、森林与二叉树之间可以相互的进行转换,且这种转换是一一对应的。树与森林转换成二叉树之后,森林与或树的相关操作都转换为二叉树的操作。在此,将讨论树的存储结构、树与森林,二叉树之间的对应关系与转换过程及相关代码。
二叉树的存储结构:
在实际应用中,可根据具体的操作的特点将树设计成不同的存储结构。但无论采用何种存储方式,都要求树的存储结构不但能够存储各个节点本身的数据域信息,还要求能准确反映树中各个节点之间的逻辑关系。于此,介绍四种链式存储方式:
1.双亲链表存储结构
双亲链表存储结构中的每一个节点存放的信息既包含节点本身的数据域信息,又包含指示双亲节点在存储结构中的位置。所以,这种存储结构可以设计成:以一组地址连续的存储单元来存放树中的各个节点,每一个节点中有两个域,一个为数据域,用来存储树中该节点本身的值;另一个是指针域,用来存储该节点的双亲节点在存储结构中的位置信息。下图演示了该存储结构:
分析: 采用双亲链表存储方式实现查找一个指定节点的双亲节点非常容易,但是要实现查找一个指定节点的孩子节点却并不容易,需要对整个链表扫描一遍
其双亲节点链表的代码描述如下:
相关代码:
public class Forest
{
private TreeNode[] tree;
public Forest(int n)
{
tree=new TreeNode[n];
}
class TreeNode
{
//数据域对象
Object data;
//父对象在数组中的位置
int parent;
}
}
2.孩子链表存储结构
孩子链表存储结构中除了存放节点本身的数据域信息之外,还存放了其所有孩子节点在存储结构中的位置信息。由于每一个节点的子节点树不同,则可将一个节点的所有孩子的位置信息按从左到右的顺序链接成一个单链表,称此单链表为该链表的孩子链表,因此,该存储结构可设计为:以一组地址连续的存储单元来存放树中的各个节点,,每一个节点有两个域,一个为数据域,用来存储树中该节点的值;另一个为指针域,用来存放该节点的孩子链表的头指针。下图演示了该存储结构:
分析:
这种存储结构与双亲链表存储结构正好相反,它便于实现查找树中指定节点的孩子节点,但不便于实现查找树中指定节点的双亲节点。
其孩子链表存储结构的示例代码如下:
相关代码:
package all_in_tree;
import java.util.LinkedList;
import java.util.List;
public class Forest
{
private TreeNode[] tree;
public Forest(int n)
{
tree=new TreeNode[n];
}
class TreeNode
{
//数据域对象
Object data;
//孩子链表的指针
List firstChild=new LinkedList();
}
}
3.双亲孩子链表存储结构
双亲孩子链表存储结构的设计方法与孩子链表存储结构类似,其主体仍然是一个存储树中各个节点信息的数组。只不过数组中的元素含有三个域,比孩子链表存储结构中多了一个存放该节点的双亲节点在数组中位置的指针域。下图演示了该存储结构:
分析:
这种存储结构既便于实现查找树中指定节点的孩子节点,又便于实现查找树中指定节点的双亲节点。
其双亲孩子链表存储结构的示例代码如下:
相关代码:
package all_in_tree;
import java.util.LinkedList;
import java.util.List;
public class Forest
{
private TreeNode[] tree;
public Forest(int n)
{
tree=new TreeNode[n];
}
class TreeNode
{
//数据域对象
Object data;
//双亲节点在数组中的位置
int parent;
//孩子链表的指针
List firstChild=new LinkedList();
}
}
4.孩子兄弟链表存储结构
孩子兄弟链表存储结构又称为“左孩/右兄”二叉链式存储结构,它类似于二叉树的二叉链式存储结构,不同点在于链表中每个节点的左指针是指向该节点的第一个孩子,而右指针是指向该节点的右邻兄弟。其本质就是先将一棵二叉树转化为一棵二叉树后存储在二叉链式结构之中。下图演示了该存储结构:
分析:
这种存储结构与树所对应的二叉树的二叉链式存储结构相同,一切对于树的操作都将通过这种方式转化成对二叉树的操作。为此,该种存储方式应用更加广泛
其孩子兄弟链表存储结构的示例代码如下:
相关代码:
package all_in_tree;
public class Forest
{
private TreeNode root;
class TreeNode
{
//数据域对象
Object data;
//其左孩子,右兄弟的指针
TreeNode left,right;
}
}
树转换为二叉树:
二叉树中的节点有左右孩子之分,而在无序树中节点的各个孩子之间是无次序之分的。为了操作方便,假设树是一棵有序树,树中每个节点的孩子按照从左到右的顺序进行编号,依次定义为第一个孩子、第二个孩子、....、第i个孩子。
将树转换为二叉树的方法可以归纳为“加线”、“删除”、”旋转“3个步骤,其具体描述如下:
-
加线:将树中所有相邻的兄弟之间加一条连线。
-
删线:对树中的每一个节点,只保留它与第一个孩子节点之间的连线,删去它与其他孩子节点之间的连线。
-
旋转:以树的根节点为轴心,将树平面顺时针旋转一定的角度并适当的进行调整,使得转化后所得的二叉树看起来比较规整(该步骤不是必须)
下图给出了将树装换成二叉树的过程的示意图:
由转换过程可知,树与由它转换成的二叉树是一一对应的,树中的任意一个节点都对应着二叉树中的一个节点,树中每一个节点的第一个孩子节点在二叉树中是对应节点的左孩子,而树中每一节点的右邻兄弟在二叉树中是对应节点的右孩子(简而言之,左孩子,右兄弟)。以下,相关的代码中采用孩子链表存储结构来存储相关的树。
注意到,对于任意一个节点,其孩子链表中的第一个节点的值为其当前节点的左孩子节点;孩子链表中的任意一个节点为其下一个孩子节点的双亲节点,同时,下一个孩子节点为该节点的右孩子节点。为此,我们可以采用以下的方式,实现树与二叉树之间的相互转换
相关代码:
package all_in_tree;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
/**
* 该类用于演示树转化为二叉树的相关代码,其中,树采用孩子链表的存储结构
* 同时,借助一个Map,用于记录中间转化过程,以实现后期对二叉树的构造
* @author 学徒
*
*/
public class Forest
{
//树中节点的数目
private int nodeCount;
//用于记录树中各个存储节点的指针,便于树的构造过程
private Map<Integer,Node> nodeMap=new HashMap<Integer,Node>();
//用于描述树的孩子链表的存储结构
private TreeNode[] tree;
//用于记录树的中间转化过程中的相关信息
private MapNode[] map;
//转化后的二叉树的根节点索引
private Node root;
/**
* 中间记录转化过程的节点
* @author 学徒
*
*/
class MapNode
{
//存储相关的数据
Object data;
//存储其左孩子节点在数组中的位置的下标,初始化为-1表示没有
int left=-1;
//存储其右孩子节点在数组中的位置的下标,初始化为-1表示没有
int right=-1;
}
/**
* 用于创建一棵树具有n个节点的数
* @param n 树的节点数目
*
*/
public Forest(int n)
{
//存储树的节点的数目
this.nodeCount=n;
//用于初始化
tree=new TreeNode[n];
map=new MapNode[n];
for(int i=0;i<n;i++)
{
tree[i]=new TreeNode();
map[i]=new MapNode();
nodeMap.put(i, new Node());
}
root=(Node)nodeMap.get(0);
}
/**
* 用于设置树中某个节点的数据域的相关值
* @param index 树中节点在数组中的下标
* @param data 树的数据域
*/
public void setTreeData(int index,Object data)
{
tree[index].data=data;
map[index].data=data;
nodeMap.get(index).data=data;
}
/**
* 用于增加树中某个节点的孩子节点
* @param index
* @param childIndex
*/
public void addTreeChild(int index,int childIndex)
{
if(tree[index].firstChild==null)
tree[index].firstChild=new LinkedList<Integer>();
tree[index].firstChild.add(childIndex);
}
/**
* 用于实现树转化为二叉树的过程,并返回该二叉树
*
*/
public Node toTranslateBinaryTree()
{
//用于遍历树的双亲孩子节点存储结构的数组,同时修改和记录其中间结果的值,便于后序二叉树的构造
for(int i=0 ;i<this.nodeCount;i++)
{
//孩子节点的链表
Queue<Integer> childList=tree[i].firstChild;
//当前孩子节点的下标编号
int now=-1;
//上一孩子节点的下标编号,将其默认值设置为当前节点
int previous=i;
//当其孩子链表存在的时候
if(childList!=null)
{
//当孩子链表存在时,由于第一个节点为其当前节点的左孩子节点。为此,用于设置其左孩子节点
if(!childList.isEmpty())
{
now=childList.poll();
map[previous].left=now;
previous=now;
}
//处理剩下的孩子节点
while(!childList.isEmpty())
{
now=childList.poll();
map[previous].right=now;
previous=now;
}
}
}
//根据记录的中间结果构造出一棵二叉树
for(int i=0;i<this.nodeCount;i++)
{
int left=map[i].left;
int right=map[i].right;
//用于获取二叉树中相关的节点
Node node=nodeMap.get(i);
//用于构造该二叉树
if(left!=-1)
node.left=nodeMap.get(left);
if(right!=-1)
node.right=nodeMap.get(right);
}
root=nodeMap.get(0);
return root;
}
}
/**
* 用于存储树中双亲孩子节点的信息
* @author 学徒
*
*/
class TreeNode
{
//存储相关数据
Object data;
/* //其双亲节点在数组中位置的下标
int parent;*/
//用于存储孩子节点的孩子节点链表
Queue<Integer> firstChild;
/**
* 用于增加孩子节点的方法
*/
public void addChild(int child)
{
firstChild.add(child);
}
}
/**
* 二叉树中的节点描述类
* @author 学徒
*/
class Node
{
Object data;
Node left;
Node right;
}
/**
* 用于测试使用的类,该代码采用的测试用例即为“树转换成二叉树的过程示意图”中的树
* @author 学徒
*
*/
class Test
{
public static void main(String[] args)
{
Forest forest=new Forest(8);
forest.setTreeData(0,"A");
forest.setTreeData(1,"B");
forest.setTreeData(2, "C");
forest.setTreeData(3,"D");
forest.setTreeData(4,"E");
forest.setTreeData(5,"F");
forest.setTreeData(6,"G");
forest.setTreeData(7, "H");
forest.addTreeChild(0, 1);
forest.addTreeChild(0, 2);
forest.addTreeChild(0, 3);
forest.addTreeChild(1, 4);
forest.addTreeChild(1, 5);
forest.addTreeChild(5, 7);
forest.addTreeChild(3, 6);
Node root=forest.toTranslateBinaryTree();
//得到二叉树中序遍历的结果
System.out.print("中序遍历的结果 :");
inRootTraver(root);
System.out.println();
//得到二叉树后序遍历的结果
System.out.print("后序遍历的结果 :");
postRootTraver(root);
}
/**
* 用于得到二叉树中序遍历的结果
*/
public static void inRootTraver(Node root)
{
if(root!=null)
{
inRootTraver(root.left);
System.out.print(root.data+" ");
inRootTraver(root.right);
}
}
/**
* 用于得到二叉树后序遍历的结果(Node root)
*/
public static void postRootTraver(Node root)
{
if(root!=null)
{
postRootTraver(root.left);
postRootTraver(root.right);
System.out.print(root.data+" ");
}
}
}
其运行结果如下:
中序遍历的结果 :E H F B C G D A
后序遍历的结果 :H F E G D C B A
二叉树转换成树:
二叉树转换成树是由树转换成二叉树的一个逆过程,也就是一个由二叉树还原成它原来所对应的树的过程,其步骤如下:
-
加线:若某节点是其双亲节点的左孩子,则将该节点沿着右分支向下的所有节点与该节点的双亲节点用线连接
-
删线:将书中所有本来双亲节点与右孩子节点的连线删除
-
旋转:对经过步骤1、2两步后所得的树以根节点为轴心,按逆时针方向旋转一定的角度并做适当调整,使得转化后所得的树看起来比较规整
下图给出了将二叉树转化为树的过程:
以下,相关的代码中采用孩子链表存储结构来存储相关的树。
其相关的代码如下:
相关代码:
package all_in_tree;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Queue;
/**
* 用于实现将一棵二叉树转化为树的相关操作,其中树采用孩子链表节点的存储结构
* 同时,借助一个Map用来记录二叉树中节点和树存储结构中的数组的下标的编号的对应关系
* @author 学徒
*
*/
public class ReverseForest
{
//用于记录该树二叉树
private Node root;
//二叉树中节点的数目
private int nodeCount;
//用于记录树中各个存储节点的指针,便于树的构造过程
private Map<Integer,Node> nodeMap=new HashMap<Integer,Node>();
//用于记录树中各个指针对应的存储节点的编号,便于树的构造
private Map<Node,Integer> indexMap=new HashMap<Node,Integer>();
//用于描述树的孩子链表的存储结构
private TreeNode[] tree;
public ReverseForest(Node root)
{
this.root=root;
//对二叉树采用任意一种遍历方式,用于计算出其总节点的数目同时初始化其映射Map,此处采用层次遍历的方式
Queue<Node> q=new LinkedList<Node>();
q.add(root);
nodeMap.put(this.nodeCount,root);
indexMap.put(root,this.nodeCount++ );
while(!q.isEmpty())
{
Node node = q.poll();
if(node.left!=null)
{
q.add(node.left);
nodeMap.put( this.nodeCount,node.left);
indexMap.put(node.left,this.nodeCount++);
}
if(node.right!=null)
{
q.add(node.right);
nodeMap.put(this.nodeCount,node.right);
indexMap.put(node.right, this.nodeCount++);
}
}
//初始化其孩子链表的存储结构
tree=new TreeNode[this.nodeCount];
//用于初始化各个树中的节点
for(Entry<Integer,Node> node:nodeMap.entrySet())
{
int index=node.getKey();
tree[index]=new TreeNode();
tree[index].data=node.getValue().data;
}
}
/**
* 用于增加树中某个节点的孩子节点
* @param index
* @param childIndex
*/
public void addTreeChild(int index,int childIndex)
{
if(tree[index].firstChild==null)
tree[index].firstChild=new LinkedList<Integer>();
tree[index].firstChild.add(childIndex);
}
/**
* 用于将二叉树转化为树
*/
public TreeNode[] toTranslateTree()
{
//用于遍历描述树的数组
for(int index=0;index<this.nodeCount;index++)
{
//得到该树节点
Node node=nodeMap.get(index);
//得到该树节点所对应的第一个孩子节点
node=node.left;
if(node!=null)
{
addTreeChild(index,indexMap.get(node));
while(node.right!=null)
{
addTreeChild(index, indexMap.get(node.right));
node=node.right;
}
}
}
return this.tree;
}
}
class Tests
{
/**
* 测试代码,用于测试其运行结果
* @param args
*/
public static void main(String[] args)
{
TreeNode[] tree= new ReverseForest(Test.test()).toTranslateTree();
System.out.println();
for(int i=0;i<tree.length;i++)
{
System.out.print(i+(tree[i].data.toString())+": ");
Queue<Integer> q=tree[i].firstChild;
if(q!=null)
{
Iterator iterator =q.iterator();
while(iterator.hasNext())
{
System.out.print(iterator.next()+" ");
}
}
else
{
System.out.print("无孩子节点");
}
System.out.println();
}
}
}
/**
* 用于存储树中双亲孩子节点的信息
* @author 学徒
*
*/
class TreeNode
{
//存储相关数据
Object data;
//用于存储孩子节点的孩子节点链表
Queue<Integer> firstChild;
/**
* 用于增加孩子节点的方法
*/
public void addChild(int child)
{
firstChild.add(child);
}
}
/**
* 二叉树中的节点描述类
* @author 学徒
*/
class Node
{
Object data;
Node left;
Node right;
}
/**
* 用于测试使用的类,该代码采用的测试用例即为“树转换成二叉树的过程示意图”中的树
* @author 学徒
*
*/
class Test
{
public static Node test()
{
Forest forest=new Forest(8);
forest.setTreeData(0,"A");
forest.setTreeData(1,"B");
forest.setTreeData(2, "C");
forest.setTreeData(3,"D");
forest.setTreeData(4,"E");
forest.setTreeData(5,"F");
forest.setTreeData(6,"G");
forest.setTreeData(7, "H");
forest.addTreeChild(0, 1);
forest.addTreeChild(0, 2);
forest.addTreeChild(0, 3);
forest.addTreeChild(1, 4);
forest.addTreeChild(1, 5);
forest.addTreeChild(5, 7);
forest.addTreeChild(3, 6);
Node root=forest.toTranslateBinaryTree();
//得到二叉树中序遍历的结果
System.out.print("中序遍历的结果 :");
inRootTraver(root);
System.out.println();
//得到二叉树后序遍历的结果
System.out.print("后序遍历的结果 :");
postRootTraver(root);
return root;
}
/**
* 用于得到二叉树中序遍历的结果
*/
public static void inRootTraver(Node root)
{
if(root!=null)
{
inRootTraver(root.left);
System.out.print(root.data+" ");
inRootTraver(root.right);
}
}
/**
* 用于得到二叉树后序遍历的结果(Node root)
*/
public static void postRootTraver(Node root)
{
if(root!=null)
{
postRootTraver(root.left);
postRootTraver(root.right);
System.out.print(root.data+" ");
}
}
}
运行结果:
中序遍历的结果 :E H F B C G D A
后序遍历的结果 :H F E G D C B A
0A: 1 3 5
1B: 2 4
2E: 无孩子节点
3C: 无孩子节点
4F: 6
5D: 7
6H: 无孩子节点
7G: 无孩子节点
森林转换成二叉树:
森林是若干棵树的集合,而任何一棵和树对应的二叉树其根节点的右子树一定为空,因而可得到将森林转换成二叉树的方法如下:
- 将森林中的每棵树转换成相应的二叉树
- 按照森林中树的先后顺序,将一棵二叉树视为前一棵二叉树的右子树依次连接起来,从而构成一棵二叉树
下图给出了将森林转换为二叉树的过程:
从中可以看出,森林与二叉树之间存在着一一对应的关系。森林中第一棵树的根节点对应着二叉树的根节点;森林中第一棵树的根节点的子树所构成的森林对应着二叉树的左子树;森林中除第一棵树之外的其它树所构成的森林所对应着二叉树的右子树。
除了将每棵树转化为相应的二叉树,之后按照森林中树的先后顺序,将一棵二叉树视为前一棵二叉树的右子树依次连接起来,构成一棵二叉树的方法之外,还存在着一种增加一个虚节点,之后将森林转化成一棵树,然后再按照将树转化成二叉树的方式转化成二叉树的方法
下图给出了增加一个虚节点将森林转换成二叉树的过程:
森林转化为二叉树的代码与树转化为二叉树的代码基本相同,此处不再重复
二叉树转换成森林:
二叉树与森林之间存在着一一对应的关系,可以根据以下步骤将一棵二叉树转化为森林:
-
将二叉树的根节点的右孩子节点的各个连线之间依次断开,将一棵二叉树分解为多棵二叉树
-
依次将每一棵二叉树通过加线、删线、旋转的方式转换为一棵树
-
各个树构成的整体即为一个森林
下图给出了将一棵二叉树转化为森林的过程:
二叉树转化为森林的代码与二叉树转化为树的代码基本相同,此处不再重复