1.概念简述

:是n>=0个结点的有限集。空树。
非空树:有且仅有一个根节点,n>1时,其余结点可分为m个互不相交,其中每一个集合本身又是一棵树,且成为子树。
:结点拥有的子树数。
叶结点或终端结点:度为0
非终端结点或者分支结点:度不为0
树的度是树内各结点的度的最大值。
结点间的关系:双亲,孩子,兄弟,祖先。
其他概念:结点的层次从根开始定义,根为第一层,根的孩子为第二层。
互为堂兄弟:双亲在同一层的结点。
树的深度或高度:树中结点的最大层次
有序树:若该树中结点的各子树看成从左向右是有次序的,不能互换的。否则为无序树。
森林是m>=0颗互不相交的树的集合。
本节内容主要介绍二叉树

2.二叉树

定义:对于开关、01、正反等,都适合用树状结构来建模。二叉树是n>=0个结点的有限集合,该集合可为空集,或由一个根节点和两颗互不相交的,分别称为根节点的左子树和右子树的二叉树组成。
特点
每个结点最多有两颗子树,二叉树不存在度>2的结点。
左子树和右子树有顺序的,次序不能颠倒。注意区分左子树和右子树。
五种基本状态:空。只有一个根节点。只有左。只有右。既有左,又有右。
特殊二叉树:斜树(左斜树和右斜树)。线性表其实是树的一种特殊形式。
满二叉树:(完美二叉树)特点
叶结点只能在最下层。
非叶结点的度一定是2。
同样深度的二叉树中,满二叉树的结点个数最多,叶子树最多。
完全二叉树:对一颗具有n个结点的二叉树按层序编号,如果编号为i的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同。
完全二叉树的特点
叶子结点只能在最下两层。
最下层的叶子一定集中于左部连续位置。
倒数二层,若有叶子结点,一定都在右部连续位置。
同样结点的二叉树,完全二叉树的深度最小。完全二叉树举例如下:
完全二叉树举例:

二叉树的性质

二叉树的性质

3.二叉树的描述

3.1 数组描述
数组表示中,二叉树的元素按照其编号存储在数组的相应位置,当缺少元素很多时,这种表示法浪费空间,当二叉树的编号从0开始时,最坏情况的空间需求是2^n-1;
3.2 链表描述
二叉树最常用的表示方法是指针,每个元素用1个节点表示,节点有两个指针域,分别称为leftChild和rightChild.除两个指针域外,每个节点还有一个element域。链表二叉树的节点结构代码如下

//链表二叉树的节点结构
template<class T>
struct binaryTreeNode
{
    T element;
    binaryTreeNode<T> * leftChild,//左子树
                        * rightChild;//右子树
    binaryTreeNode(){leftChild=rightChild=NULL;}
    binaryTreeNode(const T & theElement )
    {
        element(theElement);
        leftChild=rightChild=NULL;
    }
    binaryTreeNode(const T& theElement,binaryTreeNode * theLeftNode,binaryTreeNode * theRightNode)
    {
        element(theElement);
        leftChild=theLeftNode;
        rightChild=theRightNode;
    }   
};

父节点的每一个指向孩子的指针表示一条边,n个元素的二叉树仅有n-1条边,所以有n+1个指针域没有值,被置为NULL.从根节点开始,沿着leftChild和rightChild指针域,可以访问二叉树的所有节点。
3.3 二叉树常用操作
确定高度
确定元素数目
复制
显示或打印二叉树
确定两颗二叉树是否一样
删除整颗树
这些操作可以通过有步骤地遍历二叉树来完成,在二叉树的遍历中,每个元素仅被遍历一次,访问一个元素,意味着可以对该元素实施任何操作.
3.4 二叉树的建立
为了能让每个节点确认是否有左右孩子,对它进行扩展,将二叉树中每个节点的空指针引出一个虚节点,其值为一特定值,假设为”#”,此处建立二叉树利用了递归的原理,此处使用前序遍历的算法建立二叉树,当然也可以使用中序或者后序建立二叉树.
代码示例如下

//二叉树的建立
template<class T>
void createBinaryTree(binaryTreeNode <T>* t)
{
    T ch;
    std::cin>>ch;
    if (ch=='#')
        t=NULL;
    else
    {
        t=(binaryTreeNode)new(sizeof(binaryTreeNode));
        if (!t)
            exit(OVERFLOW);
        t->element=ch;//生成根节点
        createBinaryTree(t->leftChild);
        createBinaryTree((t->rightChild));
    }
}

3.5 二叉树遍历
先给出visit函数的代码如下

//visit函数
template<class T>
void visit(binaryTreeNode <T> * x)
{
    //访问节点*x,仅输出element域
    cout<<x->element<<' ';
}

有四种遍历二叉树的常用算法

//二叉树的遍历
//前序遍历
template<class T>
void preOrder(binaryTreeNode <T> * t)
{
    //前序遍历二叉树*t
    if (t!=NULL)
    {
    visit(t);//访问树根
    preOrder(t->leftChild);//前序遍历左子树
    preOrder(t->rightChild);//前序遍历右子树
    }
}
//中序遍历
template<class T>
void inOrder(binaryTreeNode<T>*t)
{
    //中序遍历二叉树*t
    if (t!=NULL)
    {
        inOrder(t->leftChild);
        visit(t);
        inOrder(t->rightChild);
    }
}
//后序遍历
template<class T>
void postOrder(binaryTreeNode <T>* t)
{
//后序遍历二叉树*t
    if (t!=NULL)
    {
        postOrder(t->leftChild);
        postOrder(t->rightChild);
        visit(t);
    }
}
//层序遍历
    template<class T>
    void levelOrder(binaryTreeNode<T>* t)
    {
        //层序遍历二叉树*t
        arrayQueue<binaryTreeNode<T>*> q;
        while(t!=NULL)
        {
            visit(t);
            //将t的孩子插入队列
            if (t->leftChild!=NULL)
                q.push(t->leftChild);
            if (t->rightChild!=NULL)
                q.push(t->rightChild);
            //提取下一个要访问的节点
            try{t=q.front();}
            catch(queueEmpty){retrun ;}
            q.pop();            
        }
    }

前三种遍历的区别在于对每个节点的访问时间不同。
层序遍历中,仅当树非空时,才进入while循环,进入while循环之后,首先访问根节点,再把其子节点(如果有),加入到队列中,然后访问队首元素。若队列为空,则front()抛出类型为queueEmpty()的异常,若队列不为空,则front()的返回值指向队首元素指针,下一次循环将访问这个元素.
二叉树遍历的性质
已知前序遍历和中序,可唯一确定一颗二叉树
已知后序遍历和中序,可唯一确定一颗二叉树
已知前序和后序遍历,不能确定一颗二叉树
3.6 二叉树的抽象数据类型ADT
二叉树抽象类的代码如下

//二叉树抽象类
    template<class T>
    class binaryTree
    {
    public:
        virtual ~binaryTree(){}
        virtual bool empty()const=0;
        virtual int size()const=0;
        virtual void preOrder(void(*)(T*))=0;
        virtual void inOrder(void(*)(T*))=0;
        virtual void postOrder(void(*)(T*))=0;
    };
//确定二叉树的高度
    template<class T>
    int height(binaryTreeNode<T>*t)
    {
        if (t==NULL)
            return 0;
        int hl=height(t->leftChild);
        int hr=height(t->rightChild);
        if (hl>hr)
            return ++hl;
        else
            return ++hr;
    }

3.7 线索二叉树
父节点的每一个指向孩子的指针表示一条边,n个元素的二叉树仅有n-1条边,所以有n+1个空指针域.
遍历之后,知道了节点的前驱和后继,但是在二叉链表中只能知道每个节点指向其左右孩子节点的地址,不知道某个节点的前驱和后继,要想知道,必须遍历一次。
将其空地址利用起来,存放指向节点在某种遍历下的前驱和后继节点的地址.把指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就是线索二叉树.
注意:此时我们不知道某一节点的lchild是指向它的孩子域还是指向前驱?所以,需要增设2个标志,ltag和rtag.
ltag为0时,指向孩子,为1时,指向前驱.,rtage类似.

//二叉树的二叉线索存储结构定义:
typedef enum{Link,Thread} PointerTag;
template<class T>
struct binaryThreadingTreeNode
{
    T element;
    binaryTreeNode * leftChild,
                   * rightChild;
    PointerTag LTag,
               RTag;
};

线索化的实质就是将二叉链表中的空指针改为指向前驱或者后继的线索,
前驱和后继的信息只有在遍历该二叉树时才能得到,所以线索化的过程就是在遍历的过程中修改空指针的过程.
中序遍历线索化的递归函数的代码如下:

//中序遍历线索化的递归函数如下:
template<class T>
binaryThreadingTreeNode<T>* pre;//全局变量
template<class T>
void InThreading(binaryThreadingTreeNode<T>* p)
{
    if (p)
    {
        InThreading(p->leftChild);//递归左子树线索化
        //关键代码段
        if (!p->leftChild)//没有左孩子
        {
            p->LTag=Thread;//前驱线索
            p->leftChild=pre;//左孩子指针指向前驱
        }
        if (!pre->rightChild)//前驱没有右孩子
        {
            pre->LTag=Thread;
            pre->leftChild=p;
        }
        pre=p;//保持pre指向p的前驱
        //关键代码段截止
    }
    InThreading(p->rightChild);
}

3.8 树、二叉树、森林之间的转换
3.8.1 树→二叉树
1. 加线。在所有的兄弟节点之间加线
2. 去线。对树中每个节点,只保留它与第一个孩子节点的连线,删除其他.
3. 层次调整。以树的根节点为轴心,使结构分明。
3.8.2 森林→二叉树
森林是由若干颗树组成的,可理解为森林中的每一颗树都是兄弟,可按照处理兄弟的处理方法来操作。
1. 将每个树转换为二叉树.
2. 第一颗树不动,从第二颗二叉树开始,依次把一颗二叉树的根节点作为前一颗二叉树的根节点的右孩子,用线连接起来。
3.8.3 二叉树→树
1. 加线。若某节点的左孩子节点存在,将这个左孩子的右孩子节点、右孩子的右孩子节点、…….就是左孩子的n个右孩子节点都作为此结点的孩子.将该结点与这些右孩子节点用线连接起来.
2. 去线。删除原二叉树中所有节点与其右孩子节点的连线.
3. 层次调整.使结构分明.
3.8.4 二叉树→森林
判断一颗二叉树能转换成森林还是一颗树,就是看这颗二叉树的根节点有没有右孩子.有就是森林,没有就是一颗树
如果能转换为森林,步骤如下:
1. 从根节点开始,若右孩子存在,则把与右孩子节点的连线删除,再查看分离后的二叉树,若右孩子存在,则连线删除……,
2. 再将每颗分离的二叉树转换为树即可.
3.9 树与森林的遍历
3.9.1树的遍历分两种
1.先根遍历树,即先访问树的根节点,然后依次先根遍历根的每颗子树。
2.后根遍历,即先依次后根遍历每颗子树,然后再访问根节点.
3.9.2根的遍历分两种
1.前序遍历:先访问森林中第一棵树的根节点,然后依次先根遍历根的每颗子树,再依次用同样方式遍历除去第一颗树构成的森林.
2.后序遍历:先访问森林中第一颗树,后根遍历方式遍历每颗子树.再访问根节点,依次同样方式遍历除去第一棵树的剩余树构成的森林.
结论
树的先根和后根遍历完全可以借用二叉树的前序遍历和中序遍历的算法来实现.

4.赫夫曼树简介

4.1概念 简述
路径长度:树中每一个节点到另一个节点之间的分支构成两个节点之间的路径,路径上的分支数目称做路径长度.
树的路径长度就是从树根都每一节点的路径长度之和.
带权路径长度为树中所有叶子节点的带权路径长度之和.
带权路径长度WPL最小的二叉树称为赫夫曼树或者 称为最优二叉树.
4.2赫夫曼算法描述
1.根据给定的n个权值{w1,w2,w3,wn}构成n颗二叉树的集合F={T1,T2,T3,T4};
其中每颗二叉树Ti中只有一个带权为wi的根节点,左右子树为空.
2.F中选取两颗根节点的权值最小的树作为左右子树构造一颗新的二叉树,且置新的二叉树的根节点的权值为其左右树上根节点的权值之和.
3.F中删除这两棵树,同时将新得到的二叉树加入F中.
4.重复步骤2和3.直到F中只含一棵树.
4.2赫夫曼编码
起初是用二进制编码来表示数据.但非最优解,构造赫夫曼树后,将权值左右分支写为0和1来编码,可减少占用空间,但是新的编码如何解释出来.非0即1,若要设计长短不等的编码,必须是是任一字符的编码都不是另一个字符的编码的前缀,这种编码称为前缀编码.接收方和发送方必须约定好同样的赫夫曼编码.
定义:一般地,设需要编码的字符集为{d1,d2,d3,d4,dn},各个字符在电文中出现的次数或集合为{w1,w2,w3,…},以d1,d2,d3,d4为叶子节点,以w1,w2,w3…作为响应的叶子节点的权值来构造一颗赫夫曼树。左分支为0,右分支为1,则从根节点到叶子节点所经过的路径分支组成的0和1的序列便为该节点对应字符的编码,即为赫夫曼编码.
以后有相关的内容会继续补充上来,希望大家批评指正,谢谢

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值