【算法与数据结构】:二叉树的特点、性质、分类

200 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第21天,点击查看活动详情

1、写在前面

大家好,我是翼同学。今天文章的内容是:

  • 二叉树

2、内容

2.1、简介

(1) 概念

二叉树的定义如下:

二叉树:由 n 的结点组成的有限集合。该集合或者为空,或者由一个根结点以及两颗互不相交的左、右子树构成,而左右子树又都是二叉树。

因此,二叉树是一种重要的树结构。在二叉树中,每个结点都最多有两个子结点。

(2) 特点

  1. 二叉树可以是空的,也就是说空二叉树不含任何结点;
  2. 二叉树的每个结点最多有两个子结点(度小于或等于2),分别称为该结点的左孩子和右孩子;
  3. 二叉树的子树有左右之分,即使只有一个子树,也必须说明是左子树还是右子树(交换一个二叉树的左右子树后得到的是另一颗二叉树);
  4. 度为2的有序树并不是二叉树。因为在有序树中,删除某个度为2的结点的第一子树后,第二子树就会代替第一子树。但是在二叉树中,如果删除了某结点的左子树,则左子树为空,右子树仍然是右子树。

(3) 分类

满二叉树

如果在一个二叉树中,任意层次的结点个数都达到了最大值,所有分支结点都存在左右子树,并且所有叶结点都在同一层次上,我们将这样的二叉树称为满二叉树。

满二叉树的示意图如下:

image.png

满二叉树的特点有:

  • 满二叉树只有度为0的结点和度为2的结点。
  • 度为0的结点(叶结点)只能出现在最底层。
  • 在同样深度的二叉树中,满二叉树的结点个数最多,叶结点也最多。

完全二叉树

如果在一个二叉树中,只有最底下的两层结点的度可以小于2,并且最下面的结点都集中在该层的最左边的连续位置上。就称这个二叉树为完全二叉树。

完全二叉树的示意图如下:

image.png

完全二叉树的特点:

  • 叶结点只会出现在层次最大的两层上;
  • 任意一个结点,如果没有左子树,那么一定没有右子树;
  • 满二叉树一定是完全二叉树,而完全二叉树不一定是满二叉树。

总结来说,对于深度为k,有n个结点的完全二叉树,除第k层外,其余各层次的结点个数都达到最大,而第k层的所有结点都集中在该层的最左边的连续位置上。

举个两个例子:

image.png

其他二叉树

  • 正则二叉树:如果一棵树的任意结点不是叶结点就是有两颗非空子树,则将这棵树称为正则二叉树。也就是说,正则二叉树不存在度为1的结点,因此有时正则二叉树也被称为严格二叉树。
  • 扩充二叉树:在原先二叉树中出现空子树的位置上,增加空的叶结点所形成的二叉树被我们称为扩充二叉树。
  • 二叉搜索树:二叉搜索树的特点是,如果根结点的左子树不空,则左子树上所有结点的值均小于根结点的值;如果它的右子树不空,则右子树上所有结点的值均大于根结点的值;并且,每个结点的左右子树又分别是二叉搜索树。

(4) 性质

  1. 深度为 kk 的二叉树中最多有 2k12^k-1 个结点
  2. 非空二叉树的第 ii 层上最多有 2i12^{i-1} 个结点(也就是说,如果第 ii 层已经达到了 2i12^{i-1} 个结点,那么该树必定是满二叉树)
  3. 在任意二叉树中,如果叶结点的个数为 n0n_0 ,度为2的结点个数为 n2n_2,则 n0=n2+1n_0=n_2+1
  4. 扩充二叉树中新增的外部结点的个数等于原二叉树的结点个数加一
  5. 具有 nn 个结点的完全二叉树的深度为 log(n+1)log(n+1)

另外,如果对一颗有nn个结点的完全二叉树按层次自上而下(每层从左到右)对结点进行编号(从11nn),则对任意结点ii而言:

  • 如果结点的编号 ii 等于 11 ,则结点 ii 为根结点(没有父结点)
  • 如果 ii 大于 11 ,则该结点 ii 的父节点编号为 i/2i/2
  • 如果 2i2i 小于或等于 nn ,则 ii 的左孩子的编号为 2i2i,否则 ii 无左孩子
  • 如果 2i+12i+1 小于或等于 nn ,则 ii 的右孩子的编号为 2i+12i+1,否则 ii 无右孩子。

(5) 抽象数据类型定义

二叉树的抽象数据类型定义如下:

// 二叉树的抽象数据类型定义
template <class T> 	
class ADTbinaryTree {
public: 	            			
    virtual void clear() = 0;		            // 清空
    virtual bool empty() const = 0;                 // 判空
    virtual int height() const = 0;	            // 高度
    virtual int size() const = 0;	            // 结点总数
    virtual void preOrderTraverse() const = 0;      // 前序遍历
    virtual void inOrderTraverse() const = 0;	    // 中序遍历
    virtual void postOrderTraverse() const = 0;	    // 后序遍历
    virtual void levelOrderTraverse() const = 0;    // 层次遍历
    virtual ~ADTbinaryTree() { };                   // 析构
};

2.2、二叉树实现

(1) 顺序存储结构

对于二叉树而言,如果是完全二叉树,我们可以采用顺序存储结构来实现二叉树。但如果是一般二叉树,通常我们会选择链式存储结构来存储二叉树,为了不造成存储空间上的浪费。

简单介绍下二叉树的顺序存储结构:

二叉树的顺序存储结构就是一组地址连续的存储单元依次从上而下,从左到右地存储二叉树的结点,并且在存储结点时,每个结点的的存储位置(数组下标)可以体现结点之间的逻辑关系。也就是利用到了二叉树的性质。

这段话怎么理解呢?

回顾性质

如果对一颗有nn个结点的完全二叉树按层次自上而下(每层从左到右)对结点进行编号(从11nn),则对任意结点ii而言:

  • 如果结点的编号 ii 等于 11 ,则结点 ii 为根结点(没有父结点)
  • 如果 ii 大于 11 ,则该结点 ii 的父节点编号为 i/2i/2
  • 如果 2i2i 小于或等于 nn ,则 ii 的左孩子的编号为 2i2i,否则 ii 无左孩子
  • 如果 2i+12i+1 小于或等于 nn ,则 ii 的右孩子的编号为 2i+12i+1,否则 ii 无右孩子。

画一个完全二叉树如下:

image.png

为了体现结点之间的逻辑关系,利用二叉树编号的性质,用顺序存储结构来表示该二叉树的示意图如下:

image.png

在上图完全二叉树中有9个结点,即n=9n=9

此时举个例子:编号ii为4的结点为DD,又因为2in2i≤n,即898≤9,所以结点DD的左孩子的编号为 2i=82i=8(也就是HH),可以观察到,上图中结点DD的左孩子确实是HH

像这样,从上而下,从左到右地将二叉树的结点存储下来,并利用二叉树性质,就可以轻松地体现二叉树各个结点之间的逻辑关系。这就是二叉树的顺序存储结构。

(2) 链式存储结构

当我们采取链式存储结构来存储二叉树时,每个结点除了存储数据元素本身的值data外,还需要设置两个指针,分别用于指向左、右子树。当任意结点的某个子树为空时,对应的指针就可以设置为NULL。如下所示:

image.png

另外,我们会设置一个指针root,用于指向二叉树的根结点。

示意图如下:

image.png

因此类定义如下:

template <class T>
class BinaryTree : public ADTbinaryTree<T> {
private:
    // 二叉链表的结点
	struct Node{
		T data;         // 结点的数据域
		Node *left;     // 指向左孩子的指针
		Node *right;    // 指向右孩子的指针
        // 无参构造函数
		Node() : left(NULL), right(NULL) { }
        // 有参构造函数
		Node(const T &val, Node *l=NULL, Node *r=NULL) {
            data = val;
            left = l;
            right = r;
        }
        ~Node() { }
    }
    Node *root;                             // 指向根结点的指针 root
    void clear( Node *t);		    // 清空函数
    int height( Node *t) const;	            // 二叉树的高度
    int size( Node *t) const;               // 二叉树的结点总数
    int leafNum( Node *t) const;	    // 二叉树的叶节点总数 
    void preOrder( Node *t) const;          // 前序遍历
    void inOrder( Node *t) const;	    // 中序遍历
    void postOrder( Node *t) const;	    // 后序遍历
    void preOrderCreate(T flag, Node* &t);  // 创建二叉树

public:
    // 构造空的二叉树
    BinaryTree() : root(NULL) {}

    // 析构函数
    ~BinaryTree() { clear(); }

    // 判断二叉树是否为空
    bool empty() const {
        return root == NULL;
    }

    // 清空函数
    void clear() {
        if(root) clear(root);
        root = NULL;
    }

    // 求结点的总数
    int size() const {
        return size(root);
    }

    // 求二叉树的深度
    int heigh() const {
        return heigh(root);
    }

    // 求二叉树的结点总数
    int leafNum() const {
        return leafNum(root);
    }

    // 前序遍历(递归)
    void preOrderTraverse() const {
        if(root) {
            preOrder(root);
        }
    }

    // 中序遍历(递归)
    void inOrderTraverse() const {
        if(root) {
            inOrder(root);
        }
    }

    // 后序遍历(递归)
    void postOrderTraverse() const {
        if(root) {
            postOrder(root);
        }
    }

    // 层次遍历
    void levelOrderTraverse();

    // 前序遍历(非递归)
    void preOrderTraverse() const;

    // 中序遍历(非递归)
    void inOrderTraverse() const;

    // 后序遍历(非递归)
    void postOrderTraverse() const;

    // 利用带外部结点的层次序列来创建二叉树
    void levelOrderCreate(T flag);

    // 利用带外部结点的前序序列来创建二叉树
    void preOrderCreate(T flag) {
        preOrderCreate(flag, root);
    }
};

3、写在最后

好了,文章的内容就到这里,感谢观看。