数据结构笔记——树和图(王道408)(持续更新)

文章目录

  • 传送门
  • 前言
  • 树(重点)
    • 树的数据结构
      • 定义
      • 性质
    • 二叉树的数据结构
      • 定义
      • 性质
      • 储存结构
    • 二叉树算法
      • 先中后序遍历
        • 层次展开法
        • 递归模拟法
      • 层次遍历
      • 遍历序列逆向构造二叉树
    • 线索二叉树(难点)
      • 定义
        • 线索化的本质
      • 二叉树线索化
      • 线索二叉树中找前驱后继
        • 中序
        • 先序
        • 后序
    • 树算法
      • 储存结构
      • 树和森林的遍历
        • 树遍历
        • 森林遍历
    • 树应用
      • 哈夫曼树
      • 并查集
        • 数据结构
        • 优化
          • 并集:控制高度
          • 查集:压缩路径

传送门

思维导图+精炼的个人思考

前言

数据结构的笔记相比于其他3门,笔记的重要性要低很多,毕竟对于选择408的同学来说,大二时候应该有足够的时间学习,所以基础是比较好的,再加上csdn上一大堆数据结构和算法的帖子,我再重复造轮子也没啥意思了。

所以我这篇文章不打算写的很细节,就是单纯地把思路提纯出来,并附上自己深入的理解,再搭配思维导图就行了,而不去记录过于细节的知识。

树(重点)

树的数据结构

定义

数据结构笔记——树和图(王道408)(持续更新)_第1张图片
除了根节点,任何节点有且仅有1前驱:

  1. 根节点:0前驱,n后继
  2. 分支节点:1前驱,n后继
  3. 叶节点:1前驱,0后继

节点之间的关系:

  1. 祖先/子孙:所有有血缘关系的前辈/子孙。注:你叔和你没血缘关系,所以不算祖先
  2. 父亲/孩子:直接血缘关系的上下层节点
  3. 兄弟/堂兄弟: 在树的同一层,区分点在于是否是同一个爹生的

术语:

  1. 高度:从下往上数,叶节点是1
  2. 深度:从上往下算,根节点默认是1
  3. 节点的度:分支数
    • 在图里要分入度出度,树的入度确定,所以不讨论
    • 叶节点度=0,非叶节点度≠0
  4. 树的度:最大的节点度

性质

数据结构笔记——树和图(王道408)(持续更新)_第2张图片

  1. 每个度代表分出一个节点,所以n个度即分出n节点,考虑到1个根节点,即n+1个节点。
    • 不需要考虑重复,因为每次分支都是实实在在的,后面再分支也不会影响现在已经分出去的节点
  2. 度为m的树是m叉树(树度≤m)的特例
    • 树度=m,至少有一个为m度节点,因此至少有m+1个节点
  3. 给定深度/高度后的最多节点数。满度:m叉树,满度情况下,每层节点数是以1开头,m为比的等比数列,总和用等比数列求和
  4. 给定深度/高度后的最少节点数
    • m叉树,度没有下限,为了满足高度,极限情况需要有一条线,即h个节点
    • 度为m的数,在满足高度的一条线前提下,还要找一个节点额外分(m-1)个节点,所以h+m-1
      数据结构笔记——树和图(王道408)(持续更新)_第3张图片
  5. 给定节点数,求最小高度。列不等式,下界 数据结构笔记——树和图(王道408)(持续更新)_第4张图片

二叉树的数据结构

定义

数据结构笔记——树和图(王道408)(持续更新)_第5张图片
二叉树是一个纯粹的递归定义,二叉树只有两种状态:

  1. 空树
  2. 非空:根节点+左右子树
    • 左右子树也可以是空树或者非空
    • 左右之分,代表有序

具体来讲,有5种细分状态:

  1. 叶节点:度=0
  2. 单个孩子的节点:度=1
  3. 两个孩子的节点:度=2

通过度就可以区分节点的状态,后面说的所谓度为几,其实就是说xx状态的节点。

数据结构笔记——树和图(王道408)(持续更新)_第6张图片

完全二叉树:节点是从上到下,从左到右连续排列的,没有空隙
满二叉树:最后一层是满的完全二叉树

直接特性就是,如果n层有节点,代表n-1层一定是排满的,且n层这个节点左边也是排满的

接下来解读二者差别:

  1. 度分析
    • n+1层无节点,因此n层所有节点度=0
    • 完全二叉树
      • n-2层开始往上都满度。n层有节点,说明n-1层满,从n-2层开始往上都是满度(2)的
      • n-1层可能有0,1,2三种度,从左到右,先是一排2度,然后仅有一个1度,然后全是0度。如果有两个1度,就会打破完全二叉树连续排列的定义。
      • 总结,n-2层往上全满,n-1层可能有任何类型节点,n层全是叶子
    • 满二叉树,n层满,因此n-1层往上都是满度
  2. 父子关系。
    • 左孩子2i,右孩子2i+1
    • 父亲是孩子除以2,向下取整
  3. 结合1,2来定位完全二叉树的1度节点。
    • 1度节点肯定是最后一个孩子的父亲,直接除二向下取整,1度和2度节点都是分支节点,0度节点是叶节点。

数据结构笔记——树和图(王道408)(持续更新)_第7张图片

性质

二叉树推导:

  1. n0=n2+1
    • 推导:n=n0+n1+n2(节点总数),且n=n1+2n2+1(节点总数=总度数+1),减一下可得结果
    • 直观理解:度=1的节点不影响叶子结点数量。度=2的节点,每分叉一次,都是浪费一个叶子结点,同时生成两个叶子结点
    • 即,最开始有1个叶节点(根),每分叉一次(2度节点),就会多一个叶节点,因此最后n0=n2(分叉次数)+1(根)
  2. 2,3都是等比数列性质

完全二叉树推导:

  1. 推导公式,要从≤或者≥哪里去推导。因为上下界有两种理解方式,所以就会有两种结果:
    • 2 h − 1 − 1 < n ≤ 2 h − 1 2^{h-1}-12h11<n2h1,用右边的小于等于就可以推导出一个公式
    • 2 h − 1 ≤ n < 2 h 2^{h-1}≤n<2^h 2h1n2h,用左边的大于等于也可以推导一个h公式
  2. 给定完全二叉树的n,可以彻底确定其形状,节点的数量,类型,分布,下面有三个方程,联立就可以算出结果:
    • n=n0+n1+n2
    • n0=n2+1
    • n1只可能是0或者1,只需要看n奇偶就可以。n如果是偶数,最后这个节点是独生子,即其父为1度节点,n1=1;如果n是奇数,说明这个节点有亲兄弟,场上就不存在1度节点了。

纵观这些公式,关键在于几点:

  1. 完全二叉树对等比数列的认知
  2. 对完全二叉树结构的认识,第n-1层的度数排列,左2,中1,右0,分支带来的节点变化(对应公式)

数据结构笔记——树和图(王道408)(持续更新)_第8张图片

储存结构

顺序存储,先说完全二叉树:

  1. 首先是,第一个元素不储存,这样就可以把位序和下标对应,简化代码和逻辑。
  2. 其次就是找父子,判断层次,这些都是数量关系
  3. 然后就是判断节点状态,是0度还是1,2度?
    • 12度的区分这就需要结合n来判断,看看其左孩子,右孩子的位序是否越界n
    • 0度判断要先找到0度2度的分界点,即n/2向下取整,左边是2,右边是0。至于这个节点本身,就要区分12度了。

一般的二叉树,如果在数组中连续存储,就会破坏位序关系
如果按位序存储,仍然有很多不便。一来节点状态无法通过n来判断了,只能新增bool变量,二来还会浪费空间,因此二叉树其实一般是用链式结构储存的。

数据结构笔记——树和图(王道408)(持续更新)_第9张图片
链式储存:

假设有n个节点,那么总共就有2n个链域,除根节点外每有一个节点就会有一条边,每条边都会占用一个链域,总共n-1条边
因此就会剩下2n-(n-1)=n+1个空链域
这些链域可以用来逆向构建线索二叉树

在只有两个链域的情况下,找父节点要从根节点遍历,比较麻烦,因此要频繁查找父节点的话,还应该加入parent链域。

数据结构笔记——树和图(王道408)(持续更新)_第10张图片

二叉树算法

先中后序遍历

数据结构笔记——树和图(王道408)(持续更新)_第11张图片

所谓的先序,其实是先

数据结构笔记——树和图(王道408)(持续更新)_第12张图片

层次展开法

我此前是直接在脑子里去遍历,就是在脑子里模拟递归调用栈,虽然也能做,但是费脑子,不如下面的省劲:

逐层去展开,就像是前面从中缀转前缀/后缀的时候
先留出括号,然后将未处理部分在下一层继续展开

数据结构笔记——树和图(王道408)(持续更新)_第13张图片

二叉树,可以理解为一个代表中缀表达式的结构,分支节点上是运算符,叶节点是操作数,树结构代表了运算先后顺序

对其进行先序遍历,就是把符号放在前面,也就是前缀表达式,后序就是后缀表达式。注意,中缀的时候,需要加括号,之前我也写过一道这个算法题,我记得大致是从层次里提取括号。

数据结构笔记——树和图(王道408)(持续更新)_第14张图片

递归模拟法

至于递归代码,就很简单了:

T!=NULL是判断条件,如果空就不操作,非空就进行遍历。
visit代表对根结点的访问
根据前中后序遍历,可以在对左右孩子的递归调用之间找到一个合适的位置。

数据结构笔记——树和图(王道408)(持续更新)_第15张图片

有的考题会让你分析递归过程,前进回退,其实无论是哪种遍历,路径都是一模一样的,每个节点都被经过三次(算上左右孩子为空的情况):

  1. 刚到这个节点
  2. 从左边返回
  3. 从右边返回

前中后序遍历的区别就在于,是在哪一次经过的时候访问节点,先序就是在1,中序就是在2,后序就是3,到时候画一条路,然后脑补在该访问的时候写出节点就好。

需要注意,NULL孩子也算孩子,即使孩子为空,也要象征性地访问一下,一来是程序逻辑,二来是为了你好写顺序。

层次遍历

数据结构笔记——树和图(王道408)(持续更新)_第16张图片

思路很简单,就是用队列。

访问当前一层的时候,把下一层可以加的孩子都按序加进来。
直到最后队列为空

数据结构笔记——树和图(王道408)(持续更新)_第17张图片

遍历序列逆向构造二叉树

数据结构笔记——树和图(王道408)(持续更新)_第18张图片

前/后序+中序的逻辑一样,都是先找到根节点
然后去中序里面切分,对应回去将前/后序序列分割

如此就完成了一段的切分,然后递归地去切分剩下两段,类似于快排的感觉。

数据结构笔记——树和图(王道408)(持续更新)_第19张图片

层序的话,仍然是利用层序找根节点,然后用中序切割。

层序找根节点,是从前往后,以层为单位去找的,那么每层要考虑几个根节点呢?这就得看中序的切分情况了
中序切分后,该层所有根节点,只要有孩子就+1,最终数量就是下次要考虑的根节点数目比如下图:

  1. D
  2. AB(D中序切分后,左右非空,因此一个节点分2个)
  3. EFCG(AB中序切分后,AB节点左右都非空,因此是2+2)
  4. HI(EFCG中序切分后,只有CG有子节点,C和G各有一个,因此是1+1)

数据结构笔记——树和图(王道408)(持续更新)_第20张图片
又以下图为例:

  1. A
  2. B(A因为中序左边没有,因此是1)
  3. CD(B中序左右都有,因此是2)
  4. E

数据结构笔记——树和图(王道408)(持续更新)_第21张图片

线索二叉树(难点)

这一部分是难点,需要深入理解先中后序的逻辑,灵活利用展开的方法去分析,并且还要熟悉在树上遍历的过程。

定义

数据结构笔记——树和图(王道408)(持续更新)_第22张图片

线索化的本质

首先这里区分一下,这里说的前驱和后继,都是特指在中序遍历序列中的前驱后继,而不是二叉树上的前驱后继

一般情况下,我们遍历一颗二叉树,一定要从根节点开始,否则后面会有一些节点漏掉。比如下图,要想找到p的(中序)前驱和(中序)后继,要用q和pre指针进行一前一后的移动,从根节点开始把整个树遍历一次。

假如有一种情况,需要反复利用中序遍历序列,那么每次都从根节点获取中序遍历序列就很麻烦,于是有人直接利用线索二叉树,把中序遍历序列的信息储存到空链域里,这就是线索化的本质。姑且不论线索如何生效,你只知道利用线索就可以从中序遍历序列中的一个节点开始遍历出后面完整的中序遍历序列。

在上图中,完全可以从B开始,就可以依次遍历BEAFC,不用担心丢了某个节点。

按这个思路,按照线索可以直接遍历中序,加速遍历,甚至线索里本身就存着p在中序序列的前驱后继,你直接输出就行。

这就是线索化的意义,下图为线索化的过程,先遍历出一个中序序列,然后把中序序列前驱后继的信息填充到线索里,左对应前驱,右是后继,有n+1个空链域,因此有n+1个线索。为了区分链域储存的到底是线索还是孩子,需要用两个tag区分。

此处疑惑的点是,有的节点有后继但是没有链域存放后继,这个后面有解决办法。你姑且知道,有线索,就可以从中提取中序的前驱后继

下图为标准的储存结构图像,先序和后序的思路同中序。

数据结构笔记——树和图(王道408)(持续更新)_第23张图片

二叉树线索化

数据结构笔记——树和图(王道408)(持续更新)_第24张图片

二叉树线索化,本质就是把这个节点的左孩子链接到中序前驱,右孩子连到中序后继。

因此当务之急是在中序遍历的过程中,维护前驱和后继信息,其实在最开始已经说了,就是用一个pre指针(全局变量),pre是q的前驱,而q是pre的后继,此乃相对性

已经有信息了,那么在visit过程中就直接修改链域就好

  1. 判断条件
    • q一定非空,所以只需要判断左链域
    • 而pre可能空,所以还要都一个非空判断。
  2. 添加线索
    • 如果链域为空,就修改为对应的线索
    • 记得把tag同步修改。
  3. 最后修补
    • 注意到,最后一次visit,q指向尾部,而pre指向倒数第二,也就是说,q的后继是没有考虑的
    • 因此在程序执行完毕后,还要额外判断pre的右链域,全局变量的好处在此处体现。
    • 有些写法是直接让pre右链域=NULL,pre->rtag=1的,这种也没错,因为在中序遍历里,如果这个节点有右孩子,那这个节点就不是最后一个节点了。但是在后序遍历里面,最后一个visit的节点有可能有右孩子,所以就不能这么个判断。总的来说加判断可以涵盖一切情况,因此就统一加判断,只有前序和中序情况下,才可以不判断。

下图为具体的代码,左上角为主函数:

  1. 初始化pre=NULL,
  2. 然后线索化,q就是从T开始
  3. 最后再善后pre右链域

先序线索化基本照搬,就是在先序遍历的过程中,去添加线索。

但是这里有一个bug,对于没有左孩子的节点(比如D),按道理说,添加完线索以后就没他什么事情了,而且访问过以后,也没有机会再访问第二次了。

但是在先序线索化过程中,会先visit,添加前驱线索,之后左孩子的链域就非空了,此时如果还按照原有逻辑,就又会跳回到前驱,开始进行遍历,这就是一个死循环。

破解办法很简单,本来左孩子的链域应该是空的,此时有了指针,可以用tag来判断原来是否为空,只有tag=0(原来非空,真正的有左子树),这才能跳转到左孩子的节点。

数据结构笔记——树和图(王道408)(持续更新)_第25张图片

线索二叉树中找前驱后继

总评:这一章是线索二叉树的核心难点,尤其是三叉链表情况下的别扭题。

我的建议是,一切的一切,要抓准先序中序后续的本质特性,比如先序就是“根左右”,你到时候现场推导判断条件,灵活且不容易出错。

数据结构笔记——树和图(王道408)(持续更新)_第26张图片

中序

之所以用中序为例子,是因为中序线索化是完美的,“左根右”,有根节点,找前驱就去左孩子里面找,找后继就去右孩子里找,而先序只能找后继,后序只能找前驱。

先说后继,判断rtag,两种情况:

  1. rtag=1,证明右链域是线索,那么直接用线索
  2. rtag=0,证明右链域有右子树,那么就需要找到右子树中序遍历序列中最左边的节点
    • 其实就是右子树左下角的节点
    • 具体就是下面FirstNode函数,一直找左孩子,直到没有左子树
    • 因为此时已经线索化了,终止条件所以不再用NULL,而是用ltag判断

既然能找到中序后继,一次快速的中序遍历就很简单了:

  1. 先通过FirstNode,找到第一个节点
  2. 然后从第一个节点开始,不停找后继,直到遍历完毕

这种方法的特色在于,找后继过程中,可以通过线索大大加速遍历速度,复杂度为O(1)

中序线索的前驱思路和后继一样,整体是镜像的感觉,左变右,有前驱线索就用线索,没有就找左子树右下角节点。

LastNode函数,就是一直去找右孩子,直到rtag=1

因此也衍生出逆向遍历中序线索的方式。

数据结构笔记——树和图(王道408)(持续更新)_第27张图片

先序

中序最为规整,符合直观,先序后序就比较别扭,容易混乱,本质上是先序会浪费链域,比如本来就有左孩子,然后你右链域如果为空,还要再指向左孩子,这就是浪费,这也就是其前驱后继只能二选一的原因,浪费了信息,自然就无法实现全部的功能。

数据结构笔记——树和图(王道408)(持续更新)_第28张图片

回归正题,先序找后继,后序找前驱,反之则不一定,容易记混,而且实际生产也没用,要我说还不如深入理解,临场画图,更加保险

先序找后继,本质思路就是“根左右”:

  1. rtag=1,有线索,用线索
  2. rtag=0,无线索,即有右孩子,此时用“根左右”思考
    • 有左孩子,则左孩子为后继
    • 无左孩子,则右孩子为后继

数据结构笔记——树和图(王道408)(持续更新)_第29张图片
前驱不一定有,逻辑如下:

  1. ltag=1,有线索,用线索
  2. ltag=0,有左孩子
    • 问题在于,遍历序列是“根左右”,以根开始,无论你找左孩子还是右孩子,先序情况下只要你找的是孩子,你顶天了只能是后继
    • 因此ltag=0的时候只能从根开始遍历,用不了线索

但是很明显,这里可以加考点,如果用三叉链表,允许找到父节点,那么事情会有转机,当然也特别麻烦(为考而考),一切的关键是理解先序遍历的根左右思想

  1. ltag=1,用线索
  2. lgat=0,先找父节点
    • 情况1:p无父节点,则无前驱
    • p有父节点,有前驱,然后进行判断。先看上层结构:“根左右”
      • 情况2:p是左孩子,则为“根(左右)右”,则父(根)节点为前驱
      • p是右孩子,p处在序列中“右”的位置,则要判断“左这一部分的情况”
        • 情况3:“左”为空,即“根()(左右)”,那么父节点当前驱
        • 情况4:“左”非空,即“根(左子树)(左右)”,那么就要在父节点左子树里,走一遍先序遍历,用其最后一个节点当前驱。这一步要求你对先序遍历极其熟悉(可以简化,有右找右,没右找左,右左都没,就是最后一个)

说实话真的有点大冰,建议直接画图思考,把这几种情况都画出来,这么多背不会的。

数据结构笔记——树和图(王道408)(持续更新)_第30张图片

后序

后序线索,只能找前驱,如果要找后继,同样麻烦。

后序前驱:

  1. ltag=1,用线索
  2. ltag=0,则有左孩子,利用“左右根”寻找前驱
    • 有右孩子,则为“左(左右根)根”,因此右孩子就是前驱
    • 无右孩子,则为“(左右根)()根”,因此左孩子就是前驱

后续后继,因为是“左右根”,无法通过左右子树找根的后继,同样需要三叉链表配合:

  1. rtag=1,用线索
  2. rtag=0,先找父节点
    • 情况1:p无父节点,则无后继
    • p有父节点,此时上一层为**“左右根”**,我们这一层仍然需要先定位
      • 情况2:p为右孩子,则为左(左右根)根,因此p的后继就是父节点
      • p为左孩子,因此要判断“右”这一部分是否存在
        • 情况3:父节点无右孩子,则为“(左右)()根”,p的后继是父节点
        • 情况4:父节点有右孩子,则为“(左右)(右根)根”,p的后继是父节点右子树后序遍历序列中第一个输出的节点(简化逻辑就是,有左就左,没左找右,没左没右,就是后继)

再次吐槽,真的是有大冰,建议推导两三次,就不用记了,直接临场速推。

数据结构笔记——树和图(王道408)(持续更新)_第31张图片

树算法

储存结构

数据结构笔记——树和图(王道408)(持续更新)_第32张图片
树的难点在于,因为度是没限制的,所以链域给几个是不确定的,树的所有结构都是为了解决这个核心问题而创造的。

最开始双亲表示法的思路:

  1. 出度不确定,入度确定啊
  2. 因此采用逆向指针,链域=1,非根节点就可以保证有且只有1个前驱

因为结构比较简陋,因此一般用数组存,也只能用数组存,因为找孩子节点只能用遍历实现(查)

增删如何实现呢?增,在最后增加元素就行,删就是直接把链域抹-1就行,但是更好的方法是把最后一个元素挪到这个位置覆盖,因为这样可以缩减数组长度,提高遍历速度。

数据结构笔记——树和图(王道408)(持续更新)_第33张图片

双亲表示是纯数组表示,而孩子表示法是数组+链表的混合存储结构(其实就是图的邻接表结构用在了树上)

延续链域不确定的思路,解决不确定的数量自然会想到链表,因此用一串链表表示一个节点的所有孩子:

  1. 数组元素储存节点本身
  2. 链表元素储存孩子的位置信息。
    • 链表元素可以看做是两类指针的结合体,孩子下标算一个指针,指向具体元素,而链表next也是指针,指向下一个链表元素。

这个结构其实是图的结构,优劣势这里就不讲了,简单来说就是找孩子简单找双亲难。

最后就是孩子兄弟表示法,纯链式存储。

纯链式存储怎么能解决链域不定的问题呢?换个思路,链域里不储存所有孩子的指针,而是储存一个孩子+右兄弟,这样链域就固定为两个了,可以用二叉树储存了。

从视图来看,新的二叉树就是就是原有的树顺时针旋转45°,然后用孩子兄弟法重新连线后的结果。
逆过来转换就是左转,然后重新连线。

为了防止出错,可以用两种颜色的笔区分孩子链域和兄弟链域,又或者你直接记住左分叉是孩子链域,右分叉是兄弟链域就好。

数据结构笔记——树和图(王道408)(持续更新)_第34张图片
森林和二叉树,只需要补兄弟边就好,每一颗树的根,合起来在同一层,互为兄弟。

之后就是经典的旋转转化了,还原的时候,几何上应该从右边开始画线,一层一层往左边推。

树和森林的遍历

对应关系,森林->树->二叉树,二叉树是宇宙万法的尽头,硬要你写代码的话,一切代码都可以用二叉树实现。

在这里插入图片描述

树遍历

树只有先序和后序,不存在中序的说法,因为无法确定孩子数量,所以干脆就不往中间排,要么先访问再依次递归孩子,要么先递归完孩子再访问。

数的初始逻辑结构和其二叉树形态有所不同,因此遍历序列也不相同,对应关系:

  1. 树先序=二叉树
  2. 树后序=二叉树

数据结构笔记——树和图(王道408)(持续更新)_第35张图片
数据结构笔记——树和图(王道408)(持续更新)_第36张图片

森林遍历

数据结构笔记——树和图(王道408)(持续更新)_第37张图片

森林遍历涉及二层递归,这里先简单理解下。先序还好,主要是中序,为什么树没有中序,森林这里就是中序了呢?因为可以以一个节点,切分出两片森林,这才可以把根节点放在中间,即中序。理论上森林还有后序,但是过于阴间,就没有放出来。

写代码的话,比较复杂,直接换个思路,用效果等价:

  1. 森林先序=每棵树先序遍历
  2. 森林中序=每棵树后序遍历

实在考出来写代码,也可以取巧,先把森林转化为二叉树储存结构,然后把森林的遍历等效到树的遍历,再对标到二叉树的遍历,比如森林中=树后=二叉树中,这样也可以写代码。

数据结构笔记——树和图(王道408)(持续更新)_第38张图片

树应用

哈夫曼树

数据结构笔记——树和图(王道408)(持续更新)_第39张图片

首先说带权路径长度(WPL,Weighted Path Length)

一条路径的WPL=经过的边数×终点权值
一棵树的WPL=所有叶节点的WPL

数据结构笔记——树和图(王道408)(持续更新)_第40张图片

哈夫曼树就是WPL最小的二叉树,给定一些带权叶节点,这里首先定义一下树的总权值=叶节点权值和,构造一颗哈夫曼树过程如下:

  1. 所有叶节点构成一个集合
  2. 合并
    • 找场上总权值最小的两棵树
    • 二合一,并将树的总权值记录在根节点上
  3. 循环2步骤,直到集合中只剩一个元素

哈夫曼树的特点如下:

  1. WPL最小。权值小的,路径长,权值大的,路径短,那么总共的WPL就被平衡下来,达到最小。
  2. 无歧义。初始节点最后只能成为叶节点,中间结点无意义。
  3. 哈夫曼树不唯一
    数据结构笔记——树和图(王道408)(持续更新)_第41张图片

哈夫曼树的应用是可变长度编码

权值可以理解为使用频率,或者重要程度。重要的,就应该放在上面,以更少的消耗编码:

  1. 先按照权值构造哈夫曼树
  2. 哈夫曼树获得编码

如下图,C最重要,因此只用1个bit,ABD长度依次增加。

这种不定长编码的问题在于,可能会有歧义,因为无法定长分节解析,但是恰巧哈弗曼编码无歧义,为前缀编码,因此可以使用异步方式解析。如何解码呢?其实就是把这一串序列送到哈夫曼树里面,走到叶节点就解析一个字符,然后下一个序列继续从根开始。这个过程其实就是一个FDA,和编译原理有共同之处

如此,从概率来看最后编码和解码的总消耗都是最少的(实际上不见得最少)

数据结构笔记——树和图(王道408)(持续更新)_第42张图片

并查集

数据结构笔记——树和图(王道408)(持续更新)_第43张图片

数据结构

并查集就是集合,集合之间互不相交,我们此处要用数据结构描述这种互不相交的关系。

基于这个关系,还应该实现两个基本功能:

  1. 查:给定元素,判断所属的集合
    • 这个操作说白了就是找根节点
  2. 并:合并两个集合
    • 直接让一颗树的根节点成为另一棵树的子节点就好

这两种方法,用双亲表示法都是最有效率的,实现也很容易。

其实也可以用链表,我也做过链表的题,但是因为链表找到头比较慢,要O(n),而树结构分叉,高度大概只有O(logN),查一次要的迭代次数更少,因此时间效率上,双亲表示法完胜

现在唯一的技术难点其实就是如何用一个数组储存一个森林呢?本质就在于要同时维持多个根节点,但是这个问题其实不是问题,因为不同的根节点可以通过下标来区分,反正我们只有并查操作,只需要找双亲,所以这样储存完全没问题,就这么简单粗暴。

说白了,把操作限制在并查,就可以针对性的使用高效率的结构实现,操作越复杂,耗时越多,还需要用更精巧复杂的结构来降低操作的损耗,这是不变的道理

数据结构笔记——树和图(王道408)(持续更新)_第44张图片

优化

数据结构笔记——树和图(王道408)(持续更新)_第45张图片

并集:控制高度

优化效率,就要分析复杂度。

并集为O(1),改下指针就行,查需要逆向迭代,如果所有元素串成一条线,那么最坏就是O(n),反之,如果平铺开来,尽可能降低高度,就可以提高效率。

注意,双亲法是树,不限制孩子个数,所以每次并集都是直接并到根节点上,现在我们要优化这个过程:

仔细分析Union过程,在把一颗高度为n的树插到另一颗树上时,这颗子树算上根节点,总高度其实是变成了n+1。
假如有三个节点ABC,把C插到B上,再把B插到A上,总高度就是3
换个思路,如果把C插到B,然后把A插到B,总高度只有2,在第二次插入过程中,总高度并没有变化。

具体逻辑如下:

  1. 把矮树并到高树上,就不会增加总高度
  2. 若两树高度相同,则并集会令高度+1

但是呢,我们对高度其实并没有一个衡量,所以只能用挂载节点的数量(元素数量)来近似高度,既然无法判断高矮,判断大小也是可以的,具体操作如下:

数据结构笔记——树和图(王道408)(持续更新)_第46张图片

但是你以为这就完了吗?如果你从离散的叶节点开始合并,你会发现最后的长度无论如何都不会超过这个数,即时间复杂度最好O(1),最坏都是O(logN)

在这里插入图片描述

查集:压缩路径

前面说的是在并集过程中对于高度的优化

现在进一步,在查集的过程中进一步优化,属实是脑洞大开,令人拍案:

  1. 首先找到根节点,保存根节点位置
  2. 然后进行二轮回溯压缩路径,从路径终点开始
    • 先保存父节点备用
    • 拆桥,把当前节点挂载到根节点
    • 保存的父节点派上用场,当做下一轮循环要调整位置的节点

如此,每次查询,都可以进行一次压缩,越查越快,可以对抗并集导致的高度增加,配合并集优化,可以使得一般情况下高度维持在4以内,牛逼。

数据结构笔记——树和图(王道408)(持续更新)_第47张图片

你可能感兴趣的:(个人随笔/学习笔记,数据结构+算法,数据结构,笔记)