数据结构-树
- 树的定义与术语
- 树的遍历
- 树的顺序存储结构和链式存储结构实现
考研时间紧张,以后有空再传代码啦
一、树的定义与术语
1.1 定义
树型结构是一种动态的,非线性的,可描述结 构层次特性的数据结构。这种结构是按分枝关 系把信息联系起来的数据组织形式。
一棵树(tree)T是由一个或一个以上结点组成的有限集
- 其中有一个特定的结点R 称为T的根结点
- 集合(T-{R})中的其余结点可被划分为n≥0个互 不相交的子集T1,T2,…, Tn,其中每个子集本身 又是一棵树,并且其相应的根结点R1,R2,…, Rn 是R的子结点
子集Ti(1≤i≤n)称为树T的子树(subtree)。
- 子树可如下排序:Ti排在Tj之前,当且仅当i<j
- 为方便起见, 子树从左到右排列, 其中T1被称为R 的最左子树, R1被称为R的最左子结点
1.2 术语
结点:数据元素+若干指向子树的分支
结点的度:分支的个数
树的度:树中所有结点的度的最大值
根结点:是一个没有双亲结点的结点。一棵树中最多有一个根结点
边:边表示双亲结点与孩子结点的链接
叶子结点:度为零的结点
分支结点:度大于零的结点
结点的大小∶是指子孙的个数,包括其自身
路径:由从根到该结点所经分支和结点构成
**结点的层次:**假设根结点的层次为1,第i层的结点的子树根结点的层次为i+1
树的深度:树中叶子结点所在的最大层次
**树的高度:**从根结点到树中最深结点的路径长度
森林:是m(m≥0)棵互 不相交的树的集合
任何一棵非空树是一个二元组 Tree = (root,F) 其中:root 被称为根结点 F 被称为子树森林
如果树中除了叶子结点外,其余每一结点 只有一个孩子结点,这种树称做斜树。 对于每个结点仅有一个左孩子结点的树叫做左斜树。对于每个结点仅有右孩子结点的树叫做右斜树。
二、ADT
数据对象D: D是具有相同特性的数据元素的集合。
数据关系R:
若D为空集,则称为空树。 否则: (1) 在D中存在唯一的称为根的数据元素root; (2) 当n>1时,其余结点可分为m (m>0)个互 不相交的有限集T1, T2, …, Tm,其中每一 棵子集本身又是一棵符合本定义的树, 称为根root的子树。
基本操作:
➕ 结构构造/销毁型操作
➕ 引用型操作:
获取根结点,查找,遍历
➕加工型操作:
插入,删除元素结点
// General tree node ADT
template <class Elem> class GTNode
{
public:
GTNode(const Elem&); // Constructor
~GTNode(); // Destructor
Elem value(); // Return value
bool isLeaf(); // TRUE if is a leaf
GTNode* parent(); // Return parent
GTNode* leftmost_child(); // First child
GTNode* right_sibling(); // Right sibling
void setValue(Elem&); // Set value
void insert_first(GTNode<Elem>* n);
void insert_next(GTNode<Elem>* n);
void remove_first(); // Remove first child
void remove_next(); // Remove sibling };
};
template<classElem> class GenTree
{
private:
void printhelp (GTNode *); //Print helper function
public:
GenTree();//Constructor
~GenTree();//Destructor
voidclear();//Sendnodestofreestore
GTNode*root();//Returntheroot
voidnewroot(Elem&, GTNode<Elem>*, GTNode<Elem>*); //Combinetwosubtrees
voidprint();//Printatree};
};
三、树的遍历
- 先根(次序)遍历
若树不空,则先访问根结点,然后依次先根遍历各棵子树。 - 后根(次序)遍历
若树不空,则先依次后根遍历各棵子树,然后访问根结点。 - 按层次(广度)遍历
若树不空,则自上而下自左至右访问树中每个结点。
先根:A B E F C D G H I J K
后根:E F B C I J K H G D A
层次:A B C D E F G H I J K
先根遍历算法(递归)
template<typename Elem>
voidGenTree<Elem>::printhelp(GTNode<Elem>*root)
{
if(root->isLeaf())cout<<"Leaf:";
else cout<<"Internal:";
cout<<root->value()<<"\n";
for(GTNode<Elem>*temp=root->leftmostChild(); temp!=NULL; temp=temp->rightSibling())
printhelp(temp);
}
四、树的实现
相比于二叉树,通用的树的度可能大于2;可能是一个很大的值。因此用孩子指针的方式就不现实,因此大多都考虑使用父指针。
4.1 父指针表示法
对每个结点只保存一个指针域,指向其父结点。
4.1.1 特点
- 优点:顺着结点的父指针链追溯祖先结点很简单
- 缺点:找结点的子孙和兄弟结点很复杂
- 缺点:兄弟结点之间的左右次序没有存储
4.2.2 应用
父指针表示法常用于维护由一些不相交子集构成的集合
对于不相交集合,希望提供两种基本操作
-
判断两个结点是否同一个集合;
-
归并两个集合
因此命名为并查算法(并查集)
并查算法用一棵树代表一个集合。如果两个结点在同一棵树中,则认为它们在同一个集合中。
算法思想:
一开始每个元素都在独立的只包含一个结点的树中,而他自己就是根节点。通过使用函数differ可以检查一个等价对中的两个元素是否在同一棵树中。如果不是就通过union函数归并两个等价类。归并时通常让节点个数少的指向结点个数多的。
例如:等价对**(A,B)** (A,C) (A,H) (B,H) (C,H) (H,E) (E,D) (E,F) (E,G) (D,F) (F,G) (I,F)
- 所有结点都为独立根节点
- 归并ab ch gf de if 归并gf时由于g指向f,因为归并fi时,f的结点个数更多,因而I指向F
- 归并HA EG 以(E,G)为例,G的父节点为F,E是D,由于D树结点个数比F树少,因此D指向F
- 最后归并(H,E) 同理,A指向F
这种方法可以把结点的深度控制在 l o g n log_n logn
对于c,如果采用路径压缩进行合并,那么找H根节点的时候,会同时把H指向A,然后把E指向F,再进行合并。结果如下图
这种方法使得对n个结点进行n次FIND操作的路径压缩代价约为 θ ( n l o n g ∗ n ) \theta(n long^*n) θ(nlong∗n)
在计算机科学中,n的重对数,写为 l o g ∗ n log^*n log∗n(通常读作“log star”),是对数函数在结果小于或等于1之前必须迭代应用的次数。
def log_star(n):
ret = 1
foo = log(n, 2)
while foo != 1:
foo = log(foo, 2)
ret += 1
return ret
#出自Kmbingbing
4.2 子结点表表示法 ----- 链式实现
4.2.1 存储形式
在数组中存储树结点。每个结点包括:结点值、父指针(或索引)及一个指向子结点链表的指针
4.2.1 特点
- 优点:结点的最左子结点由链表的第一个表项直接找到
- 优点:两颗树存储在同一数组中时,归并两颗树简单
- 缺点:找结点的右兄弟结点比较困难
- 缺点:两颗树分别存储在不同数组中时,归并两颗树困难
4.3 左子结点/右兄弟结点表示法-----链式实现
4.3.1 存储形式
每个结点存储结点的值以及指向父结点、最左子结点和右侧兄弟结点。
left:指向最左子结点
right:指向第一个右兄弟结点的指针或索引
4.3.2 特点
比子结点表表示法空间效率更高,且每个结点的空间是固定长度的。
对于树的归并需要调整三个指针,一个是R的右兄弟,一个是R的父节点,和R’的最左儿子。
4.4 动态结点表示法(1)-----链式实现
4.4.1 存储形式
动态结点表示法:每个结点存储一个基于数组的子结点指针表
struct dynamicNode { //动态表示树的树节点。假设限制有5个子节点
dynamicNode *children[5];
int data;
};
4.4.2 特点
对任意一个结点可能的子结点个数加以限制,对每个结点分配确定数目的指针域。因此假设在结点生成时就已经知道它的子结点数目,对子结点的数目强加了不必要的限制
4.5动态结点表示法(2) -----链式实现
4.5.1 存储形式
template<typename E>
struct node
{
node *next;
E elem;
}
struct dynamicNode
{
int val;
int size;
node<dynamicNode> * children;
};
- 为每个结点分配可变的存储空间。
- 每个结点存储一条子结点指针链表。
4.5.2 特点
灵活,动态分配结点空间
4.6 动态左子结点/右兄弟结点表示法-----链式实现
左子结点/右兄弟结点表示法本质上使用二叉树来替换树.使用这种方法可以把任意树,甚至是森林替换为二叉树。
其中,左子结点是原树中结点的最左子结点。右子结点是原树中结点的右侧兄弟结点。
可以看到,给定一棵树可以通过左孩子右兄弟的办法找到一棵唯一的二叉树。从物理结构来看,他与二叉链表是相同的,只是解释不同(left_Child right_Sibling 和 left_Child right_Child)
那么森林(不相交的树们)怎么转换为二叉树?
- 增加一个虚拟的根结点,它的儿子为各棵树的根。
- 那么,就变成了树转换成二叉树的问题。
4.7 K叉树
K叉树的结点有K个子结点
子结点的数目是固定的(K个)二叉树的许多特性可以推广到K叉树
4.8 树的顺序表示法
问题:怎么建立存储内容,怎么恢复成森林或二叉树
树的顺序表示法把结点的值按照它们在先根遍历中出现的顺序存储起来,描述树形状的充足信息同时也被存储起来。
4.8.1 先根遍历序列表示法
把树的结点值按照先根遍历序列的顺序列出,把所有非空结点看成分支结点,只有空指针NULL才被当做叶结点。
应用中的问题:
- 为了找到A的右孩子,需要把其左子树都查找一遍。
- 空间开销分析比使用指针高效
- 根据满二叉树定理,其中空指针数目=n+1,结构性开销占了1/2(前面二叉树部分提到过)
目的:存储结点的值,尽可能少的存储用于重建树结构的信息
优点:节省空间,不需要存储指针
缺点:只允许顺序查找
应用:高效率磁盘存储分布式环境中计算机之间传输数据结构
4.8.2 带标记的先根序列表示法
显式地在每个结点后标识出它是叶结点还是分支结点
- 分支结点加标记(’),而叶结点不加任何标记
- 分支结点的空子结点以“/”表示,而叶结点的空子结点不加表示。
- 因为不需要存储叶子结点的空指针,所需开销更小n结点中需要存储标记位的空间
以此图为例,表示为:A’ B’ / D C’ E’ G / F’ H I
-
用顺序表示法存储树不仅要给出一个结点是分支结点还是叶节点,还必须给出有多少个子结点的信息。
-
一种替代的方法是给出一个结点的子结点表结束的位置,如:用特殊标记“)”来标明子结点表的结束。
以此图为例,可以看作一个栈,当到了叶子结点就弹出一个结点。如果当前结点所有子树都遍历完了,就在弹出,弹出对应的就是)。比如RAC时,C为叶子节点,故RAC) 弹出C 观察 A的其他子结点,然后同理D)E).然后A树遍历完了,故RAC)D)E)) 然后到R树,还有一棵右子树 。BF ,F为叶子结点,故弹出一个,RAC)D)E))BF) 然后B树都遍历完了 RAC)D)E))BF) ,再他拿出一个B RAC)D)E))BF)) R树已经遍历完了,RAC)D)E))BF)))