前言
树是一种一对多的关系,而树的存储结构并不是指代顺序存储和链式存储,而是指双亲表示法、孩子表示法、双亲孩子表示法与孩子兄弟表示法。在实现这些表示法的时候,我们不能在局限于顺序和链式,而是要充分利用这两种存储结构的特点,来实现树的数据结构
一、双亲表示法
双亲表示法指的是每个节点都能找到自己双亲的位置。我们用一个顺序表来存储一棵树,每个元素除了自己的数据域外还有一个指向自己双亲位置的游标,由于根节点没有双亲所以他的指针域是-1,如下图的树可用下表来表示
用c++实现的代码如下:
typedef struct{
TElemType data;
int parent;
}PTNode;
typedef struct{
PTNode nodes[MAXSIZE];
int r, n; // 根的位置和节点数
}PTree;
这么做,可以很方便的从任意节点找到根节点,那如果要从根节点找其他节点呢,那就只能遍历了,基于此我们可以改进一些,通过新增长子域来解决,如下图:
我们也可以增加一个右兄弟域来表示同级节点,如下图:
综上,存储结构设计是一个非常灵活的过程,你有什么样的需求,就可以设计什么样样的结构,但存储结构也不是越复杂越好,越复杂的存储结构意味着更多的空间开销或者是时间开销,总言之设计一个存储结构就是不断地在时间和空间上做权衡,直至设计出一款你满意且符合需求的结构
二、孩子表示法
考虑到树是一对多的结构,那我们能不能把每个节点的孩子,给表示出来,就像这样:
但是这么做,当树中各节点的度相差较大时,会十分浪费空间,那我们可不可以想一个办法,让指针域的个数按需分配。我们可以在增设一个度数域,来表示每个节点的度数,如下:
但是这么做又产生了一个新的问题,因为每个节点度不一样,维护链表就变得很麻烦,不如我们干脆直接放弃表示考虑处理节点的度,在线性表中如果一个数据的个数无法很好的确定,我们一般使用链表,没错我们可以把孩子串成链表,然后由每个节点指向他们的首元节点,如下图:
代码如下:
// 孩子节点的结构
typedef struct CTNode{
int child;
CTNode *next;
}*ChildPtr;
// 双亲节点结构
typedef struct{
TElemType data;
ChildPtr firstchild;
}CTBox;
// 树的结构
typedef struct{
CTBox nodes[MAXSIZE];
int r, n;
}CTree;
三、双亲孩子表示法
诶,如果我既想向下找节点,又想向上找根怎么办?简单我们把双亲和孩子表示法结合一下,只需要在双亲节点的指针域新增他双亲的位置就好了
// 孩子节点的结构
typedef struct CTNode{
int child;
CTNode *next;
}*ChildPtr;
// 双亲节点结构
typedef struct CTBox{
TElemType data;
ChildPtr firstchild;
ParentPtr parent;
}*ParentPtr;
// 树的结构
typedef struct{
CTBox nodes[MAXSIZE];
int r, n;
}CTree;
四、孩子兄弟表示法(树与二叉树沟通的桥梁)
其实到这里,我们已经能完美地表示一颗树了,那我们为什么还要讲孩子兄弟表示法,因为他十分重要,他其实形成了一种特殊的树-二叉树,在后面的章节我们会详细展开讲二叉树,而且很大基于树的应用都是以二叉树为基础的。那孩子兄弟表示法是什么,如果一个节点的孩子确定,那么那么他剩下的兄弟也确定,我们可以在每一个节点设置两个指针,分别指向他的孩子与兄弟,代码如下:
typedef struct CSNode{
TElemType data;
CSNode *firstchild, *rightsib;
}*CSTree;
结构示意图如下:
这是他画成二叉树的样子: