数据结构与算法C++描述(11)---树及二叉树

来源:互联网 发布:阿里云服务器实名认证 编辑:程序博客网 时间:2024/06/06 18:43

1、 树的相关概念

1.1 树的定义

根据文献[1],树定义为:树(tree)t是一个非空的有限元素的集合,其中一个元素为根,余下的元素(如果有的话)组成t的子树(subtree)。
树一般用来描述具有层次结构的数据,比如家族关系、管理机构的上下级关系等。

1.2 有关树的常用术语

为了描述更为清晰,以下图的“联邦政府模型”树为例说明(图片来自参考文献[1])。
这里写图片描述
1. 叶子(leaf):树中没有孩子的元素,如上图中的“陆军”、“海军”等;
2. 根(root):唯一没有父节点的元素,如上图中的“联邦政府”即为root;
3. 树的级数:树根的级数为1,其孩子(如果有)的级为2,孩子的孩子级为3;
4. 元素的度:指其孩子的个数,如上图中元素“国防部”的度为4;
5. 树的度:指其元素度的最大值,如上图树的度为4。

2、二叉树的相关概念

二叉树(Binary tree)t是有限个元素的集合(可以为空)。当二叉树非空时,其中有一个称为根(root)的元素,余下的元素(如果有的话)被组成2个二叉树,分别称为左子树和右子树。如下图所示。
这里写图片描述
可总结出二叉树的特性如下(注:二叉树中的元素编号规则是从上到下,从左至右依次编号):
1. 包含n个元素的二叉树的边数为n-1。
这个很好理解,除了根节点外,每个节点都和其父节点之间有一条边,从而共有n-1条边。
2. 若二叉树的高度为h,h>=0,则该二叉树最少有h个元素,最多有2h1个元素。
(1)当每个父节点有且仅有左子树,此时二叉树有h个元素;
(2)当每个父节点都同时具有左右子树时,二叉树有2h1个元素,此时的二叉树也称为满二叉树。
3. 包含n个元素的二叉树的最大高度为n,最小为[log2(n+1)]。
性质2的逆问题。。。。。。
4. 设二叉树中一元素的序号为i,1<=i<=n。则有下述关系成立:
(1)当i=1时,该元素为二叉树的根。若i>1,则该节点父节点的编号为[i/2](向下取整);
(2)当2i>n时,该元素无左孩子,否则,其左孩子的编号为2i;
(3)当2i+1>n时,该元素无右孩子,否则,其右孩子的编号为2i+1。
这个性质要严格证明的话可以用归纳法来证。在这里说一下验证思路:
对于(1),考虑i>1时的情形,对于二叉树中的父节点j,其左孩子编号为2j,右孩子编号为2j+1。现在已知孩子编号i,求其父节点编号。从而推出j=i/2或j=(i-1)/2,继而j=[i/2](向下取整)。
对于(2),因父节点i的左孩子编号为2i,树中共有n个元素,若2i>n,则表明左孩子不存在。对于(3)同理

2、链表描述二叉树

上一部分介绍了二叉树的基本理论,下面利用链表队列来实现二叉树的描述。在文献[1]中利用了C++的类建立了链表队列,当然也可以利用struct来建立链表队列。
对于C++中的struct和类描述,两者的最大区别在于默认的继承方式不同,类的继承方式默认为private私有继承;而struct默认为public公有继承。
利用链表队列来描述二叉树的大致思路如下:
首先建立二叉树中的节点类,然后在二叉树类中实现对二叉树节点的相关操作。

2.1 链表队列的建立

在以前的文章中队列也详细介绍过链表队列,在此直接上代码。

/*--------------------------基于链表描述的队列---------------------------*/template <class T> class LinkedQueue;//队列链表节点类template <class T>class Node{    friend LinkedQueue<T>;     //声明为LinkedQueue类的友类public:    Node() {};    ~Node() {};private:    T data;    Node<T> *link;};//链表队列类template <class T>class LinkedQueue{public:    //初始时,front=rear=0    LinkedQueue() { front = rear = 0; };    ~LinkedQueue();    bool IsEmpty() const               //判断是否为空    {        return front == 0;    }    LinkedQueue<T> &Add(const T &x);   //添加元素    LinkedQueue<T> &Delete(T &x);      //删除元素private:    Node<T> *front;                    //头节点    Node<T> *rear;                     //末节点};//析构函数template <class T>LinkedQueue<T>::~LinkedQueue(){    Node<T> *next = new Node<T>;    while (front)                     //未到队列末尾    {        next = front->link;          //指向下一个节点        delete front;                //删除当前节点        front = next;                //循环    }}//添加元素template <class T>LinkedQueue<T> &LinkedQueue<T>::Add(const T &x){    Node<T> *p = new Node<T>;    p->data = x;    p->link = 0;    if (front)               //队列不为空,当前末节点指向p        rear->link = p;    else                     //队列为空        front = p;           //p为队列首元素          rear = p;                //更新队列末节点    return *this;}//删除元素template <class T>LinkedQueue<T> &LinkedQueue<T>::Delete(T &x){    if (IsEmpty())        throw OutOfRange();    else    {        x = front->data;        Node<T> *next = front;        front = front->link;        delete next;    }    return *this;}

2.2 建立二叉树节点

每个二叉树中的元素(即二叉树节点)有三个变量:元素数据值data、左子树LeftChild、右子树RightChild。
在建立一个二叉树节点时,有三种方式:

  1. 默认构造方式,建立一个空节点;
  2. 对节点元素值赋值,并将其左右子树置为空;
  3. 对节点元素值赋值,并对左右子树赋值。
    以上描述均在BinaryNode类中体现:
/*---------------------------二叉树节点类--------------------------------*/template <class T> class BinaryTree;template <class T>class BinaryTreeNode{    friend BinaryTree<T>;      //声明BinaryTree类为其友类,便于Binary类访问本类的所有变量public:    //建立二叉树节点方式1:默认构造方式    BinaryTreeNode() { LeftChild = RightChild = 0; };    //建立二叉树节点方式2    BinaryTreeNode(const T &e) {        data = e;        LeftChild = RightChild = 0;    }    //建立二叉树节点方式3    BinaryTreeNode(const T& e, BinaryTreeNode<T> *left, BinaryTreeNode<T> *right)    {        data = e;        LeftChild = left;        RightChild = right;    }private:    T data;                                      //节点数据    BinaryTreeNode<T> *LeftChild,                //左子树                    *RightChild;                 //右子树};

2.3 二叉树类

在利用指针描述二叉树时,二叉树的初始根节点root就可以根据二叉树的性质来遍历整个二叉树,对比于数组名(数组元素的首地址)。那么,对二叉树的操作是基于root指针的。对二叉树有以下操作:
这里写图片描述

2.3.1 二叉树类的声明及简单函数的实现

在此要说明一下,将四种遍历方式的函数声明为私有变量并利用函数名作为形参的原因:首先,作为私有变量,外界不会改变其操作,有利于封装,利用函数名作为形参,可实现在此种遍历方式上的多种操作,体现了面向对象编程的特点。比如,在实现删除二叉树函数Delete时,就直接利用了后序遍历的方式,此时只要将删除节点的函数名Free传入私有的后序遍历函数即可。缺点就是,程序看着有点长。。。。。。但对使用者来说很良心。。。。。。

/*----------------------------------------二叉树类定义--------------------------------------*/int _count = 0;     //记录节点个数变量template <class T>class BinaryTree{public:    BinaryTree() { root = 0; };    ~BinaryTree() {};    bool IsEmpty()const                    //判断二叉树是否为空    { return ((root) ? false : true); }    bool Root(T &x) const                 //取根节点的data域,放入x中,若不存在,返回false    {        if (root)        {            x = root->data;            return true;        }        else            return false;    }    //将树left和树right以及element合并成一棵新树    void MakeTree(const T& element, BinaryTree<T> &left, BinaryTree<T> &right);    //分解一个树    void BreakTree(T& element, BinaryTree<T> &left, BinaryTree<T> &right);    //前序遍历函数    void PreOrder(void(*Visit)(BinaryTreeNode<T> *u))    {   PreOrder(Visit, root);  }    //中序遍历函数    void InOrder(void(*Visit)(BinaryTreeNode<T>* u))    {   InOrder(Visit, root);   }    //后序遍历函数    void PostOrder(void(*Visit)(BinaryTreeNode<T> *u))    {   PostOrder(Visit, root); }    //逐层遍历函数    void LevelOrder(void(*Visit)(BinaryTreeNode<T> *u));    //前序遍历输出函数---公有的,便于外部访问    void PreOutput()    {   PreOrder(Output, root); cout << endl;   }    //中序遍历输出函数    void InOutput()    {   InOrder(Output, root); cout << endl;    }    //后序遍历输出函数    void PostOutput()    {   PostOrder(Output, root); cout << endl;  }    //逐层遍历输出函数    void LevelOutput()    {   LevelOrder(Output); cout << endl;   }    //删除一棵二叉树.采用后序遍历的方式删除一棵二叉树    void Delete()    {   PostOrder(Free, root); root = 0;    }    //计算树的高度    int Height()const { return Height(root); }    //获取树的节点个数    int Size()    {        _count = 0;        PreOrder(Add1, root);        return _count;    }private:    BinaryTreeNode<T> *root;               //根节点指针    //前序遍历---私有的,有利于封装,并且可实现多次操作    void PreOrder(void(*Visit)(BinaryTreeNode<T> *u), BinaryTreeNode<T> *t);    //中序遍历    void InOrder(void(*Visit)(BinaryTreeNode<T> *u), BinaryTreeNode<T> *t);    //后序遍历    void PostOrder(void(*Visit)(BinaryTreeNode<T> *u), BinaryTreeNode<T> *t);    //输出节点函数    static void Output(BinaryTreeNode<T> *t)    {   cout << t->data << "  ";    }    //删除节点函数    static void Free(BinaryTreeNode<T> *t)    {   delete t;   }    //计算子树的最大高度    int Height(BinaryTreeNode<T> *t) const;    //记录节点个数    static void Add1(BinaryTreeNode<T> *t) { _count++; }};

2.3.2 两个二叉树合并成一个树—MakeTree()函数

//将树left和树right以及element合并成一棵新树template <class T>void BinaryTree<T>::MakeTree(const T& element, BinaryTree<T> &left, BinaryTree<T> &right){    //创建新树,以当前树的根节点为根节点,树left的根节点为左子树,树right的根节点为右子树    root = new BinaryTreeNode<T>(element, left.root, right.root);    //阻止访问left和right    left.root = right.root = 0;}

2.3.3 分解一棵树—BreakTree()函数

//分解树//this,left和right必须是不同的树template <class T>void BinaryTree<T>::BreakTree(T& element, BinaryTree<T> &left, BinaryTree<T> &right){    //检查数是否为空    if (!root)        throw BadInput();    //分解树,将当前树的根节点作为element,根节点的左孩子为树left的根节点    //根节点的右孩子为right的根节点    element = root->data;    left.root = root->LeftChild;    right.root = root->RightChild;    //删除当前根节点    delete root;    root = 0;}

2.3.4 前序遍历—PreOrder()函数

所谓前序遍历就是,遍历顺序为:根节点->左子树->右子树

//前序遍历template <class T>void BinaryTree<T>::PreOrder(void(*Visit)(BinaryTreeNode<T> *u), BinaryTreeNode<T> *t){    if (t) {        Visit(t);                      //访问根节点        PreOrder(Visit,t->LeftChild);  //递归前序遍历左子树        PreOrder(Visit,t->RightChild); //递归前序遍历右子树    }}

2.3.5 中序遍历—InOrder()函数

所谓中序遍历就是,遍历顺序为:左子树->根节点->右子树

//中序遍历template <class T>void BinaryTree<T>::InOrder(void(*Visit)(BinaryTreeNode<T> *u), BinaryTreeNode<T> *t){    if (t) {                           //树不为空        InOrder(Visit,t->LeftChild);   //递归中序遍历左子树        Visit(t);                      //访问根节点        InOrder(Visit, t->RightChild); //递归中序遍历右子树    }}

2.3.6 后序遍历—PostOrder()函数

所谓后序遍历就是,遍历顺序为:左子树->右子树->根节点

//后序遍历template <class T>void BinaryTree<T>::PostOrder(void(*Visit)(BinaryTreeNode<T> *u), BinaryTreeNode<T> *t){    if (t) {                            //树不为空        PostOrder(Visit,t->LeftChild);  //递归后序遍历左子树        PostOrder(Visit,t->RightChild); //递归后序遍历右子树        Visit(t);                       //访问根节点    }}

2.2.7 逐层遍历—LevelOrder()函数

所谓逐层遍历就是,遍历顺序遵照从上到下,从左至右的顺序。
在此用到了链表队列结构,由于队列元素是先进先出的。因此在逐层遍历二叉树时,先访问根节点,后再访问左子树,若存在根节点则将其放入队列,接着访问 右子树,若存在根节点则将其放入队列,此次循环结束,依次将队列中的元素取出,则左子树的根节点(若存在)排在右子树根节点(若存在)的前面。

//逐层遍历template<class T>void BinaryTree<T>::LevelOrder(void(*Visit)(BinaryTreeNode<T> *u)){    LinkedQueue<BinaryTreeNode<T>*> Q;     //声明节点为二叉树节点类型的链表队列Q    BinaryTreeNode<T> *t;                  //声明二叉树节点t    t = root;                              //t赋为根节点    while (t)                              //树不为空    {        Visit(t);                          //首先访问根节点        if (t->LeftChild)                  //若左子树根节点存在,进入队列            Q.Add(t->LeftChild);        if (t->RightChild)                 //若右子树根节点存在,进入队列            Q.Add(t->RightChild);        try        {            Q.Delete(t);                   //依次出队列        }        catch (OutOfRange)                 //如果捕获越界异常,则表明队列中无元素,遍历完成        {            return;        }               }}

2.2.8 计算输的高度—Height()函数

计算树的高度时,分别计算左子树的高度和右子树的高度,两者中的大者既是树的高度。

//计算子树的最大高度template<class T>int BinaryTree<T>::Height(BinaryTreeNode<T> *t) const{    if (!t) return 0;                         //树为空    int height_left = Height(t->LeftChild);   //左子树高度    int height_right = Height(t->RightChild); //右子树高度    if (height_left > height_right)           //若左子树高度大于右子树        return ++height_left;                 //左子树返回值加1    else                                      //否则,右子树高度加1        return ++height_right;}

2.3 测试

y.MakeTree(1, a, a);    z.MakeTree(2, a, a);    x.MakeTree(3, y, z);    y.MakeTree(4, x, a);    y.PreOrder(ct);    cout << "树中节点个数:  " << num_node << endl;    cout << "前序遍历树:    ";    y.PreOutput();    cout << "中序遍历树:    ";    y.InOutput();    cout << "后序遍历树:    ";    y.PostOutput();    cout << "逐层遍历树:    ";    y.LevelOutput();    cout << "树中节点个数:   ";    cout << y.Size() << endl;    cout << "树的高度:      ";    cout << y.Height() << endl;

2.4 测试结果

这里写图片描述


参考文献:
[1] 数据结构算法与应用:C++描述(Data Structures, Algorithms and Applications in C++ 的中文版)

阅读全文
0 0
原创粉丝点击