数据结构5-树和二叉树

205 阅读15分钟

1.树和二叉树

  • 树和定义
  • 树的基本用语
  • 二叉树的定义

2.二叉树的性质和存储结构

  • 二叉树的性质
  • 二叉树的存储结构

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

  • 遍历二叉树
  • 线索二叉树

4.树和森林

  • 树的存储结构
  • 森林与二叉树的转换
  • 树和森林的遍历

5.哈夫曼树及其应用

  • 哈夫曼树的基本概念
  • 哈夫曼树的构造算法
  • 哈夫曼编码

1.1树和定义

树:由多个节点的组成的有限集

  1. 有且仅有一个称之为根的节点,简称根
  2. 除了根节点以外的其余节点可分为多个互不相干的有限集,其中每一个集合本身又是一棵树,简称根的子树

1.2树的基本定义

  1. 节点:树中的一个独立单元,包含一个数据元素及若干指向其子树的分支
  2. 节点的度:节点拥有的子树数(含有几个分支)
  3. 树的度:树内各节点度中的最大值
  4. 叶子:度为0的节点
  5. 非终端节点:度不为0的节点(也称内部节点除了根节点)
  6. 双亲和孩子:节点A的子树的根(与节点A直接连接的节点B)称为该节点的孩子,相应的,该节点A称为孩子的双亲
  7. 兄弟:同一个双亲的孩子
  8. 祖先:从根到该节点所经分支上的所有节点
  9. 子孙:以某个节点为根,它的子树中任一节点
  10. 层次:节点的层次(根为第一层,根的孩子为第二层,依次类推)
  11. 堂兄弟:双亲在同一层的节点
  12. 树的深度(高度):树中节点的最大层次
  13. 有序树和无序树:把树中节点的各子树看成从左至右是有次序的称为有序树,反之为无序树
  14. 森林:有多个互不相交的树的集合

1.3二叉树的定义

二叉树:由多个节点构成的集合

  1. 有且仅有一个称为根的节点,简称根
  2. 除了根节点以外的其余节点仅可分为两个互不相干的子集,一个为左子树,一个称为右子树,且两个集合又是二叉树

🔴树和二叉树的区别

二叉树每个节点至多有两个子树,且该子树有左右之分,不能颠倒


2.1二叉树的性质

性质1:

二叉树的第i层至多有2^i-1个节点

性质2:

深度为k的二叉树至多有2^k-1个节点

性质3:

二叉树的终端节点数为n,度为2的节点数为m,则n=m+1

性质4:

具有n个节点的完全二叉树的深度为[log2n]+1

2.1.1 满二叉树和完全二叉树

满二叉树:

每一层的节点数都是最大节点数

完全二叉树:

每个节点都需要和对应的满二叉树中的编号1-n的节点一一对应

(只允许最后一层有空缺节点,且空缺节点在右边,即叶子节点只能在层次最大的两层出现)

2.2二叉树的存储结构

顺序存储结构链式存储结构

顺序存储结构:

(适合完全二叉树)

使用一组地址连续的存储单元来存储数据元素

链式存储结构:

(适合一般二叉树)

设置不同的节点结构可构成不同形式的链式存储结构

该节点至少包含3个域:数据域,左子树域,右子树域

为了便于找到节点的双亲,多设置一个指向双亲节点的指针

前者为二叉链表,后者为三叉链表

3.1遍历二叉树

遍历:按照某条搜索路径寻访树中的每个节点

(保证每个节点都被访问到,且每个节点有且只被访问一次)

如限定从左到右,对于遍历分三种:先序遍历中序遍历后序遍历层次遍历

*先序遍历

若二叉树为空,操作为空,否则

先访问根节点

在先序遍历左节点

再先序遍历右节点

*中序遍历

若二叉树为空,操作为空,否则

先中序遍历左子树

再访问根节点

在中序遍历右子树

*后序遍历

若二叉树为空,操作为空,否则

先后序遍历左子树

在后序遍历右子树

再访问根节点

*层次遍历

从上到下,从左到右

从树根开始逐层访问二叉树上的所有结点

对于遍历而言。大多都是采用递归的算法来实现的,下列将使用非递归算法

对于中序遍历的非递归算法:

1.初始化一个空栈s用于存放内容,指针p指向根节点

2.申请一个节点空间q,用来存放栈顶弹出的元素

3.当p非空或者栈s非空时,循环执行以下操作

  • 如果p非空,p指向该节点的左孩子(遍历左子树)
  • 如果p为空,则弹出栈顶元素并访问根节点,将p指向该节点右孩子(遍历右子树)

🔴:由二叉树的先序序列和中序序列,或由后序序列和中序序列,可以唯一确定一颗二叉树

 例如:
 中序序列和后序序列分别为:BDCEAFHG,DECBHGFA
      A
     /  \
    B    F
     \    \
      C    G
     / \  /
    D   E H

3.1.1二叉树遍历算法的应用

1.创建二叉链表(按照先序遍历)

1.查找字符序列,读入字符ch

2.如果ch是“#”,则表明该二叉树为空树,否则执行以下操作

  • 申请一个节点空间
  • 将ch赋给T->data
  • 递归创建T的左子树
  • 递归创建T的右子树

🔺:常用“#”来表示为空树

 例如:
 ABC##DE#G##F###
      A
     /
    B
   / \
  C   D
     / \
    E   F
     \
       G
2.复制二叉树(先序遍历)

1.如果是空树,递归结束,否则执行以下操作

  • 申请一个新节点空间,复制根节点
  • 递归复制左子树
  • 递归复制右子树
3.计算二叉树深度(后序遍历)

1.如果是空树,递归结束,深度为0,否则执行以下操作

  • 递归计算左子树的深度m
  • 递归计算右子树的深度n
  • 如果m>n,二叉树深度为m+1,否则为n+1(加上根节点)
4.统计二叉树中节点的个数

如果是空树,递归结束,节点为0,否则

  • 使用循环的方式计算左右子树节点
  • 总节点=左子树节点+右子树节点+根节点(1)

3.2线索二叉树

当以二叉链表作为存储结构时,可以找到左,右孩子信息,节点在任一序列中的前驱,后驱信息

🔴:由于有n个节点的二叉链表中一定有n+1个空链表,可以利用这些空链表来存放其他信息

规定

  1. 若一个节点有左子树,则Ichild域指示左孩子,否则Ichild域指示其前驱
  2. 若一个节点有右子树,则Rchild域指示右孩子,否则Rchild域指示其后驱
  3. 为了避免混淆,需要改变节点结构,增加两个标志域(LTag,RTag)

当LTag,Rtag为0时指向孩子,为1时指向前后驱

· 以这种节点结构构成的二叉链表作为二叉树的存储结构——线索链表

· 其中指向节点前后驱的指针——索

· 对二叉树以某种次序遍历使其变成线索二叉树的过程——线索化

同时,建立一个头节点,这样可以使二叉树变成一个双向链表

1.以节点p为根的子树中序线索化
  1. 如果p非空,左子树递归线索化
  2. 如果p的左孩子为空,则给p加上左线索,将LTag置为1,让p的左孩子指针指向pre(前驱);否则让LTag置为0
  3. 如果pre的右孩子为空,则给pre加上右线索,将RTag置为1,让pre的右孩子指针指向p(后继);否则让RTag置为0
  4. 将pre指向刚访问过的节点p,即pre=p
  5. 右子树递归线索化
2.带头节点的二叉树中序线索化

与上面一样,只是在最开始先建立头节点并对头节点的指针域进行操作


4.1树的存储结构

1.双亲表示法:

利用一组连续的存储单元来存储树的节点(数组),每个节点有一个data域,一个parent域

 数组下标|节点|双亲
 0|R|-1                                 
 1|A|0
 2|B|0
 3|C|0
 4|D|1
 5|E|1
 6|F|G
 7|G|6
 8|H|6
 9|K|6
 ​
 -1常用来表示无双亲
2.孩子表示法:

由于一个节点可能出现多个孩子,所以可以把同一个双亲的孩子的节点排列起来,看成线性表,且以单链表作为存储结构(即在每个双亲后面的指针指向一个单链表,该单链表存放该双亲孩子信息,若无孩子,则指针为空)

 数组下标|节点|指针(指示其孩子)
 0|R|·-1-2-3                                 
 1|A|·-4-5
 2|B|·
 3|C|·-6
 4|D|·
 5|E|·
 6|F|·-7-8-9
 7|G|·
 8|H|·
 9|K|·
 ​
 注意:单链表中的最后一个节点的指针域为NULL
3.孩子兄弟表示法:(对于二叉树而言)

🔴:又称二叉树表示法或二叉树表表示法

需要两个指针链域:一个指向第一个孩子节点,一个指向下一个孩子的兄弟节点

(即一个节点有两个指针链域,一个指向该节点的孩子,一个指向该节点孩子的兄弟)

4.2森林与二叉树的转换

树变成二叉树

加线:在兄弟之间加一根连线

抹线:对每个结点(除了左孩子)除去与其他孩子的关系(兄弟关系)

旋转:以树的根结点为轴心,将树顺时针旋转45°

1.森林变成二叉树

  1. 将树变成二叉树
  2. 每棵树的根结点用线相连
  3. 以第一颗树根结点以轴心,顺时针旋转45°
  4. (第一颗子树森林转换成左子树,剩余树的森林转换成右子树)

二叉树变成树

加线:若p结点是双亲结点的左孩子,则将p的右孩子,右孩子的右孩子(沿着分支找到所有的右孩子)都与p的双亲相连

抹线:抹掉原二叉树中双亲与右孩子之间的连线

调整:将结点按层次排序形成树结构

2.二叉树变成森林

  1. 先将每个结点与右孩子节点的连线删除,得到分离的二叉树

    (删除连线后,可能出现单节点,寻找最近的原左孩子的双亲并连接)

  2. 把每棵二叉树变成树

  3. 整理排序变成森林

4.3树和森林的遍历

树的遍历

先根遍历后根遍历

🔴由于树没有左子树,右子树之分,所有没有中根遍历(记得和遍历二叉树区分开来)

先根遍历:先访问树的根节点,然后依次先根遍历根的每颗子树

后根遍历:先依次后根遍历每颗子树,然后访问根节点

森林的遍历

先序遍历中序遍历

🔴

先序遍历:

  1. 访问森林中第一颗树的根节点
  2. 先序遍历第一颗树的根节点的子树森林
  3. 先序遍历除去第一颗树之后剩余的树构成的森林

(相当于反复步骤1.2直到森林为空)

 树1根    树2根     树3根
   |      |         |
 多个子树 
 ​
 第一步;树1根
 第二步:子树森林
 第三步:树2,树3重新构成森林,重复步骤1.2

中序遍历:

  1. 中序遍历森林中第一颗树的根节点的子树森林
  2. 访问第一颗树的根节点
  3. 中序遍历除去第一棵树之后剩余的树构成的森林
 树1根    树2根     树3根
   |      |       |
 多个子树 
 ​
 第一步;子树森林
 第二步:树1根
 第三步:树2,树3重新构成森林,重复步骤1.2

5.1哈夫曼树的基本概念

哈夫曼树又称最优树,是一类带权路径长度最短的树

  • 路径:从树中一个节点到另一个节点之间的分支构成这1两个节点之间的路径
  • 路径长度:路径上的分支数目
  • 树的路径长度:从树根到每一叶子节点的路径长度之和
  • 权:赋予某个实体的一个量,是对实体的某个或某些属性的数值化描述
  • 节点的带权路径长度:从该节点到树根之间的路径长度与节点上权值的乘积
  • 树的带权路径长度:树中所有叶子节点的带权路径长度之和
  • 哈夫曼树:假设有M(m1,m2....)个权值,可以构造一棵含n个叶子节点的二叉树,每个叶子节点的权值为mi,其中带权路径长度最小的二叉树就是哈夫曼树(最优二叉树)

5.2哈夫曼树的构造算法

1.哈夫曼树的构造过程
  1. 给定n个权值,构造n棵只有根节点的二叉树,这n棵二叉树又构成森林F
  2. 在森林F中选取两棵根节点的权值最小的树作为左右子树构造出一颗新的二叉树(即构建一个双亲),且新的二叉树的根节点的权值为左右子树权值之和(即双亲的权值是两个孩子权值的和
  3. 在森林F中删除这两棵树,并加入构建新的二叉树(此时,新的二叉树含有子树)
  4. 重复2.3,直到F只含一棵树为止
2.哈夫曼树算法的实现

由于哈夫曼树没有度为1的节点,所有一颗含有n个叶子节点的哈夫曼树一共有2n-1个节点,将其存储在的大小为2n-1的一维数组中

对于哈夫曼树的每个节点而言,需要包含(双亲信息,两个孩子节点信息,还需要存放自身的权值)

2.1初始化哈夫曼树

动态申请2n个单元(每个单元存储双亲,左孩子,右孩子,权值)然后循环2n-1次,依次将1到2n-1所有单元中的内容初始化为0,然后循环n次,输入前n个单元中叶子节点的权值

🔴:

1.使用数组存储时,从下标为1的单元开始存储

2.在没有构造哈夫曼树时,只有前n个单元有权值,构造完哈夫曼树时,在第n单元后多了n-1个单元有权值(两两树组合成一个新树,n个树一共组成n-1个新树)

2.2创建哈夫曼树

循环n-1次,通过n-1次的选择,删除,合并来创建哈夫曼树

🔴:

1.选择:选择双亲为0且权值最小的两个树根节点m,n

2.删除:将m,n的双亲改为非0

3.合并:将m,n的权值加起来形成一个新节点权值,并将新的权值依次存放在数组的n+1号及之后的单元,同时记录这个新节点左孩子的下标为m,右孩子的下标为n

🔵:一般来说:对于左右子树的权值需要统一,即在构造哈夫曼树时,左子树的权值需要全部都小于右子树的权值,反过来也可以,但是需要统一

节点i(数组下标)wightparentIchildRchild
13500
25500
37600
49700
58612
615735
724046

注:节点为1.2.3.4为初始的树根节点,节点为5.6.7是构造的新树

3.哈夫曼编码

为了数据压缩后的数据文件尽可能短,可采用不定长编码

即把出现次数多的字符编以较短的编码

可用哈夫曼树来进行编码,约定左分支标记为0,右分支标记为1,则根节点到每个叶子节点路径上的0/1序列就是相应的编码

两个概念:

  1. 前缀编码:如果在一个编码方案中,任一个编码都不是其他编码的前缀就是前缀编码
  2. 哈夫曼编码:对一棵树,约定左分支为0,右分支为1,根到对应叶子的路径山的0/1序列变成一个二进制串,该串就是哈夫曼树

🔴: 哈夫曼树是前缀编码,因为哈夫曼树对应的路径的终点一定是叶子,所有任一哈夫曼编码一定不会和其他哈夫曼编码的前缀部分完全重叠

3.1文件的编码和译码

编码:有了字符集的哈夫曼编码表之后,依次读取文件中的字符c,在哈夫曼树编码表HC中找到此字符,将字符吃转换成编码表中存放的编码串(已知哈夫曼编码表)

译码:依次读取文件的二进制码,从哈夫曼树的根节点出发,走到叶子节点时就可以译出相应的字符,然后重新从根出发进行译码直到文件结束(已知哈夫曼树,且每个节点的权值也知道)