1.1 基本概念
1.1.1树的定义
- 树是一种非线性的数据结构(
树的定义是使用了递归,所以与树相关的算法都使用递归
) - 树是由 n(n>=0) 个节点组成的有限集合
- 如果
n= 0
称为空树
- 如果
n > 0
则:- 有一个特定的称之为
根(root)
的节点 - 根节点
只有
直接后继,但没有
直接前驱 - 除根以外的其他节点划分为 m(m >0) 个互补相交的有限集合 T0 ,T1, 。。。Tm-1 ,每一个集合又是一棵树,并且称为
根的子树
(sub tree)。
- 有一个特定的称之为
- 如果
1.1.2 树的示例
1.1.3 树中度的概念
- 树的节点包含一个数据及若干指向子树的分支
结点
拥有的子树数目称为结点
的度- 度为0 的结点成为叶节点
- 度不为0的结点称为分支节点
树的度定义为所有结点中度的最大值
- 注意区分树的度和结点的度
1.1.4 树中的前驱和后继
- 结点的直接后继称为该结点的孩子
- 相应的,该结点称为孩子的双亲
- 结点的孩子的孩子的。。。称为该结点的子孙
- 相应的,该结点称为子孙的祖先
- 同一个双亲的孩子之间互称兄弟
1.1.5 树的高度
- 根为第一层
- 根的孩子为第二层
- 。。。。
1.1.5 树的有序性
- 如果树中结点的各个子树从左向右是有次序的,子树间不能交换位置,则称该树为有序树,否则为无序树
1.1.6 森林
- 由n >= 0 棵互不相交的树组成的集合。
1.2 树的一些常用操作
- 将数据元素插入树中
- 将元素从树中删除
- 获取树的 结点数
- 获取树的高度
- 获取树的度
- 清空树中的元素
1.2.1 树和结点的存储结构设计
- GTree (通用树的意思)
1.2.2 设计要点
-
GTree 为通用树结构,每个节点可以存在多个后继节点
-
GTreeNode 能够包含任意多指向后继节点的指针
-
实现树结构的所有操作(增删查等)
-
GTreeNode 的设计
template <typename T>
class GTreeNode : public TreeNode<T>
{
public:
LinkList< GTreeNode<T> * > child;
};
- GTree的设计和实现
1.2.3 GTree(通用树结构)的实现架构
1.2.4 思考
- 每个树节点中为什么包含指向前驱节点的指针?
- 根结点 =====> 叶节点 : 非线性数据结构
- 叶结点 =====> 根结点 : 线性数据结构(链表)(可以通过线性表解决问题)
1.3查找操作
1.3.1 查找的方式
// 基于数据元素值的查找
GTreeNode<T>* find(const T& value) const;
// 基于节点的查找
GTreeNode<T>* find(TreeNode<T>* node)const
1.3.2 树中元素和节点的查找
- 目标(H元素)
- 目标(E结点)
1.3.3 基于数据元素值的查找
- 定义功能: find(node,value)
- 在node 为根结点的树中查找value所在的结点
- 在node 为根结点的树中查找value所在的结点
1.3.4 基于结点值的查找
1.3.5 代码实现
GTreeNode<T>* find(GTreeNode<T>* node , const T& value) const
{
GTreeNode<T>* ret = NULL; //这里其实默认考虑了,node 为空的情况
if(node != NULL)
{
if(node->value == value)
{
return node;
}
else
{
// for循环是遍历每一个子树
for(node->child.move(0); !node->child.end() && (ret == NULL); node->child.next())
{
ret = find(node->child.current(),value); // ret= 不能少
}
}
}
return ret;
}
GTreeNode<T>* find(GTreeNode<T>* node , const GTreeNode<T>* obj) const
{
GTreeNode<T>* ret = NULL;
if(node == obj)
{
return node;
}
else
{
if(node != NULL)
{
for(node->child.move(0); !node->child.end() && (ret == NULL); node->child.next())
{
ret = find(node->child.current(),obj); // ret= 不能少
}
}
}
return ret;
}
1.3.6 总结
- 总结
- 查找操作是树的关键操作之一
- 基于数据元素的查找可判断值是否存在于树中
- 基于结点的查找可以判断树中是否存在指定结点
- 插入操作和删除操作都依赖于查找操作
1.4 树中结点的插入操作
1.4.1 问题 如何指定新节点在树中的位置
- 问题分析:
- 树是非线性的,无法采用下标的形式定位数据元素
- 每一个书结点都有唯一的前驱结点(父节点)
- 因此,必须先找到前驱节点,才能完成新节点的插入
1.4.2 插入新节点流程图
1.4.3 代码实现
virtual bool insert(TreeNode<T>* node)
{
bool ret = true;
if(node != NULL)
{
if(this->m_root == NULL)
{
node->parent = NULL;
this->m_root = node;
}
else
{
GTreeNode<T>* np = find(node->parent);
if(np != NULL)
{
GTreeNode<T> * n = dynamic_cast<GTreeNode<T>*>(node);
if(np->child.find(n) < 0) //没必要查找整个树
{
np->child.insert(n);
}
}
else
{
THROW_EXCEPTION(InvalidOperatorException, "invalid parent tree node...");
}
}
}
else
{
THROW_EXCEPTION(InvalidParameterException, "parameter node cannot be NULL.....");
}
return ret;
}
virtual bool insert(const T&value, TreeNode<T>* parent)
{
bool ret = true;
GTreeNode<T> * node = new GTreeNode<T>();
if(node != NULL)
{
node->parent = parent;
node->value = value;
insert(node);
}
else
{
THROW_EXCEPTION(NoEnoughMemoryException, "No memory to create nex tree node ...");
}
return ret;
}
1.5 树的清空操作
1.5.1 画图clear()
1.5.2 伪代码
1.5.3 问题
- 树中的节点可能来源于不同的存储空间,如何判断堆空间中的节点并释放。
- 问题分析
- 单凭内存地址很难准确判断具体的存储区域
- 只有对空间的内存需要主动释放
- 清除操作只需要对堆空间中的节点进行释放
1.5.4 解决方案: 工厂模式
- 在GTreeNode 中增加保护成员变量
- 将GTreeNode 中的operator new 重载为保护成员函数
- 提供工厂方法GTreeNode* NewNode();
- 在工厂方法中 new 新节点并将m_flag 设置为true.
1.5.5 代码
template <typename T>
class GTreeNode : public TreeNode<T>
{
protected:
bool m_flag;
void* operator new(unsigned int size)
{
return Object::operator new(size);
}
public:
LinkList< GTreeNode<T>* > child;
GTreeNode()
{
m_flag = false;
}
bool flag()
{
return m_flag;
}
static GTreeNode<T>* NewNode()
{
GTreeNode<T> * ret = new GTreeNode<T>();
if(ret != NULL)
{
ret->m_flag = true;
}
return ret;
}
};
void free(GTreeNode<T> * node)
{
if(node != NULL)
{
for(node->child.move(0); !node->child.end(); node->child.next())
{
free(node->child.current());
}
if(node->flag())
{
delete node;
}
else
{
cout << node->value << endl;
}
}
}
1.6 树中结点的删除操作
1.6.1删除操作成员函数的设计要点
- 将被删结点所代表的子树进行删除
- 删除函数返回一颗堆空间中的树
- 具体返回值为指向树的智能指针对象
1.6.2 为什么返回值要用智能指针类
- 我们想达到的目的就是在不使用删除的这个树的时候,会自动释放
- 当我们需要使用的时候,直接使用就可以了
int* func()
{
return new int;
}
int main()
{
int* p = func();
return 0;
}
// 申请的| new int; 处于三不管地带,解决方案就是使用智能指针。
SharedPointer<int>* func()
{
return new int;
}
int main()
{
SharedPointer<int> p = func();
return 0;
}
1.6.3
- 实用的 设计原则
当需要从函数中返回堆中的对象时,使用智能指针(SharedPointer) 作为函数的返回值
1.6.4
- void remove(GTreeNode* node, GTree* & ret) // GTree* & ret 树的指针的应用
1.6.4 流程图
1.7 树中结点的属性操作
1.7.1 结点数
1.7.2 树的高度
1.7.3
int count(GTreeNode<T>* node) const
{
int ret = 0; // node 为空的情况
if(node != NULL)
{
ret = 1; // node 为1的情况
for(node->child.move(0); !node->child.end(); node->child.next())
{
ret += count(node->child.current());
}
}
return ret;
}
int height(GTreeNode<T>* node) const
{
int ret = 0;
if(node != NULL)
{
for(node->child.move(0); !node->child.end(); node->child.next())
{
int h = height(node->child.current());
if(ret < h)
{
ret = h;
}
}
ret = ret + 1;
}
return ret;
}
int degree(GTreeNode<T>* node) const
{
int ret = 0;
if(node != NULL)
{
ret = node->child.length();
// if(ret != 0)
// {
for(node->child.move(0); !node->child.end(); node->child.next())
{
int d = degree(node->child.current());
if(ret < d)
{
ret = d;
}
}
// }
}
return ret;
}
1.7 树中结点的遍历操作
1.7.1 思考
- 如何按照层次结构遍历通用树中的每一个数据元素 ?
- 当前的事实
- 树是非线性的数据结构,树的结点没有固定的编号方式
新的需求 - 为通用树结构提供新的方法,快速遍历每一个结点
- 树是非线性的数据结构,树的结点没有固定的编号方式
1.7.2
参考一 : 狄泰软件课程
如有侵权:请联系邮箱 1986005934@qq.com