2020数据结构--树

一、树

1.树的基本概念

1.1树的定义

树是n(n>=0)个结点的集合,n=0时,称为空树,这是一种特殊情况。在任意一棵非空树中应该满足:

  • 有且仅有一个特定的称为根的结点。
  • 当n>1时,其余结点可分为m(m>0)个互不相交的有限集合T1,T2…Tm其中每个集合本身又是一棵树,称为根结点的子树。

树的定义是递归的,是一种递归的数据结构。树作为一种逻辑结构,同时也是一种分层结构,具有以下两个特点:

  • 树的根结点没有前驱结点,除根结点外的所有结点有且只有一个前驱结点。
  • 树中所有结点可以有零个或者多个结点。

树适合表示具有层次结构的数据,树中的某个结点(除根结点外)最多只能和上一层的一个结点(即其父结点)有直接关系,n个结点的树中有n-1个边。

1.2 基本术语
  • 根A到结点K的唯一路径上的任意结点,称为K的祖先结点,K称为A的子孙结点。路径上最接近结点K的结点E称为K的双亲结点,K为其孩子结点。树根A为树中唯一没有双亲的结点,有相同双亲结点的结点称为兄弟结点。
  • 树中的一个结点的子结点个数称为该结点的度,树中结点的最大度数称为树的
  • 度大于0的结点称为分支结点(又称为非终端结点);度为0的结点称为叶子结点(又称终端结点)。在分支结点中,每个结点的分支数就是该结点的度。
  • 结点的层次从树根开始定义,根结点为第一层,子结点为第二层,以此类推。
  • 结点的深度从根结点开始自顶向下逐层累加的。
  • 结点的高度从叶节点开始自底向上逐层累加的。
  • 树的高度(深度)是树结点中的最大层数。
  • 树中结点的子树从左到右的是有次序的,不能交换,称为有序树,反之称为无序树。
  • 树中两个结点之间的路径是由这两个结点之间所经过的结点序列构成的;路径长度是路径所经过的边的个数。
    注:树中的的路径是从上而下的,同一双亲结点的两个孩子结点之间不存在路径。
  • 森林是m(m>=0)棵互不相交的树的集合。把树的根结点删去就成了森林;反之只要给n个独立的树加上一个结点,并把这n个树作为该结点的子树,森林就变成了树。

2.二叉树

2.1定义及主要特性
  • 每个结点之多有两个子树(不存在度大于2的结点),有左右子树之分,次序不能颠倒。
  • 二叉树也是递归定义的,二叉树是有序树,就算只有一个结点也需要区分左右子树。

二叉树和度为2的有序树的区别

  • 度为2的树至少有3个结点,二叉树可以为空。
  • 度为2的有序树的孩子结点的左右次序是相对于另一个孩子而言的,若只有一个孩子结点,则无需区分左右次序,二叉树则相反。
2.2几个特殊的二叉树
  • 满二叉树:**高度为h,并且由2{h} –1个结点的二叉树,被称为满二叉树。**即树中的每层都含有最多的结点;满二叉树的叶子结点都集中在二叉树的最下一层,除叶子结点之外的每个结点的度数均为2.
    可对其进行编号,从根结点开始(根结点编号为1),自上而下,自左向右。对于编号为i的结点,若有双亲,其双亲为i/2(向下取整),所有左孩子,则为2i;若有右孩子,则为2i+1
    在这里插入图片描述
  • 完全二叉树:一棵二叉树中,只有最下面两层结点的度可以小于2,并且最下一层的叶结点集中在靠左的若干位置上。这样的二叉树称为完全二叉树。
  • 特点:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。显然,一棵满二叉树必定是一棵完全二叉树,而完全二叉树未必是满二叉树。
    在这里插入图片描述
  • 二叉排序树:(或称二叉查找树)设x为二叉查找树中的一个结点,x节点包含关键字key,节点x的key值记为key[x]。如果y是x的左子树中的一个结点,则key[y] <= key[x];如果y是x的右子树的一个结点,则key[y] >= key[x]。
    在这里插入图片描述
  • 平衡二叉树:树上任一结点左子树和右子树的深度之差不超过1。
2.3二叉树的性质
  • 在二叉树的第iii层上至多有2^{i-1}个节点。
  • 深度为kkk的二叉树上至多含有2^k-1个节点。(至少有k个节点)
    满二叉树第kkk层的节点个数比其第 1 ~ k-1层所有的节点多1个。
    而第k层的子树有2^{k-1}个。
  • 对任何一个二叉树,若他含有n0个叶子节点,n2个度为2的节点,则存在关系式:n0=n2+1。
  • 具有n个结点的完全二叉树的深度为(向上取整)log2(n+1),或[(向下取整)log2n]+1。
2.4二叉树的存储结构
  • 顺序存储结构:用一组地址连续的存储单元依次自上而下、自左向右存储完全二叉树上的结点元素,将完全二叉树上编号为i的结点元素存储在某个数组下标为i-1的分量中。
    在这里插入图片描述
  • 分析:由于二叉树不一定都是完全二叉树,所以希望通过标号来访问父节点以及兄弟、子节点的话,则必须为树添加虚节点。从上面的存储结构可以看出这样做的坏处就是会有大量的存储空间被浪费。所以一般二叉树都用链式结构进行存储。
  • 链式存储结构:由于顺序存储的效率太低,故使用二叉链表来表示,。二叉链表至少包括三个域:数据域data、左指针域lchild、右指针域rchild。
    在这里插入图片描述
  • 说明:在一个含有n个结点的二叉树中,二叉链表含有2n个指针域,其中必有n+1个空指针域。

3.二叉树的遍历和线索二叉树

3.1二叉树的遍历

常见的有先序遍历(NLR)、中序遍历(LNR)、后续遍历(LRN)。

  • 先序遍历:(又称为前序遍历),根结点->左子树->右子树。
  • 中序遍历:左子树->根结点->右子树。
  • 后续遍历:左子树->右子树->根结点。
  • 时间复杂度都是O(n),空间复杂度都为O(n)。
  • 由二叉树的先序和中序序列可以唯一的确定一棵二叉树;同理后续和中序也可以唯一地确定一个二叉树;层序和中序也可以唯一确定一个二叉树。
3.2线索二叉树
  • 在二叉链表表示地二叉树中存在大量空指针,若利用这些空链域存放指向其直接前驱或后继地指针,则可方便操作。引入线索二叉树为了加快查找结点前驱和后继地速度。
  • 在二叉树线索化时,若无左子树,令lchild指向其前驱结点;若无右子树,令rchild指向其后继结点,还需增加两个标志域表明当前指针域所指对象是指向左(右)子结点还是直接前驱(后继)。
  • 以这种结点结构构成地二叉链表作为二叉树地存储结构,称为线索链表,其中指向结点前驱和后继地指针称为线索。加上线索的二叉树称为线索二叉树。对二叉树的某种遍历使其成为线索二叉树的过程称为线索化。
3.3线索二叉树的构造
  • 构造中序线索二叉树,具体分为五步:
    1.如果当前节点p飞空,根据中序的规定,应该先对其左字数递归线索化.
    2.如果p的左孩子为空,需要给p加上左线索,也就是他的前驱,并将其LTag(本文中的算法将LTag和RTag命名为lTage和rTage)设置为1,让p的做孩子指向pre(p的前驱,也就是上一个访问的节点),否则就把LTage设置为0.
    3.如果pre的右孩子为空,需要给pre加上右线索(后继),并将RTage设置为1,让pre的右孩子指向p,否则RTage就为0.
    4.迭代pre,pre=p
    5.右字数递归线索化,重复第2至4步.
  • 遍历中序线索二叉树
    在遍历中序线索二叉树之前,应该先了解,怎么在中序线索二叉树中找到节点的前驱和后继
    1.查找当前节点p的前驱:
    当p->LTage为1的时候,p的左孩子直接指向的就是他的前驱.
    当p->LTage为0的时候,p的左孩子指向的是p的左子树的根,p和他的前驱pre之间是没有任何链来连接的.但通过中序遍历的特性可以知道,当前节点p的前驱应该是中序遍历其左子树的最后一个节点.
    例如下图 ,根节点A的具有左子树,根据中序遍历的,该二叉树的中序遍历序列为DBEAC,A的前驱应该是E。而节点C不具有左子树,访问节点C之前,访问的是结点A,
    所以pre=A,C的前驱是A
    2.查找当前节点p的后继:
    当p->RTage为1的时候,p的右孩子直接指向的就是他的后继.
    当p->RTage为0的时候,p的后继是他右子树访问的第一个节点。如上图,A的后继就为C
    在这里插入图片描述
  • 构造前序线索二叉树
    和中序原理一样,这里不做多解释。
  • 遍历前序线索二叉树
    确定先序线索二叉树的前驱和后继
    1.查找p指针所指节点的前驱:
    当p->LTage为1,p的右孩子指的就是其前驱
    当p->LTage为0,p有右孩子,此时根据先序遍历的规律可知,p的前驱有两种情况。当p是双亲的左孩子的时候,他的前驱是双亲节点。否则他的前驱应该是双亲节点的左子树按照先序遍历的最后一个节点。
    这很好理解。如图,当前这个树的先序遍历序列为ABDEC,D没有左子树,他是B的左孩子,根据先序遍历结果,他的前驱为B,这是先序遍历规律所导致的,要知道线索二叉树中线索的创建都是依赖其顺序遍历的结果的。
    2.查找p指针所指结点的后继
    若p->RTag为1,则p的右孩子直接指向后继
    若p->RTage为0,则说明p有右子树,其后继分为两种情况,存在左子树的话就是左子树的根,否则是右子树的根
  • 构造后续线索二叉树,同上类似
  • 遍历后续线索二叉树
    确定后序线索二叉树的前驱和后继
    1.查找p指针的前驱:
    若p->LTag为1,则p的左孩子直接指向前驱
    若p->LTag为0,RTag也为0的时候p的右链指示前驱,当p->LTag为0而RTag为1时p的左链指向前驱.
    根据后序遍历的规律得知,节点p遍历的顺序永远再他的子节点后面,当节点p同时拥有左孩子和右孩子的时候,节点p
    之前的遍历的节点一定是他的左子树的根,所以当LTag和RTag都为0的时候p的右链指向其前驱,同理,当p的RTag为
    1的时候,由于没有了右字树,当p的左孩子的根遍历结束后就直接遍历到了p,所以当LTag=0,RTag=1的时候,p的左链
    为前驱.
    2.查找指针p的后继
    当p->RTag为1的时候,直接指向其后继
    当P->RTag为0的时候有四种情况
    • 若p是二叉树的根,后继为空
    • 若p是双亲的右孩子,则后继为双亲,这个按照后序遍历规则就可以推导出来.
    • 若p是双亲的左孩子,且p没有右兄弟,其后继为双亲节点.
    • 若p是双亲的左孩子,且p有右兄弟,则p的后继为右兄弟上按照后序遍历得出的第一个节点

4.树和森林

4.1树的存储结构

即可用顺序存储也可以用链式存储,主要有以下三种存储方式:双亲表示法、孩子表示法、孩子兄弟表示法。

  • 双亲表示法:我们人可能因为种种原因,没有孩子,但无论是谁都不可能是从石头里蹦出来的,孙悟空显然不能算是人,所以是人一定会有父母。树这种结构也不例外,除了根结点外,其余每个结点,它不一定有孩子,但是一定有且仅有一个双亲。
    我们假设以一组连续空间存储树的结点,同时在每个结点中,附设一个指示器指示其双亲结点到链表中的位置。也就是说,每个结点除了知道自己是谁以外,还知道它的双亲在哪里。
    在这里插入图片描述

    • 其中data是数据域,存储结点的数据信息。而parent是指针域,存储该结点的双亲在数组中的下标。
      由于根结点是没有双亲的,所以我们约定根结点的位置域设置为-1,这也就意味着,我们所有的结点都存有它双亲的位置。如图6-4-1中的树结构和表6-4-2中的树双亲表示所示。
      在这里插入图片描述
    • 这样的存储结构,我们可以根据结点的parent指针很容易找到它的双亲结点,所用的时间复杂度为O(1),直到parent为-1时,表示找到了树结点的根。可如果我们要知道结点的孩子是什么,对不起,请遍历整个结构才行。
      这真是麻烦,能不能改进一下呢?
      当然可以。我们增加一个结点最左边孩子的域,不妨叫它长子域,这样就可以很容易得到结点的孩子。如果没有孩子的结点,这个长子域就设置为-1,如表6-4-3所示。(表中下标为0的firstchild应该为1)
      在这里插入图片描述
    • 对于有0个或1个孩子结点来说,这样的结构是解决了要找结点孩子的问题了。甚至是有2个孩子,知道了长子是谁,另一个当然就是次子了。
      另外一个问题场景,我们很关注各兄弟之间的关系,双亲表示法无法体现这样的关系,那我们怎么办?嗯,可以增加一个右兄弟域来体现兄弟关系,也就是说,每一个结点如果它存在右兄弟,则记录下右兄弟的下标。同样的,如果右兄弟不存在,则赋值为-1。
      在这里插入图片描述
  • 孩子表示法:换一种完全不同的考虑方法 . 由于树中每个结点可能有多棵子树,可以考虑用多重链表,即每个结点有多个指针域,其中每个指针指向一棵子树的根结点,我们把这种方法叫做多重链表表示法。不过,树的每个结点的度,也就是它的孩子个数是不同的。所以可以设计两种方案来解决。

    • 方案一
      一种是指针域的个数就等于树的度,复习一下,树的度是树各个结点度的最大值。
      在这里插入图片描述
      其中data是数据域。childl到childd是指针域,用来指向该结点的孩子结点。
      对于图6-4-1的树来说,树的度是3,所以我们的指针域的个数是3,这种方法实现如图6-4-2所示。
      在这里插入图片描述
      这种方法对于树中各结点的度相差很大时,显然是很浪费空间的,因为有很多的结点,它的指针域都是空的。不过如果树的各结点度相差很小时,那就意味着开辟的空间被充分利用了,这时存储结构的缺点反而变成了优点。
      既然很多指针域都可能为空,为什么不按需分配空间呢。于是我们有了第二种方案。
    • 方案二
      第二种方案每个结点指针域的个数等于该结点的度,我们专门取一个位置来存储结点指针域的个数
      其中data为数据域,degree为度域,也就是存储该结点的孩子结点的个数,child1到childd为指针域,指向该结点的各个孩子的结点。
      在这里插入图片描述
    • 这种方法克服了浪费空间的缺点,对空间利用率是很高了,但是由于各个结点的链表是不相同的结构,加上要维护结点的度的数值,在运算上就会带来时间上的损耗。
      能否有更好的方法,既可以减少空指针的浪费又能使结点结构相同。
      仔细观察,我们为了要遍历整棵树,把每个结点放到一个顺序存储结构的数组中是合理的,但每个结点的孩子有多少是不确定的,所以我们再对每个结点的孩子建立一个单链表体现它们的关系 。
      这就是我们要讲的孩子表示法。具体办法是,把每个结点的孩子结点排列起来,以单链表作存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空。然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中。
      在这里插入图片描述
      在这里插入图片描述
  • 其中data是数据域,存储某结点的数据信息。firstchild是头指针域,存储该结点的孩子链表的头指针。
    这样的结构对于我们要查找某个结点的某个孩子,或者找某个结点的兄弟,只需要查找这个结点的孩子单链表即可。对于遍历整棵树也是很方便的,对头结点的数组循环即可。
    但是,这也存在着问题,我如何知道某个结点的双亲是谁呢?比较麻烦,需要整棵树遍历才行,难道就不可以把双亲表示法和孩子表示法综合一下吗?当然是可以。
    在这里插入图片描述

  • 3.孩子兄弟表示法:刚才我们分别从双亲的角度和从孩子的角度研究树的存储结构,如果我们从树结点的兄弟的角度又会如何呢? 当然,对于树这样的层级结构来说,只研究结点的兄弟是不行的,我们观察后发现,任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。 因此,我们设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟。
    在这里插入图片描述

    • 其中data是数据域,firstchild为指针域,存储该结点的第一个孩子结点的存储地址,rightsib是指针域,存储该结点的右兄弟结点的存储地址。
      在这里插入图片描述
4.2树、森林和二叉树的转换
  • 树转换为二叉树的规则:每个结点左指针指向它的第一个孩子结点,右指针指向它在树中的相邻兄弟结点,可以表示为“左孩子,右兄弟”。由于根结点没有兄弟,故由树转换而得的二叉树没有右子树。
  • (1)加线。在所有兄弟结点之间加一条连线。
    (2)去线。树中的每个结点,只保留它与第一个孩子结点的连线,删除它与其它孩子结点之间的连线。
    (3)层次调整。以树的根节点为轴心,将整棵树顺时针旋转一定角度,使之结构层次分明。(注意第一个孩子是结点的左孩子,兄弟转换过来的孩子是结点的右孩子)
    在这里插入图片描述
  • 森林转换为二叉树:
    (1)把每棵树转换为二叉树。
    (2)第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。
    在这里插入图片描述
  • 二叉树转换树:是树转换为二叉树的逆过程。
    (1)加线。若某结点X的左孩子结点存在,则将这个左孩子的右孩子结点、右孩子的右孩子结点、右孩子的右孩子的右孩子结点…,都作为结点X的孩子。将结点X与这些右孩子结点用线连接起来。
    (2)去线。删除原二叉树中所有结点与其右孩子结点的连线。
    (3)层次调整。
    在这里插入图片描述
  • 二叉树转换为森林:假如一棵二叉树的根节点有右孩子,则这棵二叉树能够转换为森林,否则将转换为一棵树。
    (1)从根节点开始,若右孩子存在,则把与右孩子结点的连线删除。再查看分离后的二叉树,若其根节点的右孩子存在,则连线删除…。直到所有这些根节点与右孩子的连线都删除为止。
    (2)将每棵分离后的二叉树转换为树。
    在这里插入图片描述
4.3树和森林的遍历

树的遍历操作主要有先根遍历和后根遍历。

  • 先根遍历:若树非空,则先访问根结点,再按照从左到右的顺序遍历根结点的每一棵子树。这个访问顺序与这棵树对应的二叉树的先序遍历顺序相同。
  • 后根遍历:若树非空,则按照从左到右的顺序遍历根结点的每一棵子树,之后再访问根结点。其访问顺序与这棵树对应的二叉树的中序遍历顺序相同。
    在这里插入图片描述
    在这里插入图片描述

森林的遍历

  • 前序遍历:
    (1)访问森林中第一棵树的根结点;
    (2)前序遍历第一棵树的根结点的子树;
    (3)前序遍历去掉第一棵树后的子森林。

  • 中序遍历:
    (1)中序遍历第一棵树的根结点的子树;
    (2)访问森林中第一棵树的根结点;
    (3)中序遍历去掉第一棵树后的子森林。
    在这里插入图片描述
    在这里插入图片描述

4.4并查集

王道课本。

5.树与二叉树的应用

5.1二叉排序树
  • 插入新结点
    假设我们要为数组 a[] = {10 , 5 , 15 , 6 , 4 , 16 }构建一个二叉排序树,我们按顺序逐个插入元素。
    在这里插入图片描述

    • 插入的过程
      如果是空树,则创建一个新节点,新节点作为根,因此以元素10构建的节点为该二叉查找树的根。
      插入5,5比10小,与10的左孩子节点进行比较,10的左孩子节点为空,进行插入。
      插入15,15比10大,与10的右孩子节点进行比较,10的右孩子节点为空,进行插入。
      插入6,6比10小,与10的左孩子节点5比较;6比5大,与5的右孩子节点进行比较,5的右孩子为空,进行插入。
      插入4,4比10小,与10的左孩子节点5比较;4比5小,与5的左孩子节点进行比较,5的左孩子为空,进行插入。
      插入16,16比10大,与10的右孩子节点15比较;16比15大,与15的右孩子节点进行比较,15的右孩子为空,进行插入。
  • 删除结点,有以下三种情况:
    1.被删除节点同时有左子树与右子树。
    2.被删除节点只有左子树或只有右子树。
    3.被删除节点没有子树。
    对于第一种情况,我们的处理方式是将前驱节点的值保存在当前结点,继而删除前驱节点。
    对于第二种情况,我们直接用子树替换被删节点。
    对于第三种情况,我们可以直接删除节点。
    在这里插入图片描述

  • 查找:我们可以递归或非递归地进行元素的查找。元素的查找过程与元素的插入过程一致,也是在不断地与当前结点进行比较,若值比当前节点的值大,则在右子树进行查找,若值比当前节点的值小,则在左子树进行查找,可以看到这是一个很适合递归操作的过程。而由于二叉排序树这种左小右大的节点特征,也很容易进行非递归查找。
    二叉排序树的最小值位于其最左节点上;最大值位于其最右节点上。

5.2平衡二叉树
  • 一棵AVL必须具备以下条件:
    1.必须是一个二叉排序树
    2.每个结点的左子树和右子树的高度差至多为1.

如:图一中左边二叉树的节点45的左孩子46比45大,不满足二叉搜索树的条件,因此它也不是一棵平衡二叉树。
右边二叉树满足二叉搜索树的条件,同时它满足条件二,因此它是一棵平衡二叉树。
在这里插入图片描述
如:左边二叉树的节点45左子树高度2,右子树高度0,左右子树高度差为2-0=2,不满足条件二;
右边二叉树的节点均满足左右子树高度差至多为1,同时它满足二叉搜索树的要求,因此它是一棵平衡二叉树。
在这里插入图片描述

  • 相关概念

  • 平衡因子:将二叉树上节点的左子树高度减去右子树高度的值称为该节点的平衡因子BF(Balance Factor)。
    对于平衡二叉树,BF的取值范围为[-1,1]。如果发现某个节点的BF值不在此范围,则需要对树进行调整。

  • AVL失衡调整
    节点的插入或删除都有可能导致AVL树失去平衡,因此,失衡调整是插入与删除操作的基础。
    AVL树的失衡调整可以分为四种情况,我们逐一分析。
    假设我们要为数组a[]={4,5,6,3,2,8,7,0,1}构建一棵AVL树。
    情况一:左单旋转
    首先插入{4,5,6},在插入元素6后出现不平衡的情况:
    在这里插入图片描述
    当我们在右子树插入右孩子导致AVL失衡时,我们需要进行单左旋调整。旋转围绕最小失衡子树的根节点进行。
    在删除新节点时也有可能会出现需要单左旋的情况。
    情况二:右单旋转
    我们继续插入元素{3,2},此时二叉树为:
    在这里插入图片描述
    插入3、2后出现了不平衡的情况。此时的插入情况是“在左子树上插入左孩子导致AVL树失衡”,我们需要进行单右旋调整。
    情况三:先左旋后右旋
    需要进行两次旋转的原因是第一次旋转后,AVL树仍旧处于不平衡的状态,第二次旋转再次进行调整。
    我们继续插入元素{8,7}
    在这里插入图片描述
    这种情况,总结起来就是“在右子树上插入左孩子导致AVL树失衡”,此时我们需要进行先右旋后左旋的调整。
    情况四:先左旋后右旋
    根据对称性原理,当我们“在左子树上插入右孩子导致AVL树失衡”,此时我们需要进行先左旋后右旋的调整。如果你不理解接着看图。
    我们接着插入节点{0,1}
    在这里插入图片描述

  • 以上是一个完整的插入过程,可以仔细体会理解。

  • 删除:删除节点也可能导致AVL树的失衡,实际上删除节点和插入节点是一种互逆的操作:
    删除右子树的节点导致AVL树失衡时,相当于在左子树插入节点导致AVL树失衡,即情况情况二或情况四。
    删除左子树的节点导致AVL树失衡时,相当于在右子树插入节点导致AVL树失衡,即情况情况一或情况三。
    另外,AVL树也是一棵二叉排序树,因此在删除节点时也要维护二叉排序树的性质。
    删除节点时,如果节点同时拥有左子树和右子树,则在高度教低的子树上选择最大(或最小)元素进行替换,这样能保证替换后不会再出现失衡的现象。

  • 查找和遍历可以参考二叉排序树。

5.3哈夫曼树
  • 哈夫曼编码:哈夫曼编码(Huffman Coding)是一种编码方式,也称为“赫夫曼编码”,是David A. Huffman1952年发明的一种构建极小多余编码的方法。
    在计算机数据处理中,赫夫曼编码使用变长编码表对源符号进行编码,出现频率较高的源符号采用较短的编码,出现频率较低的符号采用较长的编码,使编码之后的字符串字符串的平均长度 、期望值降低,以达到无损压缩数据的目的。

  • 举例:现在有一个字符串this is an example of a huffman tree;
    这串字符串有36个字符,如果按普通方式存储这串字符串,每个字符占据1个字节,则共需要36 * 1 * 8 = 288bit。
    经过分析我们发现,这串字符串中各字母出现的频率不同,如果我们能够按如下编码:
    在这里插入图片描述
    编码这串字符串,只需要:
    (7+4+4)x3 + (3+2+2+2+2+2+2)x4 + (1+1+1+1+1+1)x 5 = 45+60+30 = 135bit
    编码这串字符串只需要135bit!单单这串字符串,就压缩了288-135 = 153bit。
    那么,我们如何获取每个字符串的编码呢?这就需要哈夫曼树了。
    源字符编码的长短取决于其出现的频率,我们把源字符出现的频率定义为该字符的权值。

  • 哈夫曼树:哈夫曼又称最优二叉树。是一种带权路径长度最短的二叉树。它的定义如下。

  • 定义:假设有n个权值{w1,w2,w3,w4…,wn},构造一棵有n个节点的二叉树,若树的带权路径最小,则这颗树称作哈夫曼树。这里面涉及到几个概念,我们由一棵哈夫曼树来解释.
    在这里插入图片描述

  • 路径与路径长度:从树中一个节点到另一个节点之间的分支构成了两个节点之间的路径,路径上的分支数目称作路径长度。若规定根节点位于第一层,则根节点到第H层的节点的路径长度为H-1.如树b:100到60 的路径长度为1;100到30的路径长度为2;100到20的路径长度为3。

  • 树的路径长度:从根节点到每一节点的路径长度之和。树a的路径长度为1+1+2+2+2+2 = 10;树b的路径长度为1+1+2+2+3+3 = 12.

  • 节点的权:将树中的节点赋予一个某种含义的数值作为该节点的权值,该值称为节点的权;

  • 带权路径长度:从根节点到某个节点之间的路径长度与该节点的权的乘积。例如树b中,节点10的路径长度为3,它的带权路径长度为10 * 3 = 30.

  • 树的带权路径长度:树的带权路径长度为所有叶子节点的带权路径长度之和,称为WPL。树a的WPL = 2*(10+20+30+40) = 200 ;树b的WPL = 1x40+2x30+3x10+3x20 = 190.而哈夫曼树就是树的带权路径最小的二叉树

  • 构造哈夫曼树:假设有n个权值,则构造出的哈夫曼树有n个叶子节点.n个权值记为{w1,w2,w3…wn},哈夫曼树的构造过程为:
    1.将w1,w2,w3…wn看成具有n棵树的森林,每棵树仅有一个节点。
    2.从森林中,选取两棵根节点权值最小的树,两棵树分别作为左子树与右子树,构建一棵新树。新树的权值等于左右子树权值之和。
    3.从森林中删除两棵权值最小的树,将构建完成后的新树加入森林中。
    4.重复2、3步骤,直到森林只剩一棵树为止。这棵树便是哈夫曼树。

图一的树b为一棵哈夫曼树,它的叶子节点为{10,20,30,40},以这4个权值构建树b的过程为:
在这里插入图片描述
在这里插入图片描述

  • 再看哈夫曼编码:为{10,20,30,40}这四个权值构建了哈夫曼编码后,我们可以由如下规则获得它们的哈夫曼编码:
    从根节点到每一个叶子节点的路径上,左分支记为0,右分支记为1,将这些0与1连起来即为叶子节点的哈夫曼编码。如下图:
    在这里插入图片描述
    在这里插入图片描述
  • 由此可见,出现频率越高的字母(即权值越大),其编码越短.这便使编码之后的字符串的平均长度、期望值降低,从而达到无损压缩数据的目的。

10.16补充

  • 度大于0的结点称为非终端节点;度为0的结点(没有子女节点)的结点称为叶子结点(非终端结点)。

  • 结点的深度是从根结点开始自顶向下逐层累加的。

  • 结点的高度是从叶节点开始自底向上逐层累加的。

  • 树的高度(深度)是树中结点的最大层数。

  • 树的性质:

    • 树中结点数等于所有结点的度数+1.
    • 度为m的树中第i层上至多有m的i-1次方个结点(i>=1).
    • 高度为h的m叉树至多有(m的h次方-1)/(m-1)个结点.
    • 具有n个结点的m叉树的最小高度为(向上取整)logm(n(m-1)+1).
  • 结点总数=所有结点的度数+1=分支总数+1(分支总数 = 0n0+1n1+2*n2…)。

  • 非空二叉树中第k层上至多有2的k-1次方个结点。

  • 高度为h的二叉树至多有2的h次方-1个结点。

  • 树采用顺序存储结构,存储的数组的下标从1开始;在最坏的情况下,一个高度为h有h个结点的树需要占用2
    的h次方-1个存储单元。

  • 二叉树属于树,二叉树可以使用树的存储方式,但是树不可以使用二叉树的存储方式。

  • 在含有n个结点的二叉链表中,含有n+1个空链域。

  • 自己手写 先序后续的非递归

  • 层次遍历需要借助队列,熟记并默写层次遍历的代码

  • 引入线索二叉树是为了加快查找结点前驱和后继的速度

  • 线索链表,是在线索二叉树的基础上建立起来的,线索二叉树只有指向左子树和右子树的指针域,其余为空,现在将其为空的指针域指向其前驱和后继,并另设立两个标志用来指示其左右指针是指向左右孩子还是前驱后继的。此种存储结构称为线索链表。

  • 带头结点的线索二叉树,为了方便(可以顺第一个结点的后继进行访问,也可以从最后一个结点起顺前驱进行查找),设立一个头结点,头结点的lchild指向根结点,rchild指向中序访问时最后一个结点,反之令中序中的第一个结点的lchild域的指针和最后一个结点的rchild域的指针均指向头结点,从而实现。。

  • 中序线索二叉树主要用来访问服务的,这种遍历不再需要栈,因为其结点中隐藏了线索二叉树的前驱和后继。利用其可以实现二叉树遍历的非递归算法。

  • 树不论采取哪种存储方式,都要求能唯一的反映树中各结点之间的逻辑关系。

  • 双亲表示法在求结点的孩子时需要遍历整个结构。

  • 二叉树转换为树或森林是唯一的。

  • 树、森、二先序相同;树先、森中、二中

  • 二叉排序树是一种递归的数据结构。

  • 含有n个结点的平衡二叉树的最大深度为O(log2n)。

  • 在含有n个带权叶子结点的二叉树中,其中带权路径长度(WPL)最小二叉树称为哈夫曼树。

  • 哈夫曼编码是一种常用的数据压缩编码。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

li_jeremy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值