数据结构(二十一)之 树

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所在的结点
      在这里插入图片描述

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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值