树的表示方式
树的结构是⼀种非线性的存储结构。存储的是具有⼀对多的关系的数据元素的集合。
树的概念
图中可以看见一个使用树形结构存储的一个集合,这个集合就是{A,B,C…}。对于数据A来说,和数据B、C、D有关系。对于数据B来说,和E,F,G有关系。这就是一对多的关系。
我们将一对多的关系的集合中的数据元素按照图中的形式进行存储,整个存储形状在逻辑结果上面看,类似于实际生活中倒着的树,所以就将这种结构称之为树形结构。
树的结点
结点∶使用树结构存储的每一个数据元素都被称为"结点"。例如图中的A就是一个结点。
根结点:有一个特殊的结点,这个结点没有前驱,我们将这种结点称之为根结点。
父结点(双亲结点)、子结点和兄弟结点︰对于ABCD四个结点来说,A就是BCD的父结点,也称之为双亲结点。而BCD都是A的子结点,也称之为孩子结点。对于BCD来说,因为他们都有同一个爹,所以它们互相称之为兄弟结点。
叶子结点∶如果一个结点没有任何子结点,那么此结点就称之为叶子结点。
结点的度︰结点拥有的子树的个数,就称之为结点的度。
树的度:在各个结点当中,度的最大值。为树的度。
树的深度或者高度︰结点的层次从根结点开始定义起,根为第一层,根的孩子为第二层。依次类推。
树的存储结构
双亲表示法
顺序表表示形式
//Node结点
/**
* @author Turing
* @version 1.0.0
* @Description TODO
**/
public class Node {
//树种存放的数据
int data;
//该结点的父节点在数组中的下标位置
int parent;
public Node(int data,int index){
this.data = data;
this.parent = index;
}
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
public int getParent() {
return parent;
}
public void setParent(int parent) {
this.parent = parent;
}
}
//双亲表示法
/**
* @author Turing
* @version 1.0.0
* @Description TODO
**/
public class TreeParentDemo {
Node node[];
int size;//当前元素个数
int maxSize;//当前最大元素个数
public TreeParentDemo(int temp){
node = new Node[temp];
this.maxSize = temp;
this.size = 0;
}
/**
* 构建跟结点
* @param data 根节点的数据
*/
public void insert(int data){
//已有根节点 ?法继续构建根节点
if(size > 1){
//提示
}
Node node = new Node(data,-1);
this.node[size] = node;
size++;
}
/**
* 插入子节点
* @param data 待插入子节点的数据
* @param index 该子节点插入到哪个结点下面
*/
public void insertSon(int data,int index){
//是否有这个父节点 如果没有该父节点 那就 提示一下
Node node = new Node(data,index);
this.node[size] = node;
size++;
}
public void show(){
for (int i = 0; i < size;i++){
System.out.println("数据:" + node[i].getData() + "父级结点为:" +node[i].getParent());
}
}
}
//测试数据
/**
* @author Turing
* @version 1.0.0
* @Description TODO
**/
public class Test {
public static void main(String[] args) {
TreeParentDemo demo = new TreeParentDemo(10);
demo.insert(1);
demo.insertSon(2,0);
demo.insertSon(3,0);
demo.insertSon(4,0);
demo.insertSon(7,1);
demo.show();
}
}
优缺点说明
由于根结点是没有双亲的,所以我们约定根结点的位置域设置为-1,这也就意味着,我们所有的结点都存有它双亲的位置。这样的存储结构,我们可以根据结点的parent指针很容易找到它的双亲结点,所用的时间复杂度为O(1),直到parent为-1时,表示找到了树结点的根。可如果我们要知道结点的孩子是什么,对不起,请遍历整个结构才行。
这真是麻烦,能不能改进一下呢?当然可以。我们增加一个结点最左边孩子的域,不妨叫它长子域,这样就可以很容易得到结点的孩子。如果没有孩子的结点,这个长子域就设置为-1。
对于有0个或1个孩子结点来说,这样的结构是解决了要找结点孩子的问题了。甚至是有2个孩子,知道了长子是谁,另一个当然就是次子了。另外一个问题场景,我们很关注各兄弟之间的关系,双亲表示法无法体现这样的关系,那我们怎么办?嗯,可以增加一个右兄弟域来体现兄弟关系,也就是说,每一个结点如果它存在右兄弟,则记录下右兄弟的下标。同样的,如果右兄弟不存在,则赋值为-1。
但如果结点的孩子很多,超过了2个。我们又关注结点的双亲、又关注结点的孩子、还关注结点的兄弟,而且对时间遍历要求还比较高,那么我们还可以把此结构扩展为有双亲域、长子域、再有右兄弟域。存储结构的设计是一个非常灵活的过程。一个存储结构设计得是否合理,取决于基于该存储结构的运算是否适合、是否方便,时间复杂度好不好等。
孩子表示法
孩子兄弟表示法
//结点
/**
* @author Turing
* @version 1.0.0
* @Description TODO
**/
public class Node {
//数据
int data;
//孩子指针域
Node child;
//兄弟指针域
Node sibling;
public Node(int data){
this.data = data;
}
}
/**
* @author Turing
* @version 1.0.0
* @Description TODO
**/
public class TreeCP {
Node root;
//新增一个临时结点 方便接收定位的数据
Node tempNode;
public TreeCP(int data){
root = new Node(data);
}
/**
* 在指定的位置添加结点
* @param data
*/
public void insert(int data,int temp){
//定位到该节点
getNode(root,temp);
Node node = tempNode;
if (node == null){
System.out.println("没有结点");
} else{
if (node.child == null){
node.child = new Node(data);
}else {
Node tempNode = node.child;
Node newNode = new Node(data);
newNode.sibling = tempNode.sibling;
tempNode.sibling = newNode;
}
}
}
public void getNode(Node node,int temp){
if (node.data == temp){
tempNode = node;
}
//如果我的兄弟结点?空,则取出元素
if (node.sibling != null){
getNode(node.sibling,temp);
}
if (node.child != null){
getNode(node.child,temp);
}
}
}
//测试
/**
* @author Turing
* @version 1.0.0
* @Description TODO
**/
public class Test {
public static void main(String[] args) {
TreeCP treeCP = new TreeCP(1);
treeCP.insert(2,1);
treeCP.insert(3,1);
treeCP.insert(4,2);
System.out.println(treeCP.root.child.sibling.data);
}
}
这种表示法,给查找某个结点的某个孩子带来了方便,只需要通过firstchild找到此结点的长子,然后再通过长子结点的rightsib找到它的二弟,接着一直下去,直到找到具体的孩子。当然,如果想找某个结点的双亲,这个表示法也是有做陷的,那怎么办呢?
对,如果真的有必要,完全可以再增加一个parent指针域来解决快速查找双亲的问题,这里就不再细谈了。