通用树的实现(General Tree Implementations)
数据结构中说起树我们常常会想到二叉树,可是二叉树只是树的一个特例。本文主要讲解通用树(General Tree)的实现,希望能让读者了解到更多有关树的知识,更加了解树。
一:通用树的常用方法(General Tree ADT)
// General tree node ADT
template < typename E>
class GTNode {
public:
//得到节点的值
E value();
//判断该节点是否是叶子节点(没有子树)
bool isLeaf();
//得到该节点的父节点
GTNode* parent();
//该节点的最左边的孩子(从左往右数的第一个孩子)
GTNode* leftmostChild();
//得到该节点的右兄弟(同一层的节点上紧挨该节点的第一个节点)
GTNode* rightSibling();
//重置该节点的值
void setValue(E&);
//在该节点的最左边孩子处插入一个节点
void insertFirst(GTNode< E >*);
//在该节点的右兄弟处插入一个节点
void insertNext(GTNode< E >*);
//删除该节点的第一个孩子节点
void removeFirst();
//删除该节点的右兄弟节点
void removeNext();
};
在General Tree ADT中,比较重要的两个函数是leftmostChild()和rightSibling();因为这两个函数可以把General Tree 变成二叉树的模样,具体下面有解释。
二、通用树的实现(General Tree Implementation)
1:按照二叉树模式的实现
(1)解释:我们都知道二叉树有左子树和右子树,我们也可以把General Tree设置子树,因为General Tree每个节点可能有
0
0
0到
k
k
k个子节点,所以我们需要知道一棵树的节点最多有几个节点。
(2)实现:如图这颗General Tree,拥有最多子节点的节点是A,三个子节点,所以我们定义这颗General Tree的节点如下
template <typename T>
class GeneralTreeNode{
GeneralTreeNode* firstChild;
GeneralTreeNode* secondChild;
GeneralTreeNode* thirdChild;
};
对于图中A来讲,它有三个子节点,而对于B来讲,它只有一个子节点,这就造成B的其他两个子节点的空间浪费。这也是这种方式实现General Tree的一个缺点。
2:父指针表示法(Parent Pointer Implementations)
(1)解释:这种方法就是只存储每个节点的父节点。这样的好处就是空间相对1的方式没有浪费,最多知识根节点因为没有父节点浪费了一个指针的空间而已。但是,这样存有什么用?想知道这种存储方式有什么用先回答一个问题:给两个节点,如何判断它们在同一颗树上?
如图,有两棵树,假如给你两个节点是F和X,你如何判断这两个节点在同一颗树上?
这个问题的答案也就是我们这样存储General Tree的好处,因为只存储节点的父节点,每个节点我们可以找到它的父节点,递归下去,我们能找到
r
o
o
t
root
root节点,只要判断两个节点找到的根节点是否一样就可以判断在不在同一颗数上。
(2)实现
#include <iostream>
using namespace std;
//类ParTreeNode描述了树的结点定义
template<class T>
class ParTreeNode
{
private:
T value;
ParTreeNode<T>* parent;
int nCount;
public:
ParTreeNode(){
parent=NULL;
nCount=1;
}
~ParTreeNode() {}
T getValue(){
return value;
}
void setValue(const T& val){
value=val;
}
ParTreeNode<T>* getParent(){
return parent;
}
void setParent(ParTreeNode<T>* par){
parent=par;
}
int getCount(){
return nCount;
}
void setCount(const int count){
nCount=count;
}
};
//类ParTree描述了树的定义
template<class T>
class ParTree
{
public:
//array用来存储树节点
ParTreeNode<T>* array;
int Size;
ParTree(const int size){
array=new ParTreeNode<T>[size];
Size=size;
}
~ParTree() {
delete []array;
}
//找到node节点所在树的root节点
ParTreeNode<T> *Find(ParTreeNode<T>* node)const{
if (node->getParent()!=NULL){
node=node->parent;
}
return node;
}
//把节点为j的那颗树合并到有节点i的树上
void Union(int i,int j){
ParTreeNode<T>* parent1=Find(array[i]);
ParTreeNode<T>* parent2=Find(array[j]);
parent2->parent=parent1;
}
//判断两个节点在不在同一颗树上
bool Different(int i,int j){
return Find(array[i])!=Find(array[j]);
}
};
3:子结点表表示法(List of Children Implementations)
(1)解释:这种存储就是把节点的孩子从左到右存到一个链表中(为什么要用链表?因为每个节点的孩子数是不确定的,为了不造成空间浪费,使用链表是一种较好的方法)。所以,每个节点包含一个值,一个指向父节点的指针,一个指向存储了孩子节点的指针。
(2)优缺点分析:
优点:能够快速的得到节点的第一个孩子(从左数的第一个孩子),因为第一个孩子就是链表的第一个节点。
缺点:不太容易找到节点的右兄弟。
4:动态左孩子右兄弟的实现(Dynamic Left-Child/Right-Sibling Implementation)
(1)动态体现在哪里:前面几种方法都是顺序表式存储节点,若要删除或者插入一个节点,就很不方便。这种方法容易插入和删除某个节点。
如图:每个节点的左子节点存该节点的第一个孩子,右子节点存储下一个兄弟,这样存储,能够方便找到孩子和兄弟节点。
这种存储方式相对于其他几种方式来说是最有效的,它不仅可以高效的存储一颗树,甚至可以存储森林(多个树的集合)
如有错误,欢迎在评论区指出~