本笔记是听郝斌老师视频所做

树的定义

在树中常常将数据元素称为结点。树是n个结点的有限集合。当n=0的时候,称为空树;任意一颗非空树满足以下条件:

  • 有且仅有一个特定的称为根的结点(root)。
  • 当n>1的时候,除了根节点外其他的结点被分割为m个互不相交的有限集合,我们称之为子树

专业定义:

  1. 有且仅有一个称为根的结点。
  2. 有若干个互不相交的子树,这些子树本身也是一棵树。

通俗的定义:

  1. 树是由结点和边组成的。
  2. 每个结点只有一个父结点但可以有多个子结点。
  3. 但有一个结点例外,该结点没有父结点,此结点称为根结点。

专业术语

  1. 深度:从根结点到最底层结点的层数称为深度。根结点是第一层。
  2. 叶子结点(终端结点):没有子结点的结点,或者说是度为0的结点。
  3. 非终端结点(分支结点):实际上就是非叶子结点,或者说是度不为0的结点。

    • 结点的度:某结点所拥有的子树的个数称为该结点的度(二叉树的结点的度小于等于2,非二叉树的可以大于2)。
    • 树的度:树中各个结点度的最大值称为该树的度。
  4. 有序树:如果一棵树中结点的各个子树从左到右是次序的,即若交换了结点各子树的相对位置,则构成了不同的树,称这棵树为有序树。
  5. 无序树:如果一棵树中结点的各个子树从左到右是次序的,即若交换了结点各子树的相对位置,还是同一颗树,称这棵树为无序树。
  6. 孩子结点:某结点的子树的根节点称为该结点的孩子结点(children node),反之,该结点称为器孩子结点的双亲结点(parent node)。
  7. 双亲结点:见7。
  8. 路径:如果树的结点序列n1,n2,n3,n4…nk满足如下关系:结点ni是结点ni+1的双亲的话,则把n1,n2,n3,n4…nk称为一条由n1到nk的路径(path)。
  9. 路径长度:路径上经过的变数称为路径长度。
  10. 结点的层数:规定根节点的层数为1(备注:当然,在有些材料中定义根节点的层数为0)。
  11. 树的深度(高度):树中所有结点的最大层数称为树的深度,也称为树的高度。

树分类

  1. 一般树:任意一个结点的子结点的个数都不受限制
  2. 二叉树:任意一个几点的子结点数最多为两个,且子结点的位置不可更改(有序树)
    • 一般二叉树:
    • 满二叉树:在不增加树的层数的前提下,无法再多添加一个结点的二叉树就是满二叉树。
  3. 完全二叉树:如果只是删除了满二叉树最底层最右边连续若干个结点,这样就形成的二叉树就是完全二叉树
    完全二叉树
  4. 二叉树的5种形态:
    二叉树的5种状态
  5. 森林:m(m≥0)棵互不相交的树的集合。自然界中的树和森林的概念差别很大,但在数据结构中树和森林的概念差别很小。从定义可知,一棵树有根结点和m个子树构成,若把树的根结点删除,则树变成了包含m棵树的森林。当然,根据定义,一棵树也可以称为森林。

一般树的存储:

由于树中各个结点的度的变化范围较大,给存储结构带来了复杂。有几种基本的存储方法。

双亲表示法

根据树的定义可以知道,树中每个结点有且仅有一个双亲结点。根据这个特性,可以用一维数组来存储树中的各个结点(一般按照层序存储),数组的一个元素对应树中的一个结点,数组元素包括树中结点的数据信息以及该结点的双亲在数组中下标。本质上是一个静态链表

定义

template<class DataType>
struct PNode {// 数组元素的类型
    DataType data;// 树中结点的数据信息
    int parent;// 该结点的双亲在数组中的下标
}

缺点

这种方法不能放映各个兄弟结点之间的关系,所以在实际应用中,常常根据需要,对这种存储方法进行修改。

孩子表示法

树的孩子表示法是一中基于链表的存储方法,主要有两种形式。

多重链表表示法

由于树中每个结点可能有多个孩子,因此,链表中的每个结点包括一个数据域和多个指针域,每个指针域指向该结点的一个孩子结点。由于树中各个结点的度不同,因此指针域的设置有两种方法。

  • 指针域的个数等于该结点的度
    • 虽然在一定程度上节约了存储空间,但由于链表中各个结点是不同构的,树的各种操作不容易实现,所以这种方法很少采用。
  • 指针域的个数等于树的度
    • 链表中各结点是同构的,各种操作相对容易实现,但为此付出的代价是存储空间的浪费。

孩子链表表示法

这是一种用多个单链表表示树的方法,即把每个结点的孩子排列起来,看成是一个线性表,且以单链表存储,称为该结点的孩子链表。这n个结点共有n个孩子链表(叶子结点的孩子链表为空表)。

定义

这n个单链表公有n个头指针,这n个头指针又构成了一个线性表,为了便于进行查找操作,可以采用顺序存储。最后,将存放n个头指针的数组和存放n个结点数据信息的数组结合起来,构成孩子链表的表头数组,所以在孩子链表表表示法中,存在两类结点:孩子结点表头结点

data为数据域,存放该结点的数据信息;
firstchild为指针域,存储该结点的第一个孩子结点的存储地址。

// 孩子结点
struct CTNode {// 孩子结点
    int child;
    CTNode *next;
};
// 表头结点
template <class DataType>
struct CBNode {
    DataType data;
    CTNode *firstchild;// 指向孩子链表的头指针
};
优点

孩子链表表示法不仅表示了孩子结点的信息,而且链在同一个单链表中的结点具有兄弟关系。

缺点

与双亲表示法相反的是,在孩子链表表示法中查找双亲比较困难。

双亲孩子表示法

双亲孩子表示法是将双亲表示法和孩子链表表示法相结合的存储方法。

优点

求父结点和子结点都很方便

孩子兄弟表示法

又称为二叉链表表示法,其方法是链表中的每个结点除了数据域外,还设置两个指针分别指向该节点的第一个孩子和右兄弟。

定义

data为数据域,存储该结点的数据信息;
firstchild为指针域,存储该结点的第一个孩子结点的存储地址;
rightsib为指针域,存储该结点的右兄弟结点的存储地址。

template <class DataType>
struct TNode {  
    DataType data;
    TNode<DataType> *firstchild, *rightsib;
}

优点

这种存储方法便于实现树的各种操作。

森林的存储

先把森林转化为二叉树,再存储二叉树

二叉树

介绍二叉树的遍历和一些基本知识点

二叉树的定义

二叉树是n(n≥0)个结点的有限集合,该集合或者空集(称为空二叉树),或者由一个根节点和两颗互不相交的、分别称为根节点的左子树和右子树的二叉树组成。

二叉树的特点

  1. 每个结点最多有两颗子树,所以二叉树中不存在度大于2的结点
  2. 二叉树是有序的,其次序不能颠倒

二叉树的五种基本形态

  1. 空二叉树
  2. 只有一个根节点
  3. 根节点只有左子树
  4. 根节点只有右子树
  5. 根节点既有右子树又有左子树

特殊二叉树

在实际应用中,有几种特殊的二叉树:

斜树

所有结点只有左子树的二叉树称为左斜树;所有结点只有右子树的二叉树称为右斜树;两者统称为斜树。

满二叉树

如果一颗二叉树的所有的分支结点都存在左子树和右子树,并且所有的叶子都在同一层上,那么这样的二叉树称为满二叉树。

特点:
1. 叶子结点都在最下面那一层
2. 只存在度为0或者为2的结点

完全二叉树

对一颗具有n个结点的二叉树按照层序编号,如果编号为i的结点与同样深度的满二叉树中的编号为i的结点在二叉树中的位置是完全相同的,则这颗二叉树称为完全二叉树。

特点:
1. 叶子结点只能出现在最下面的两层,且最下层的叶子结点集中在二叉树左侧连续的位置;
2. 如果有度为1的结点,只可能有一个,且该结点只有左孩子。

二叉树表示法

  1. 把一个普通树转化成二叉树来存储
  2. 具体转换方法:
    1. 设法保证任意一个结点的左指针域指向它的第一个孩子,右指针域指向他的堂兄弟
    2. 只要能满足此条件,就可以把一个普通树转化为二叉树
    3. 一个普通树转化成的二叉树一定没有右子树

二叉树的存储:

连续存储【完全二叉树】

  1. 优点:查找某个结点的父结点和子节点(也包括判断是否存在)比较方便。
  2. 缺点:好用但是占用内存空间过大,最坏的情况就是右斜树(但是在目前而言,空间的成本要远远低于时间的成本,所以我们一般是使用空间来换时间
  3. 备注:二叉树的顺序存储结构一般仅仅适用于存储完全二叉树。
  4. 设满二叉树节点在数组中的索引为i,那么有如下性质:
    (1)如果i=0,此节点为根节点,无双亲。
    (2)如果i>0,则其双亲为(i-1)/2。(结果取整)
    (3)结点i的左孩子为2i+1,右孩子为2i+2.
    (4)当i>0时,当i为奇数时,它是双亲节点的左孩子,它的兄弟为i+1;当i为偶数时,它是双亲节点的右孩子,它的兄弟为i-1.
    (5)深度为K的满二叉树需要长度为2k-1的数组存储。

二叉链表

二叉树一般采用二叉链表存储。

C++的模板机制:
data为数据域,存放该结点的数据信息;
lchild是左指针域,存放指向左孩子的指针,当左孩子不存在的时候为空指针;
rchild是左指针域,存放指向右孩子的指针,当右孩子不存在的时候为空指针;

template <class DataType>
struct BiNode {
    DataType data;
    BiNode<DataType> *lchild,*rchild;
}

备注: 具体实现请看下面源码。

三叉链表

三叉链表是在二叉链表的基础上增加了一个parent指针,该指针指向该结点的双亲结点。

C++的模板机制:
data为数据域,存放该结点的数据信息;
lchild是左指针域,存放指向左孩子的指针,当左孩子不存在的时候为空指针;
rchild是左指针域,存放指向右孩子的指针,当右孩子不存在的时候为空指针;
parent是双亲结点指针域,存放指向双亲结点的指针,当双亲结点不存在的时候为空指针。

template <class DataType>
struct BiNode {
    DataType data;
    BiNode<DataType> *lchild,*rchild,*parent;
}

线索链表

按照某种遍历次序对二叉树进行遍历,可以把二叉树中的所有结点排成一个线性序列。在具体的应用中,有时需要访问二叉树中的结点在某种遍历序列中的前序和后继,此时,在存储结构中应该保存结点在某种遍历序列中的前驱后继信息。

由于在一个具有n个结点的二叉链表中,在2n个指针域中只有n-1个指针域用来存储孩子结点的地址,存在n+1个空指针域,可以利用这些空的指针域存放指向该结点在某种遍历序列中的前驱和后继结点的指针。这些指向前驱和后继结点的指针称为线索,加上线索的二叉树称为线索二叉树,相应的,加上线索的二叉链表称为线索链表

为了实现线索二叉树,我们需要立个flag来判断是否有孩子结点,假如没有孩子结点的话,flag = 1,这是指针域指向的是该结点在某种遍历序列中的前驱和后继;反之,当flag = 0的时候,也就是说存在孩子结点的时候,该结点的指针域存放的就是指向孩子的指针。

C++的模板机制:
ltag和rtag是判断该结点的左指针域和有指针域是否为空的判断标志;
lchild是左指针域,存放指向左孩子的指针,当左孩子不存在的时候为空指针;
rchild是左指针域,存放指向右孩子的指针,当右孩子不存在的时候为空指针。

enum flag {Child,Thread};
template <class DataType>
struct ThrNode {    
    DataType data;
    ThrNode<DataType> *lchild,*rchild;
    flag ltag,rtag;
};

二叉树的性质

  • 性质1: 一棵非空二叉树的第i层上最多有2^(i-1)个结点(i≥1)。
  • 性质2: 若规定空树的深度为0,则深度为k的二叉树最多有2k-1个结点(k≥0),最少有k个结点。
    • 空树指的是结点数为0的树。
    • 注意:不一定是斜树
  • 性质3: 具有n个结点的完全二叉树的深度k为⌊log2n+1⌋
    • 备注:⌊log2n+1⌋表示(log2n+1)向下取整的值。
  • 性质4: 对于一棵非空二叉树,如果度为0的结点(叶子结点)数目为n0,度为2的结点数目为n2,则有n0= n2+1。
  • 性质5: 对于具有n个结点的完全二叉树,如果按照从上到下和从左到右的顺序对所有结点从1开始编号,则对于序号为i的结点,有:
    • 如果i>1,则序号为i的结点的双亲结点的序号为⌊i/2⌋;如果i=1,则该结点是根结点,无双亲结点。
    • 如果2i≤n,则该结点的左孩子结点的序号为2i;若2i>n,则该结点无左孩子。
    • 如果2i+1≤n,则该结点的右孩子结点的序号为2i+1;若2i+1>n,则该结点无右孩子。

二叉树的代码实现

# include <iostream>
// 判断类型
# include <typeinfo>
# include <cstdlib>
// 队列
# include <queue>
using namespace std;

// 结点
template <class T>
struct BiNode
{
    T data;
    BiNode<T> *lchild, *rchild;
};

// 二叉树类
template <class T>
class BiTree
{
public:
    BiTree(){root=Creat(root);}
    ~BiTree(){Release(root);}
    void PreOrder(){PreOrder(root);}
    void InOrder(){InOrder(root);}
    void PostOrder(){PostOrder(root);}
    void LeverOrder() {LeverOrder(root);}
    int CountLeaf1() {return CountLeaf1(root);}
    int CountLeaf2() {return CountLeaf2(root);}
private:
    // 二叉树的根节点
    BiNode<T> *root;
    // 初始化二叉树
    BiNode<T> *Creat(BiNode<T> *bt);
    // 释放空间
    void Release(BiNode<T> *bt);
    // 前序遍历
    void PreOrder(BiNode<T> *bt);
    // 中序遍历
    void InOrder(BiNode<T> *bt);
    // 后序遍历
    void PostOrder(BiNode<T> *bt);
    // 层序遍历
    void LeverOrder(BiNode<T> *bt);
    // 求叶子节点数
    void CountLeaf(BiNode<T> *bt,int &count);
    int CountLeaf1(BiNode<T> *bt);
    int CountLeaf2(BiNode<T> *bt);
};

template <class T>
BiNode<T> *BiTree<T>::Creat(BiNode<T> *bt) {
    T ch;
    int flag = 0;
    cout<<"请输入创建一颗二叉树的结点数据"<<endl;
    cin>>ch;
    if (typeid(ch) == typeid(char)) { // 判断类型
        flag = 0;
        cout<<"char"<<endl;
    } else {
        flag = 1;
        cout<<"int"<<endl;
    }
    if (flag == 0) {
        if (ch == '#') { // “#”表示是空
            return NULL;
        }else {
            bt = new BiNode<T>;// 生成一个结点
            bt->data = ch;
            bt->lchild = Creat(bt->lchild);
            bt->rchild = Creat(bt->rchild);
        }
    }
    if (flag == 1) {
        if (ch == 0) {
            return NULL;
        } else {
            bt = new BiNode<T>;// 生成一个结点
            bt->data = ch;
            bt->lchild = Creat(bt->lchild);
            bt->rchild = Creat(bt->rchild);
        }
    }

    return bt;
}

// 释放空间
template <class T>
void BiTree<T>::Release(BiNode<T> *bt) {
    if (bt != NULL) {
        Release(bt->lchild);
        Release(bt->rchild);
        delete bt;
    }
}

// 前序
template <class T>
void BiTree<T>::PreOrder(BiNode<T> *bt) {
    if (bt==NULL) {
        return ; // 递归结束的条件
    } else {
        cout<<bt->data<<" "; // 访问根节点bt的数据域
        PreOrder(bt->lchild); // 前序遍历bt的左子树
        PreOrder(bt->rchild); // 前序遍历bt的右子树
    }
}

// 中序
template <class T>
void BiTree<T>::InOrder(BiNode<T> *bt) {
    if (bt==NULL) {
        return ; // 递归结束的条件
    } else {
        PreOrder(bt->lchild); // 中序遍历bt的左子树
        cout<<bt->data<<" "; // 访问根节点bt的数据域
        PreOrder(bt->rchild); // 中序遍历bt的右子树
    }
}

// 后序
template <class T>
void BiTree<T>::PostOrder(BiNode<T> *bt) {
    if (bt==NULL) {
        return ; // 递归结束的条件
    } else {
        PreOrder(bt->lchild); // 后序遍历bt的左子树
        PreOrder(bt->rchild); // 后序遍历bt的右子树
        cout<<bt->data<<" "; // 访问根节点bt的数据域
    }
}

// 层序
template <class T>
void BiTree<T>::LeverOrder (BiNode<T> *bt)
{
    if(root == NULL) {
        return;
    }
    queue<BiNode<T> *> q;// 队列
    BiNode<T> *node(NULL);

    q.push(root); // 首先将根节点入队
    while(!q.empty()) // 当队伍q中不为空的时候,继续循环
    {
        node = q.front(); // 获取队头元素的值
        q.pop(); // 移除队头元素
        cout << node->data << " "; // 输出队头元素的数据域
        if(node->lchild) { // 若结点q存在左孩子,则将左孩子指针入队
            q.push(node->lchild);
        }
        if(node->rchild) { // 若结点q存在右孩子,则将右孩子指针入队
            q.push(node->rchild);
        }
    }
    cout << endl;
}

template <class T>
void BiTree<T>::CountLeaf(BiNode<T> *bt,int &count) {
    if (bt != NULL) {
        if (bt->lchild==NULL && bt->rchild==NULL) {
                count++;
        }
        CountLeaf(bt->lchild,count);
        CountLeaf(bt->rchild,count);
    }
}

// 递归求二叉树叶子结点个数算法
template <class T>
int BiTree<T>::CountLeaf1(BiNode<T> *bt)
{
    int count = 0;
    if (bt != NULL) {
        if (bt->lchild==NULL && bt->rchild==NULL) {
            count++;
        }
        CountLeaf(bt->lchild,count);
        CountLeaf(bt->rchild,count);
    }
    return count;
}

// 非递归求二叉树叶子结点个数算法
template <class T>
int BiTree<T>::CountLeaf2(BiNode<T> *bt)
{
    int top = -1;
    BiNode<T>* s[66];
    int count = 0;
    while (bt!= NULL || top != -1) {
        while (bt != NULL) {
            if (bt->lchild ==NULL && bt->rchild == NULL) {
                count++;
            }
            s[++top] = bt;
            bt = bt->lchild;
        }
        if (top!=-1) {
            bt = s[top--];
            bt = bt->rchild;
        }
    }
    return count;
}


int main()
{
    BiTree<int> T;
    system("cls");

    cout<<"-------前序遍历-------"<<endl;
    T.PreOrder();
    cout<<endl;

    cout<<"-------中序遍历-------"<<endl;
    T.InOrder();
    cout<<endl;

    cout<<"------后序遍历-------"<<endl;
    T.PostOrder();
    cout<<endl;

    cout<<"------层序遍历-------"<<endl;
    T.LeverOrder();
    cout<<endl;

    cout<<"-----求叶子数-递归---"<<endl;
    cout<<T.CountLeaf1();
    cout<<endl;

    cout<<"-----求叶子数-非递归-"<<endl;
    cout<<T.CountLeaf2();
    cout<<endl;

    return 0;
}
/*
-------前序遍历-------
q w e
-------中序遍历-------
w e q
------后序遍历-------
w e q
------层序遍历-------
q w e

-----求叶子数-递归---
1
-----求叶子数-非递归-
1

Process returned 0 (0x0)   execution time : 8.764 s
Press any key to continue.
*/

二叉树的应用

树的遍历

树中最基本的操作就是遍历。树的遍历是指从根节点出发,按照某种次序访问书中的所有结点,使得每个结点都被访问一次且被访问一次。

先序遍历

  • 先序遍历【根左右】
    • 先访问根节点
    • 在先序访问左子树
    • 再先序访问右子树
    • 先序遍历示意图

中序遍历

备注:中序遍历只有二叉树才有。

  • 中序遍历【左根右】
    • 中序遍历左子树
    • 在访问根节点
    • 再中序遍历右子树
    • 中序遍历示例图
    • 中序遍历示例图-1

后序遍历

  • 后序遍历【左右根】
    • 后序遍历左子树
    • 后序遍历右子树
    • 再访问根节点
    • 后序便利
    • 后序遍历-1

层序遍历

树的层序遍历也称为树的广度遍历,其操作定义为从树的第一层(即根节点)开始,自上而下逐层遍历,在同一层中,按照从左到右的数序对结点逐个访问。

求原二叉树:

已知两种遍历序列求原始的二叉树:
通过先序和中序或者中序和后序我们可以还原出原始的二叉树,但是通过先序和后序是无法还原出原始的二叉树的。

已知先序和中序求后序

先序:ABCDEFGH
中序:BDCEAFHG
求后序:

注意:前序遍历是依据“根左右”,中序遍历是依据“左根右”来遍历的。

  • 由先序第一个“A”可知,根结点是A
  • 结合中序的“BDCEAFHG”可以得出“BDCE”位于根节点A的左结点,“FHG”位于根节点A的右结点
  • 再由先序“BCDE”和中序“BDCE”可以得知,B是“CDE”的父结点,“CDE”和“DCE”可以直接推出C是“DE”父节点并且D是左结点,E是右结点。
  • “FGH”和“FHG”易知,F是“GH”的父结点并且G是H的父结点,F是G的父节点
  • 最后,得出后序为:DECBHGFA
  • 已知前序和中序求后序

已知中序和后序求先序

中序:BDCEAFHG
后序:DECBHGFA
求先序:

注意:中序遍历是依据“左根右”,后序遍历是依据“左右根”来遍历的。

  • 后序里面最后出现的就是根结点,所以A一定是根节点
    • 所以“BDCE”是A的左子树,“FHG”是A的右子树
  • 所以在中序“BDCE”中,依据后序中的“DECB”可以得知”B”是根节点,同理F也是根节点
  • ……
  • 先序为:ABCDEFGH
  • 已知中序和后序求先序

应用

  • 树是数据库中数据组织的一中重要形式
  • 操作系统中子父进程的关系本身也是一棵树
  • 面向对象语言中类的继承关系本身也是一颗树
  • 赫夫曼树

附录


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值