第二章:树形结构

第二章:树形结构

本章结构
树和森林
二叉树
基本概念
与二叉树的转化
应用:查并集
基本概念
二叉树的遍历
应用
线索化二叉树
树形查找
哈夫曼树
二叉排序树
B/B+树
红黑树
AVL树

Part1:树和森林

1、基本概念

1.1 树

考虑如下的一棵树

A
B
C
D
E
F
  1. 拥有同一个父亲的结点1之间互称兄弟,例如(D、E、F)互称兄弟
  2. 关于结点的度数
    • 结点的度数即为孩子的个数
    • 整棵树的度数:树的结点的度数中的最大值
  3. 叶子结点:即为没有孩子的结点
  4. 重要公式(考试常考):树的总结点个数=树的结点度数之和+1

1.2 森林

即为n棵树构成的集合(n>=0)

2、与二叉树的转化2

2.1 树->二叉树

原则:左孩子,右兄弟

在这里插入图片描述

2.2 森林->二叉树

原则:

  • Step1:将森林里面的树变为二叉树
  • Step2:把第n+1棵树视为第n棵树的兄弟

在这里插入图片描述

3、应用:查并集(最优雅的数据结构之一)3

3.1 基本思想

用集合中的一个元素来代表整个集合

3.2 一个引例

假设一开始有6个元素,一开始都各自为一个集合

1
2
3
4
5
6

接着发生了集合合并,每两个元素为一个集合,其中集合A的代表元素为1,集合B的代表元素为3,集合A的代表元素为4

集合A
集合B
集合C
1
2
3
4
5
6

最终全部合并,结果非常像一棵树,最终这个集合的代表(可以理解为老大,BOSS)是1

1
2
3
4
5
6

3.3相关代码

借助3.2中的思路,我们可以轻松写出其初始化,查询集合老大,集合合并的代码

  1. 初始化

    void Inital(int fa[], int n){
      for(int i = 0; i < n; ++i)
        fa[i] = i;
    }
    

    一开始,所有元素的老大均为自己,即每个元素为一个集合;之所以可以采用一个一维数组存储,是因为每个集合的老大有且只有一个

  2. 查询集合老大

    2.1 递归实现

    int Find_Boss(int fa[], int i){
      if (fa[i] == i)
        return i;
      else
        Find_Boss(fa, fa[i]);
    }
    

    由1可知,集合老大的老大就是自己

    2.2 循环实现

    int Find_Boss(int fa[], int i){
      while(fa[i] != i)
        i = fa[i];
      return i;
    }
    
  3. 集合合并

    本质即为让集合的老大认另一个集合老大为大哥

    void Merge(int fa[], int i, int j){
      fa[Find_Boss(fa, i)] = fa[Find_Boss(fa, j)];
    }
    

Part2:二叉树

每个结点至多有两个孩子的树,称为二叉树

1、基本概念

1.1 两个特殊二叉树

  • 满二叉树:每层结点个数均达到最大
  • 完全二叉树4:除最底层外,其它层结点个数均达到最大;最底层结点从左向右依次排列

1.2 二叉树的存储结构

在这里插入图片描述

2、二叉树的遍历

2.1 二叉树的3+1种遍历

  • //序遍历:采用递归实现(序表示根节点何时被访问)
  • 层次遍历:采用辅助队列实现5

遍历模板

void Order(TreeNode * root){
  if (root != NULL)
  {
    // visit(root->data);		Pre_Order  前序遍历
    Order(root->left);
    // visit(root->data);		In_Order   中序遍历
    Order(root->right);
    // visit(root->data);		Post_Order 后续遍历
  }
}

例如:

A
B
C
D
E
  • Pre_Order: A B D E C
  • In_Order: D B E A C
  • Post_Order: D E B C A

2.2 线索化二叉树

由2.1可知一个二叉树的节点序列,在这个序列中大部分结点(n-2,除了第一个和最后一个)均有一个直接前驱和一个直接后继元素,如果我们想知道一个元素的前驱/后继信息时应该怎么办?

2.2.1 分析:以中序遍历为例

  • 如果一个结点有左右孩子,那么其前驱为左孩子,后继为右孩子,在这种情况下,我们可以立马找到这个元素的前驱和后继
  • 如果一个结点存在空指针,那么这个结点的前驱/后继无法找到,只能遍历整颗二叉树

综上所述,我们只需要考虑那么存在空指针的结点即可

2.2.2 观察:一颗有n个结点的二叉树

这棵二叉树一共闲置了n+1个指针6我们能否将这些闲置的指针利用起来,来达到我们的目的:不用遍历二叉树的情况下,找到该元素的前驱和后继。答案是肯定的,不过为了利用这些闲置的指针,我们需要对原有的结构体进行一些改造

typedef struct Thread_BinaryTreeNode{
  int data;
  bool left_is_pre, right_is_rear;
  struct Thread_BinaryTreeNode * left, * right;
}Tree_Node;

我们令:

left_is_pre = true; // 左指针指向前驱
left_is_pre = false; // 左指针指向左孩子
right_is_rear = true; // 右指针指向后继
right_is_rear = false; // 右指针指向右孩子

2.2.3 线索化二叉树

由于前驱和后继只有在遍历二叉树时才能得到,故线索化一颗二叉树的过程即为按序遍历的过程,只需把访问节点的代码修改为线索化的代码即可,如下所示

void Thread_Tree(Tree_node * now_node, Tree_Node ** pre_node){
  // 检查输入是否合法
  if(now_node == NULL || pre_node == NULL)
    return;
  Thread_Tree(now_node->left, pre_node);
  // 接下来是线索化代码
  if (now_node->left == NULL) 		// 当前结点无左孩子,则将其指向前驱
  {
    now_node->left_is_pre = ture;
    now_node->left = *pre_node;
  }
  if ((*pre_node)->right == NULL) // 当前节点无右孩子,则将其指向后继
  {
    (*pre_node)->right_is_rear = true;
    (*pre_node)->right = now_node;
  }
  // 这就是为什么需要二级指针的原因,因为需要修改指针本身的值
  *pre_node = now_node;
  // 线索化代码结束
  Thread_Tree(now_node->right, pre_node);
}

这样一来,二叉树的指针被充分利用,仅有2个指针闲置(第一个结点的前驱,最后一个节点的后继)

3、二叉树的应用

3.1 哈夫曼树

3.1.1 带权路径长度(Weight Path Length)和哈夫曼树(Huffman Tree)

  • 树的第i个结点的权重记为Wi
  • 树的根结点到该结点的边数即为Li

如果二叉树有n个结点,则有
W P L = ∑ i = 1 n W i ∗ L i WPL=\sum_{i=1}^nWi*Li WPL=i=1nWiLi
在用n个结点构造的二叉树中,WPL最小者,称为哈夫曼树

3.1.2 生成哈夫曼树

  • Setp1:在n个结点中选择两个最小者并生成一个父结点,父结点的权重为两者之和
  • Step2:在原来的n个结点中删除这两个结点,并加入这个父结点,重复

例如:

在这里插入图片描述

3.1.3 哈夫曼编码

将每一个字符视为一个结点,其出现的次数视为权重,构造哈夫曼树,按照一定的规则(左0右1/左1右0)编码。即为最优编码,可以压缩要发送的数据量

3.2树形查找

3.2.1 二叉排序树->AVL树

二叉排序树规则:

  • 左子树上的结点的值小于根结点
  • 右子树上的结点的值大于根结点

二叉排序树的插入和删除

  1. 插入:按照规则插入即可

  2. 删除:

    • 删除的结点为叶子节点:直接删除

    • 删除的结点只有一颗子树:用节点子树代替原结点(不然会断)

      在这里插入图片描述

    • 删除的结点存在两颗子树:

      删除原则:删除后其中序遍历的序列其他元素相对位置不变

      在这里插入图片描述

AVL树

请考虑如下两个二叉排序树的插入队列

  • S1:1,2,3,4,5,6,7
  • S2:4,2,6,1,3,5,7

其结果如下如所示

S1
S2
2
1
3
4
5
6
7
2
6
4
1
3
5
7

分析这两个插入队列,我们发现S1构成的二叉树退化成了一个链表,是的查找效率大大降低,时间负责度为O(n)。而S2则是一颗满二叉树,查找效率为O(log2n)

能否使用一种机制,防止出现这样子的情况?答当然有,即AVL树,该树规定,其任意节点的左右子树高度差的绝对值,不超过1,为此我们引入一个平衡因子(BF,Balance Factor)使其成为左右子树的高度差,为了方便计算BF,我们需要记录节点的高度。修改之后的结点类型如下所示

typedef struct BalanceTree_Node{
  int height;
  int value;
  struct BalanceTree_Node * left, * right;
}

AVL的实现思路是对最小不平衡树进行调整7

在这里插入图片描述

在这里插入图片描述

3.2.2 红黑树

由于AVL树的要求实在是太严格了(BF<=1)所以几乎每次插入和删除都会破坏这个条件,从而需要旋转来使之成为一颗AVL树。在那些删除和插入很平凡的场景中,AVL树的性能会大打折扣,所以出现了红黑树。(其实红黑树我也不是很懂,这里只是简单介绍一下它的规则,在408考纲里面出现了)

  1. 红黑树的5个规则
    • 根结点为黑色
    • 节点要么黑色要么红色
    • 所有叶子节点均为黑色(不存数据)
    • 每个红色结点必有两个黑色子结点(红色结点父子结点均为黑色)
    • 对任意结点,从该节点到任意叶结点的简单路径上,黑结点数量相同

可以总结为:左根右(排序树的规则:左<根<右),叶根黑(叶结点根结点黑色),不红红(红色结点不连续出现),黑路同

3.2.3 B/B+树(多路平衡查找树)

B树8

  1. 构造规则

    • 结点内关键字有序
    • 所有子树高度相同
    • 结点的孩子个数最小是(k/2)向上取整,最大是k(根结点除外)

    其中k成为B树的阶,即一个节点可以拥有的最大孩子的个数,如下图所示的为一颗B树

    在这里插入图片描述

  2. 插入和删除

    2.1 插入:

    如果可以正常插入则插入,如果结点溢出,则分裂(中间分裂)

    在这里插入图片描述

    2.2删除

    如果删除后依然满足B树规则,则删除;如果不满则合并

    • 兄弟结点够借:借兄弟
    • 兄弟结点不够借:借父结点

B+树9

在B树的基础上仅仅叶结点包含信息,非叶结点仅起到索引作用。存在两个头指针,一个指向root,一个指向第一个叶结点。应用于关系数据库MySQL。

在这里插入图片描述

Part end:参考文件和一些补充说明


  1. 关于结点和节点一些说明https://blog.csdn.net/qq_42270373/article/details/83758928 ↩︎

  2. 一些说明:做题的时候要注意,森林/树的先根遍历相当于其对应二叉树的先序遍历;而后根遍历相当于其对应二叉树的中序遍历 ↩︎

  3. 内容来自于这篇文章https://zhuanlan.zhihu.com/p/93647900 ↩︎

  4. 完全二叉树的本质https://zhuanlan.zhihu.com/p/153216919 ↩︎

  5. 在第一章的队列应用中提及 ↩︎

  6. 如何计算出n+1个闲置指针:设度为2的结点个数为n0;设度为1的结点个数为n1;设度为0的结点个数为n2;则有方程组2n0+n1+1=n;(利用树的重要公式)解得2n0+n1=n-1;而每个结点有2个指针,共2n个指针,使用了2n0+n1个指针,剩余2n-(2n0+n1)=n+1 ↩︎

  7. 这里补充一些细节在这里插入图片描述 ↩︎

  8. 关于B树的一些补充:为了减少磁盘的I/O操作,才有了B树,因为B树减少了树的高度,减少了磁盘I/O次数 ↩︎

  9. B和B+区别:(1)由于B树结点内存放信息,B+树结点内仅存放索引,所以B+树能够容纳的关键词个数也更多,其树高更小,磁盘I/O也更少(2)B+树查找每个元素均从root开始,最后均到达叶结点,所以其关键字查找路径长度相同(3)B+树由于存在叶结点之间的指针,所以非常方便遍历查询。基于这几点B+树常用语操作系统的文件系统中和数据库中 ↩︎

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值