zoukankan      html  css  js  c++  java
  • 【转载】二叉树的基本概念和实现

    原文:二叉树的基本概念和实现

    继续是《数据结构算法与应用:C++语言描述》的笔记,这是第八章二叉树和其他树的内容,本节内容介绍树的定义以及二叉树的代码实现。

    树t是一个非空的有限元素的集合,其中一个元素为根,余下的元素组成t的子树。

    在画一棵树时,每个元素都代表一个节点。树根在上面,其子树画在下面。如下图所示,其中,Ann,Mary,John是Joe的 孩子(children) ,而Joe是他们的 父母(parent)。有相同父母的孩子是 兄弟(sibling) 。Ann,Mary,John都是兄弟。此外,还有其他术语: 孙子(grandchild),祖父(grandparent),祖先(ancestor),后代(descendent)等 。树中没有孩子的元素称为 叶子(leaf) 。图中Ann,Mark,Sue和Chris是树的叶子。

    树的另一个常用术语是 级(level) 。指定树根的级是1,其孩子的级是2,依次类推。上图中Joe的级是1,而Ann,Mary,John的级是2,然后Mark,Sue,Chris的级是3。

    元素的度是指其孩子的个数。叶节点的度是0。 树的度是其元素度的最大值 。所以上图中的度是3。

    二叉树

    定义:二叉树(binary tree)t是有限个元素的集合(可以为空)。当二叉树非空时,其中有一个称为根的元素,余下的元素(如果有的话)被组成2个二叉树,分别称为t的左子树和右子树。

    二叉树和树的根本区别是:

    • 二叉树可以为空,树不能为空
    • 二叉树中每个元素都恰好有两棵子树(其中一个或两个可能为空)。而树中每个元素可以有若干子树。
    • 在二叉树中每个元素的子树都是有序的,也就是说,可以用左、右子树来区别。而树的子树间是无序的。

    下图给出了表示数学表达式的二叉树,总共有3个数学表达式。每个操作符可以有一个或两个操作数,左操作数是操作符的左子树,而右操作数则是右子树。树中的叶节点是常量或者变量。

    二叉树的特性

    特性1: 包含n(n>0)个元素的二叉树边数是n-1。

    证明 二叉树中每个元素 (除了根节点)有且只有一个父节点。在子节点与父节点间有且只有一条边,因此边数为n-1。

    二叉树的高度或者深度是指该二叉树的层数。

    特性2: 若二叉树的高度为h,h0h≥0,则该二叉树最少有h个元素,最多有2h12h−1个元素。

    证明 因为每一层最少要有1个元素,因此元素数最少为h。每元素最多有2个子节点,则第i层节点元素最多为2i12i−1个,i>0。h=0时,元素的总数为0,也就是20120−1。当h>0时,元素的总数不会超过hi=12i1=2h1∑i=1h2i−1=2h−1。

    特性3: 包含n个元素的二叉树的高度最大是n,最小是log2(n+1)⌈log2(n+1)⌉。

    证明 因为每层至少有一个元素,因此高度不会超过n。由特性2,可以得知高度为h的二叉树最多有2h12h−1个元素。因为n2h1n≤2h−1,因此hlog2(n+1)h≥log2(n+1)。由于h是整数,所以hlog2(n+1)h≥⌈log2(n+1)⌉。

    当高度是h的二叉树恰好有2h12h−1个元素时,称其为满二叉树(full binary tree)。下图就是一个高度为4的满二叉树。

    假设对高度为h的满二叉树中的元素从上到下,从左到右的顺序进行从1到2h12h−1进行编号,如上图所示。假设从满二叉树中删除k个元素,其编号为2hi,1ik2h−i,1≤i≤k,所得到的二叉树称为 完全二叉树(complete binary tree) 。如下图给出的三棵完全二叉树。注意, 满二叉树是完全二叉树的一个特例,并且有n个元素的完全二叉树的深度是log2(n+1)⌈log2(n+1)⌉ 。

    在完全二叉树中,一个元素与其孩子的编号有非常好的对应关系。其关系在下面特性4中给出。

    特性4: 设完全二叉树中一元素的序号是i,1in1≤i≤n。则有以下关系成立:

    1) 当i=1时,该元素为二叉树的根,若i>1,则该元素父节点的编号是i/2⌊i/2⌋。

    2) 当2i>n时,该元素没有左子树,否则,其左子树的编号是2i。

    3) 若2i+1>n时,该元素没有右子树,否则,其右子树的编号是2i+1。

    二叉树描述

    公式化描述

    二叉树的公式化描述利用特性4。二叉树可以作为缺少了部分元素的完全二叉树。下图给出了二叉树的两个例子。

    在公式化描述方法中,按照二叉树对元素的编号方法,将二叉树的元素存储在数组中。上图同时给出了二叉树的公式化描述,即图中右侧的数组表示。

    当缺少很多元素时,这种描述方法非常浪费空间。实际上,一个有n个元素的二叉树可能最多需要2n12n−1的空间来存储。当每个节点都是其他节点的右孩子时,存储空间达到最大。如下图所示的一棵有四个元素的二叉树,这种类型的二叉树称为右斜二叉树。当缺少的元素比较少时,这种描述方法很有效。

    链表描述

    二叉树最常用的描述方法是用链表或指针。每个元素都用一个有两个指针域的节点表示,这两个域是 LeftChild和RightChild 。除此两个指针域外,每个节点还有一个data域。其代码实现如下所示。

    template<class T>
    class BinaryTreeNode{
        friend void Visit(BinaryTreeNode<T> *);
        friend void InOrder(BinaryTreeNode<T> *);
        friend void PreOrder(BinaryTreeNode<T> *);
        friend void PostOrder(BinaryTreeNode<T> *);
        friend void LevelOrder(BinaryTreeNode<T> *);
        friend void main(void);
    private:
        T data;
        BinaryTreeNode<T>* LeftChild, *RightChild;
    public:
        BinaryTreeNode(){
            LeftChild = RightChild = 0;
        }
        BinaryTreeNode(const T& e){
            data = e;
            LeftChild = RightChild = 0;
        }
        BinaryTreeNode(const T&e, BinaryTreeNode *l, BinaryTreeNode* r){
            data = e;
            LeftChild = l;
            RightChild = r;
        }
    };
    

    二叉树的边可以用一个从父节点到子节点的指针来描述。指针放在父节点的指针域中,因为包括n个元素的二叉树恰有n-1条边,所以有 2n-(n-1)=n+1 个指针域没有值,这些域被值为0。下图给出了公式化描述中第一幅图的二叉树的链表描述。

    二叉树中不设置指向父节点的指针一般不会有什么问题,因为在二叉树的大部分函数中并不需要此指针。

    二叉树遍历

    有四种遍历二叉树的方法:

    • 前序遍历
    • 中序遍历
    • 后序遍历
    • 逐层遍历

    前3种遍历方法将在下面给出代码实现。

    template<class T>
    void PreOrder(BinaryTreeNode<T>* t){
        if (t){
            // 访问根节点
            Visit(t);
            // 前序遍历左子树
            PreOrder(t->LeftChild);
            // 前序遍历右子树
            PreOrder(t->RightChild);
        }
    }
    
    template<class T>
    void InOrder(BinaryTreeNode<T>* t){
        if (t){
            // 中序遍历左子树
            InOrder(t->LeftChild);
            Visit(t);
            InOrder(t->RightChild);
        }
    }
    
    template<class T>
    void PostOrder(BinaryTreeNode<T>* t){
        if (t){
            // 后序遍历左子树
            PostOrder(t->LeftChild);
            PostOrder(t->RightChild);
            Visit(t);
        }
    }
    

    这三种方法,每个节点的左子树都在其右子树之前遍历。 这三种遍历的区别在于对同一个节点在不同时刻进行访问。 在进行前序遍历时,每个节点是在其左右子树被访问之前进行访问的;在中序遍历时,首先访问左子树,然后访问子树的根节点,最后访问右子树。在后序遍历时,当左右子树均访问完之后才访问子树的根节点。

    下图给出上述三种方法对前文给出的数学表达式分别产生的结果,其中 Visit(t)由 cout<< t->data; 代替。

    当对一棵数学表达式树进行前、中、后序遍历时,便分别得到表达式的前缀、中缀和后缀表达式。中缀(infix)形式就是平时书写的数学表达式。使用中缀形式的时候由于没有括号,可能会产生一些歧义,比如对于 x+y*z ,可以理解为 (x+y)*z 或者x+(y*z) ,为了避免这种歧义,可以使用完全括号化的中缀表达式,每个操作符和相应的操作数都用一对括号括起来。下面是改进后的中序遍历算法的代码:

    template<class T>
    void Infix(BinaryTreeNode<T> *t){
        // 输出完全括号的中缀表达式
        if (t){
            cout << "(";
            // 左操作数
            Infix(t->LeftChild);
            // 操作符
            cout << t->data;
            // 右操作数
            Infix(t->RightChild);
            cout << ")";
        }
    }

    在后缀(postfix)表达式中,每个操作符跟在操作数之后,操作数从左到右的顺序出现;在前缀(prefix)表达式中,操作符位于操作数之前。前缀和后缀表达式都不会存在歧义,不需要采用括号或者优先级。从左到右或者从右到左扫描表达式并采用操作数栈,可以很容易确定操作数和操作符的关系。若在扫描中遇到一个操作数,把它压入堆栈,遇到一个操作符,则将其与栈顶的操作数相匹配,把这些操作数推出栈,由操作符执行相应的计算,并将所得结果作为操作数压入堆栈。

    逐层遍历就是 按从顶层到底层的次序访问树中元素,在同一层中,从左到右进行访问。 由于遍历中使用的是一个队列而不是栈,因此写一个按层遍历的递归程序很困难。下列程序是采用队列来实现对二叉树进行逐层遍历,队列中的元素指向二叉树节点,这里使用了之前队列章节中使用的类 LinkedQueue 。

    template<class T>
    void LevelOrder(BinaryTreeNode<T>* t){
        // 对*t逐层遍历
        LinkedQueue<BinaryTreeNode<T>*>  Q;
        while (t){
            Visit(t);
            // 将t的右孩子放入队列
            if (t->LeftChild)
                Q.Add(t->LeftChild);
            if (t->RightChild)
                Q.Add(t->RightChild);
            // 访问下一个节点
            try{
                Q.Delete(t);
            }
            catch (OutOfBounds){
                return;
            }
        }
    }
    

    上述程序中,首先仅当树非空时,才进入 while 循环。首先访问根节点,然后将其子节点加到队列中。当队列添加操作失败时,由Add会引发 NoMem 异常,由于没有捕捉该异常,所以发生该异常时函数将退出。在添加操作成功后,就进行从队列中删除t元素,如果成功,则删除的元素会返回到t中,这个删除的元素也就是下一个要访问的节点。下次访问该节点的时候,又会将其左右子树加入到队列的尾部,然后下一个要访问的就是根节点的右子树(如果存在),如此就可以实现逐层遍历了。而如果删除失败就表明队列为空,也就是意味着遍历的结束。

    假设二叉树中元素的数目是 n 。这四种遍历算法的空间复杂性均为O(n)O(n),时间复杂性是θ(n)θ(n)。当t的高度是n的时候,通过观察期前序、中序和后序遍历时所使用的递归栈空间可得到上述结论。当t是满二叉树的时候,逐层遍历所需要的队列空间是θ(n)θ(n)。每个遍历算法花在树中每个节点上的时间是θ(1)θ(1)(假设访问一个节点的时间是θ(1)θ(1))。

    抽象数据类型BinaryTree

    下面给出二叉树的抽象数据类型,这里只列出几个常用的操作:

    抽象数据类型 BinaryTree{
    实例
        元素集合;如果不空,则被划分为根节点、左子树和右子树;
        每个子树仍是一个二叉树
    操作
        Create():创建一个空的二叉树;
        IsEmpty:如果二叉树为空,则返回 true ,否则返回false
        Root(x):取x为根节点;如果操作失败,则返回false,否则返回true
        MakeTree(root,left,right):创建一个二叉树,root作为根节点,left作为左子树, right作为右子树
        BreakTree(root,left,right):拆分二叉树
        PreOrder:前序遍历
        InOrder:中序遍历
        PostOrder:后序遍历
        LevelOrder:逐层遍历
    }

    类BinaryTree

    下面给出类 BinaryTree 的C++定义。函数 Visit 作为遍历函数的参数,以实现不同操作的实现。该定义中使用了链表描述的二叉树。

    template<class T>
    class BinaryTree{
    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);
    public:
        BinaryTree(){ root = 0; }
        ~BinaryTree(){};
        bool IsEmpty() const{
            return ((root) ? false : true);
        }
        bool Root(T& x)const;
        void MakeTree(const T& element, BinaryTree<T>& left, BinaryTree<T>& right);
        void BreakTree(const 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));
    
    };
    

    下面会给出共享成员函数 Root,MakeTree,BreakTree 的代码。函数 MakeTree和 BreakTree 要求参与操作的三棵树应该互不相同,否则程序会得出错误的结果。

    template<class T>
    bool BinaryTree<T>::Root(T& x)const{
        // 取根节点的data域,放入x,如果没有则返回false
        if (root){
            x = root->data;
            return true;
        }
    else
        return false;
    }
    
    template<class T>
    void BinaryTree<T>::MakeTree(const T& element, BinaryTree<T>& left, BinaryTree<T>& right){
        // 将left,right和element合并成一棵新树,并且要求left和right及this必须是不同的树。
    
        // 创建新树
        root = new BinaryTreeNode<T>(element, left.root, right.root);
    
        // 阻止访问left和right
        left.root = right.root = 0;
    }
    
    template<class T>
    void BinaryTree<T>::BreakTree(T& element, BinaryTree<T>& left, BinaryTree<T>& right){
        // left,right 和this必须是不同的树
        if (!root)
            // 空树
            throw BadInput();
    
        // 分解树
        element = root->data;
        left.root = root->LeftChild;
        right.root = root->RightChild;
    
        delete root;
        root = 0;
    }
    

    下面给出四种遍历方法的实现代码。

    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);
        }
    }
    
    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);
        }
    }
    
    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);
        }
    }
    
    template<class T>
    void BinaryTree<T>::LevelOrder(void(*Visit)(BinaryTreeNode<T>* u)){
        // 逐层遍历
        LinkedQueue<BinaryTreeNode<T>*>Q;
        BinaryTreeNode<T> *t;
        t = root;
        while (t){
            Visit(t);
            // 将t的右孩子放入队列
            if (t->LeftChild)
                Q.Add(t->LeftChild);
            if (t->RightChild)
                Q.Add(t->RightChild);
            // 访问下一个节点
            try{
                Q.Delete(t);
            }
            catch (OutOfBounds){
                return;
            }
        }
    }
    

    接下来是对类BinaryTree的简单应用,程序中构造了一个四节点的二叉树,并进行了前序遍历以确定书中的节点数目。

    #include<iostream>
    #include"xcept.h"
    #include"BinaryTree.h"
    
    using std::cout;
    using std::endl;
    using std::cin;
    
    int count = 0;
    BinaryTree<int>a, x, y, z;
    
    template<class T>
    void ct(BinaryTreeNode<T>* t){
        count++;
    }
    
    void main(void){
        y.MakeTree(1, a, a);
        z.MakeTree(2, a, a);
        x.MakeTree(3, y, z);
        y.MakeTree(4, x, a);
        y.PreOrder(ct);
        cout <<"Tree y has "<< count <<" nodes"<< endl;
    
        system("pause");
        return;
    }
    

    抽象数据类型及类的扩充

    本节将扩充之前给出的抽象数据类型,增加如下二叉树操作:

    • PreOutput(): 按前序方式输出数据域
    • InOutput(): 按中序方式输出数据域
    • PostOutput(): 按后序方式输出数据域
    • LevelOutput(): 逐层输出数据域
    • Delete(): 删除一棵二叉树,释放其节点
    • Height(): 返回树的高度
    • Size(): 返回树中节点数

    输出

    四个输出函数可以通过定义一个私有静态成员函数Output来实现,该函数代码如下:

    static void Output(BinaryTreeNode<T>*t){
        cout << t->data << ", ";
    }

    而四个共享输出函数的形式如下:

    void PreOutput(){
        PreOrder(Output, root);
        cout << endl;
    }
    void InOutput(){
        Inorder(Output, root);
        cout << endl;
    }
    void PostOutput(){
        PostOrder(Output, root);
        cout << endl;
    }
    void PreOutput(){
        LevelOrder(Output);
        cout << endl;
    }
    

    由于Visit操作的时间复杂性是θ(1)θ(1),对包括n个节点的二叉树来说,每种遍历方法所花费的时间是θ(n)θ(n)(遍历成功的话),因此每种输出方法的时间复杂性均为θ(n)θ(n)。

    删除

    要删除一棵二叉树,需要删除其所有节点,可以通过 后序遍历 在访问一个节点时,将其删除,也就是先删除左子树,然后右子树,最后删除根。因此函数 Delete 的形式如下所示:

    void Delete(){
        PostOrder(Free, root);
        root = 0;
    }
    static void Free(BinaryTreeNode<T>* t){
        delete t;
    }
    

    其中函数 Free 是一个私有成员函数。要删除的二叉树有n个节点时, Delete 函数的时间复杂性是θ(n)θ(n)。

    计算高度

    通过进行后序遍历,可以得到二叉树的高度。首先得到左子树的高度hl,然后得到右子树的高度hr,则树的高度为 max{hl,hr}+1 。

    但是这里不能使用之前定义的后序遍历代码,因为在进行遍历的时候需要有返回值(也就是子树的高度)。所以首先需要在增加一个共享成员函数Height,其代码为:

    int Height() const{
        return Height(root);
    }
    

    然后增加一个私有成员函数 Height ,其实现如下:

    template<class T>
    int BinaryTree<T>::Height(BinaryTreeNode<T> *t)const{
        // 返回树*t的高度
        if (!t)
            return 0;
        // 左子树高度
        int hl = Height(t->LeftChild);
        // 右子树的高度
        int hr = Height(t->RightChild);
        if (hl > hr)
            return ++hl;
        else
            return ++hr;
    }
    

    上述函数的时间复杂性是θ(n)θ(n)。

    统计节点数

    可以用上述四种遍历方法中的任何一种来获取二叉树中的节点数,因为每种遍历方法都对每个节点仅访问一次,只要在访问每个节点的时候将一个全局计数器加1即可。所以首先在类 BinaryTree 定义外定义一个全局变量: int _count; ,然后增加一个共享成员函数 Size 和私有成员函数 Add1 ,其代码实现如下:

    static void Add1(BinaryTreeNode<T>* t){
        _count++;
    }
    
    int Size(){
        _count = 0;
        PreOrder(Add1, root);
        return _count;
    }
    

    函数 Size 的时间复杂性是θ(n)θ(n)。

    对于类 BinaryTree 的定义以及测试例子可以查看 二叉树

    小结

    本节内容主要是介绍了树的基本概念以及二叉树的定义、特性和实现代码,包括四种遍历树的方法。

  • 相关阅读:
    Samba 4.0 RC3 发布
    SymmetricDS 3.1.7 发布,数据同步和复制
    Express.js 3.0 发布,Node.js 的高性能封装
    GIFLIB 5.0.1 发布,C语言的GIF处理库
    jQuery UI 1.9.1 发布
    SVN Access Manager 0.5.5.14 发布 SVN 管理工具
    DynamicReports 3.0.3 发布 Java 报表工具
    HttpComponents HttpClient 4.2.2 GA 发布
    AppCan 2.0 正式发布,推移动应用云服务
    Ruby 2.0 的新功能已经冻结
  • 原文地址:https://www.cnblogs.com/zhehan54/p/5785920.html
Copyright © 2011-2022 走看看