数据结构(5):数【新】

1 树和二叉树的定义

1.1 树的定义

  • 根节点、分支结点、边、叶子结点
  • 空树、非空树
  • 前驱(除根结点之外,所有结点有且只有一个前驱)、后继(0个或多个后继)

在这里插入图片描述

  • 根(特定、唯一)
  • m个不相交的有限集合

在这里插入图片描述

1.2 树的基本术语

  • 祖先结点:直系长辈 - 父亲、爷爷
  • 子孙结点:直系子孙
  • 孩子结点:只是下一代孩子
  • 兄弟结点:父亲、二叔
  • 堂兄弟结点:L、M(也可以知道L、M位于同一层)
  • !!! 路径:只能从上往下
  • 路径长度:经过几条边
  • !!!树的路径长度:从根节点到每个结点的路径长度的总和

在这里插入图片描述

  • 结点的层次:从1开始(考试的时候看实际情况),从上往下
  • 结点的高度:从下往上
  • 树的高度(深度):有几层
  • 结点的度(和 “高度” 做区别):有几个分支
  • 树的度:最大的结点度

在这里插入图片描述

  • 有序树:从左至右有次序
  • 无序树

在这里插入图片描述

  • 森林:互不相交的的树的集合
  • 可以有空森林

在这里插入图片描述

在这里插入图片描述

1.3 树的性质

  • 结点数 = 总度数(边数) + 1

在这里插入图片描述

  • m叉树:可以所有结点的度都 < =m(结点的度最大为m,可以为空树)
  • 度为m的树:至少一个结点的度 = m(至少m+1个结点)

在这里插入图片描述

  • 1:m^0
  • 2:1 * m
  • 3:1 * m * m
  • n:m ^ (n-1)

在这里插入图片描述

  • 在考点3的基础上,等比数列求和

在这里插入图片描述

  • 高h的 “m叉树” 至少结点数:h
  • 高h的 “度为m树” 至少结点数:h+m-1

在这里插入图片描述

  • 借助考点4的知识,高度为h的m叉树的最大结点数
  • 注意!向上取整

在这里插入图片描述

在这里插入图片描述

1.4 二叉树的定义

  • m(m=2)叉树,可以为空(n=2)
  • 注意1:和 “度为2” 的树区别
  • 注意2:二叉树是有序树,那么就说明左右子树不能颠倒

在这里插入图片描述

在这里插入图片描述

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

2.1 几种特殊的二叉树

1.满二叉树

  • 每一层都是满的
  • 只有最后一层有叶子结点
  • 不存在度为1的结点
  • 结点 i 的左孩子为 2i,右孩子为 2i+1

2.完全二叉树

  • 在满2叉树的基础上,从最后一个结点开始去掉结点。
  • 只有最后两层有叶子结点
  • 你可以试一下,只能有一个结点的度为1(或者没有)
  • 结点 i 的左孩子为 2i,右孩子为 2i+1
  • 分支结点 和 叶子结点 的 “分界线” 可以知道 —— n/2

在这里插入图片描述

  • 从后往前删除结点,肯定先删除的右孩子,故剩下的左孩子

在这里插入图片描述

3.二叉排序树

  • 前面介绍的满二叉树、完全二叉树是在形态上的特殊二叉树
  • 二叉排序树是在功能上特殊的二叉排序树
  • 左 < 根 < 右

在这里插入图片描述

4.平衡二叉树

  • |左子树高度(深度)- 右子树| <= 1
  • 从下面的图可以看出来,左边平衡二叉树的搜索效率更高

在这里插入图片描述

在这里插入图片描述

2.2 二叉树的性质

1.二叉树的参考性质

  • 联立下面的两个方程得知:叶子结点的数量 比 度为2结点的数量 多一个

在这里插入图片描述

  • 这个之前介绍过了,等比数列嘛,懂得都懂!

在这里插入图片描述

  • 还是之前介绍过了,等比数列求和

在这里插入图片描述

2.完全二叉树的参考性质

  • 方法一:注意向上取整
  • 等号在哪,往哪取整!

在这里插入图片描述

  • 方法二:注意向下取整
  • 等号在哪,往哪取整!

在这里插入图片描述

  • 完全二叉树中,度为1的结点只能有一个(或者没有)
  • 那么可以知道,结点数为偶数,有一个度为1的结点
  • 结点数为奇数,没有度为1的结点
  • 叶子结点数 - 度为2的结点数 = 1
  • 结合上面的分析,就可以得到下图中的结论

在这里插入图片描述

在这里插入图片描述

2.3 二叉树的存储结构

1.顺序存储

①算法思想

  • tree[0]空着,方便后序操作,例如左孩子(i/2),右孩子(i/2+1)
  • 结点结构体里的 bool 属性,为true说明为空

在这里插入图片描述

②代码实现

#include <iostream>
using namespace std;
// ? 本代码实现对二叉树的顺序存储结构
// ? 适合存储完全二叉树的情况

#define MAX_SIZE 100
// ! 顺序存储结点的数组结点
struct TreeNode
{
  int value;  // 结点中的数据元素
  bool isEmpty; // 结点是否为空 (true为空)
};

// ! 初始化静态数组树
// ! 数组当形参时,就是传引用
void InitTreeNode(TreeNode t[])
{
  for (int i = 0; i < MAX_SIZE; i++)
  {
 	  t[i].isEmpty = true; // 初始化时所有结点标记为空
  }
}

int main()
{
  TreeNode t[MAX_SIZE]; // 声明一个顺序存储表
  InitTreeNode(t);   // 初始化顺序存储表
  // ! 此时t就相当于一颗二叉树
}

③算法分析

  • 这些我就不一一介绍了
  • 需要注意一下第i个结点所在的层次,看不懂的话就去看 “二叉树的性质” 那一小节

在这里插入图片描述

  • 顺序存储 对于 非完全二叉树 的存储情况很不友好

在这里插入图片描述

2.链式存储

①算法思想

  • !!!n个结点的二叉链表共有n+1个空链域(你数数下图中空指针,发现确实是n+1个)
  • !!!这些空指针域是有大用处的,具体在线索二叉树那块介绍

在这里插入图片描述

②代码实现

// ? 链式存储的结点结构体
typedef struct BiTNode
{
    ElemType data;
    struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;

// 定义一颗空树
BiTree root = NULL;
root = (BiTree)malloc(sizeof(BiTNode));

// 定义一个二叉树结点
BiTnode *p = (BiTNode *)malloc(sizeof(BiTNode));

/*
	从上面可以看出 BiTNode 和 *BiTree的区别。
	
	分析下 root
		1、它并不是二叉树结点,它是指向一个二叉树结点。
		2、它不仅仅代表一个二叉树结点,实际象征着一棵链式二叉树
	
	p才真正代表一个二叉树结点指针
*/

在这里插入图片描述

③算法分析

  • 链式存储的结点里,因为存在左、右孩子指针,所以找左、右孩子十分方便
  • 对比顺序存储找左、右孩子(完全二叉树情况),顺序存储是(i/2,i/2+1),也挺方便的
  • 但是,找父结点就不太方便了(考研中一般考不带父指针的情况)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

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

3.1 遍历二叉树

1.算法思想

  • 先序遍历:根 --> 左 --> 右
  • 中序遍历:左 --> 根 --> 右
  • 后序遍历:左 --> 右 --> 根

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

2.代码实现 - 先中后

#include <iostream>
using namespace std;
// ? 本代码实现二叉树DualTree的链式存储
// ! 实验数据: -+a##*b##-c##d##/e##f##

// ! 二叉树结点
typedef struct BiTNode
{
    char data;                       // 数据域
    struct BiTNode *lchild, *rchild; // 指针域:左、右孩子指针
} BiTNode, *BiTree;

// ! 初始化二叉树
void InitTreeRoot(BiTree &root)
{
    root = NULL;
}

// ! 添加新结点
void InsertBiTNode(BiTree &root)
{
    char ch;
    cin >> ch;
    if (ch == '#')
    {
        root = NULL;
    }
    else
    {
        root = (BiTree)malloc(sizeof(BiTree));
        root->data = ch;
        InsertBiTNode(root->lchild);
        InsertBiTNode(root->rchild);
    }
}

// ! 先序遍历
void ProOrder(BiTree root)
{
    if (root == NULL)
    {
        return;
    }
    cout << root->data << " ";
    ProOrder(root->lchild);
    ProOrder(root->rchild);
}

// ! 中序遍历
void MiddleOrder(BiTree root)
{
    if (root == NULL)
    {
        return;
    }
    MiddleOrder(root->lchild);
    cout << root->data << " ";
    MiddleOrder(root->rchild);
}

// ! 后序遍历
void PostOrder(BiTree root)
{
    if (root == NULL)
    {
        return;
    }
    PostOrder(root->lchild);
    PostOrder(root->rchild);
    cout << root->data << " ";
}

int main()
{
    BiTree biTree;
    InitTreeRoot(biTree);
    InsertBiTNode(biTree);
    cout << "先 序 遍 历: ";
    ProOrder(biTree);
    cout << endl
         << "中 序 遍 历: ";
    MiddleOrder(biTree);
    cout << endl
         << "后 序 遍 历: ";
    PostOrder(biTree);
    cout << endl
         << "树的深度:" << TreeDepth(biTree);
    return 0;
}

3.先序遍历

  • visit(BiTree T) 函数可以定义一下自己想要的操作,例如打印输出

在这里插入图片描述

  • 空间复杂度:O( h + 1 ),h为高度,+1是处理最后的空结点(+1可以舍去)

在这里插入图片描述

在这里插入图片描述

4.中序遍历

在这里插入图片描述

在这里插入图片描述

5.后序遍历

在这里插入图片描述

在这里插入图片描述

6.树的深度

// ! 求树的深度
int TreeDepth(BiTree root)
{
    if (root == NULL)
    {
        return 0;
    }
    int l = TreeDepth(root->lchild);
    int r = TreeDepth(root->rchild);
    return l > r ? l + 1 : r + 1;
}

在这里插入图片描述

7.层次遍历

①算法思想

在这里插入图片描述

②代码实现

// ? 下面实现二叉树的层序遍历

// ! 链式队列结点
typedef struct LinkNode
{
    BiTNode *data; // 此处代表的含义是指针,不是结点;与二叉树不同,看下图对比
    struct LinkNode *next;
} LinkNode;

// ! 辅助队列
typedef struct
{
    LinkNode *front, *rear; // 队列的对头、队尾
} LinkQueue;

void LevelOrder(BiTree root)
{
    LinkQueue Q;
    // ……
}
  • 使用辅助队列,而且并不是存入结点,而是存入指向该结点的指针

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

3.2 根据遍历序列确定二叉树

1.单个遍历序列确定二叉树

  • ① 中序遍历可以对应多种形态 (不唯一)

在这里插入图片描述

  • ② 前序遍历可以对应多种形态 (不唯一)

在这里插入图片描述

  • ③ 后序遍历可以对应多种形态 (不唯一)

在这里插入图片描述

  • ④ 层序遍历也会对应多种不同形态的二叉树(不唯一)

在这里插入图片描述

  • ⑤ 结论 - 关键:中序遍历

在这里插入图片描述

2.算法思想 - 举例

  • 前序第一个元素,A为根结点,那么可以把中序序列划分成左、右子树了
  • 并且通过中序序列,可以知道左、右子树各自的结点数(看A的位置)
  • 元素D,为左子树的根结点,再进一步划分

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

3.小结

  • 在这4种构造二叉树的问题中,其实都是对 中序序列 的划分
  • 使用 前序 去划分中序
  • 使用 后序 去划分中序
  • 使用 层序 去划分中序
  • !!!这一小节老师没有提到代码,课后自己可以尝试补充!!!

在这里插入图片描述

  • 没有中序序列,不能构造二叉树

在这里插入图片描述

3.3 线索二叉树

1.前言

  • 之前二叉树的缺点
    • 1、必须从根结点开始遍历,不能从指定结点开始
    • 2、找前驱、后继很麻烦(从头遍历 + 辅助指针pre)
  • 为什么找后继也很麻烦呢?
    • 后继和孩子结点不同
    • 因为我们此时是从中序遍历序列去指定一个结点,例如G,如果要找G的后继:
      • 1、需要sub指针
      • 2、当q指针找到p(q==p,有传入p指针当参数的情况),sub才能记录p的后继
      • 3、如果没有插入p指针,就对比结点里的数据是否相等
  • 想要下图的过程代码,跳到5.3.4二叉树线索化 1.老办法小节

在这里插入图片描述

2.线索二叉树的基本概念

  • 之前在二叉树的链式存储小结提到:n个结点的二叉树有n+1个空链域,就是用在二叉树线索化

  • 左孩子指针 - 存储前驱线索

  • 右孩子指针 - 存储后继线索

  • 注意了!!!注意,此时我们提到的前驱、后继是中序遍历序列里的前驱、后继。和二叉树结点的前驱(双亲)和后继(子孙)不一样。

  • 所以,看下面的图中,D的左孩子指针为NULL,因为D在中序遍历序列里面没有前驱

  • 再一次注意了!有右孩子的时候,右孩子也不一定是后继,比如AC,A的后继是F

  • 再举个例子,看BE,如果E的左子树不为空,那么B的后继就不是E了

  • 接着上一行的问题,如果一个结点的右孩子指针指向右孩子,而不是后继线索,那么如何查找后继呢?

  • 答案会在5.3.5 线索二叉树的使用里介绍

在这里插入图片描述

3.线索二叉树的存储结构

// 左、右线索标志
typedef struct ThreadNode{
	ElemType data;
    struct ThreadNode *lchild, *rchild;
    int lTag, rTag;	// 0指向孩子;1指向线索
}
// 此时可以叫做线索链表

在这里插入图片描述

  • 从1到3小节,都是以“中序线索二叉树的构造”举例介绍线索二叉树的思想,下一小节里就只介绍先序、后序线索二叉树了

在这里插入图片描述

4.线索二叉树的构造

  • ① 先序线索二叉树

在这里插入图片描述

在这里插入图片描述

  • ② 后序线索二叉树

在这里插入图片描述

在这里插入图片描述

  • ③ 三种线索二叉树的对比

在这里插入图片描述

在这里插入图片描述

3.4 二叉树线索化

1.老办法

  • 如果对于老办法还不太清楚的话,去5.3.3线索二叉树 1.前言小节,有具体介绍
// 辅助全局变量,用于查找结点p的前驱
BiTNode *;				// p指向目的结点
BiTNode *pre = NULL;	// 指向点前访问结点的前驱
BiTNode *final = NULL;	// 用于记录最终结果

// 访问结点q
void visit(BiTNode *q) {,
    if (q==p)
        final = pre;
    else
        pre = q;
}
// 寻找中序前驱就少不了中序遍历
void FindPre(BiTree T) {
    if (T!=NULL) {
        InOrder(T->lchild);
        visit(T);
        InOrder(T->rchild);
    }
}

在这里插入图片描述

2.中序线索化

这里重点介绍中序线索化,后面的先序、后序线索化思想类似,就不会再大量描述了

①算法思想

  • 线索二叉树的结点结构体里有标志位属性
  • 最后线索化结束了,还没有将C的右孩子指针还没线索化
  • 可以借助pre是全局变量的特性,并且最后pre就是指向C结点,通过pre对rchild设置为NULL,rTag也得为1,因为这是一颗被线索化过的二叉树

在这里插入图片描述

②代码实现

在这里插入图片描述

③算法分析

  • 思考:处理遍历的最后一个结点(结点i)时,为什么没有判断rchild是否为空?
  • 回答:中序遍历的顺序:左-根-右。如果最后遍历的结点有右孩子,那么还会接着遍历,因为是左-根-右。此时结点i就是根,左孩子已经先一步遍历完了。
  • 上面的解释如果觉得看不懂得话,就换一种思路,问:最后一个结点可以有左、右孩子吗?
    • 左孩子,可以。因为遍历顺序左-根-右,最后一个结点得左孩子已经遍历完了,不影响。
    • 右孩子,不可以。因为还得接着遍历,这样这个结点就不是最后一个结点了,矛盾。

在这里插入图片描述

3.先序线索化

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

4.后序线索化

在这里插入图片描述

在这里插入图片描述


在这里插入图片描述

3.5 线索二叉树的使用

1.中序线索二叉树

①找中序后继

  • 在中序线索二叉树中,如果p->rTag==1,右孩子指针被线索化了,那么直接得到中序后继
  • 若p->rTag==0,有右孩子。就要找右子树得中序遍历最左边的结点

在这里插入图片描述

  • FirstNode(ThreadNode *p)函数:得到以p为根的子树中,第一个被中序遍历的结点(会去找左下角)

  • NextNode(ThreadNode *p)函数:得到p结点在中序遍历里的后继结点

  • 那么借助上面两个函数就能对二叉树进行中序遍历了:

    • 1、先使用第一个函数得到第一个被中序遍历的结点
    • 2、以这个结点开始,给第二个函数传入传参数,得到一下个中序遍历的结点,以此类推
  • 这种使用for循环的中序遍历二叉树,空间复杂度只需要O(1)

在这里插入图片描述

②找中序前驱

  • 和找中序后序的思想差不多,就不再写了
  • 找左子树的最右边的结点(左-根-右)

在这里插入图片描述

在这里插入图片描述

2.先序线索二叉树

①找先序后继

  • 1、当rTag为1,被线索化,直接指向先序后继
  • 2、当rTag为0,指向右孩子,可以知道p有右孩子,但不确定有没有左孩子。此时就需要分情况讨论了:
    • ① 有左孩子,先序后继就是左孩子自己(根-左-右)
    • ② 没有左孩子,先序后继就是右孩子自己(根-左-右)

在这里插入图片描述

②找先序前驱

  • ltag==1,左孩子被线索化,直接得到先序前驱
  • ltag==0,有左孩子。根据先序遍历的顺序(根-左-右),p的左、右子树遍历的元素都只能是p的后继,故得不到p的前驱。
  • 硬要找的话,只能使用土办法

在这里插入图片描述

  • 上面说到先序线索二叉树找先序前驱实现不了,那如果加一个条件,多一个p的父结点,此时还能找到p的先序前驱吗?—— 看一下图
  • 下图中红色的结点就是p的位置,下图分了4种p可能出现的位置讨论

在这里插入图片描述

3.后序线索二叉树

①找后序后继

  • 1、当rTag为1,被线索化,直接指向后序后继
  • 2、当rTag为0时,有右孩子。因为后序遍历(左-右-根),那么左、右子树结点只能是p的前驱,所以无法再后序线索二叉树种找到p的后序后继。
  • 3、硬找的话:土办法

在这里插入图片描述

  • 上面说到后序线索二叉树找先序后继实现不了,那如果加一个条件,多一个p的父结点,此时还能找到p的先序后继吗?—— 看一下图
  • 下图中红色的结点就是p的位置,下图分了4种p可能出现的位置讨论

在这里插入图片描述

②找后序前驱

  • 1、lTag==1,左孩子被线索化,直接得到后序前驱
  • 2、lTag==0,有左孩子结点。因为不清楚有没有右孩子,需要分情况讨论:
    • ① 、有右孩子,根据后序遍历顺序(左-右-根p),p的后序前序就是它的右孩子
    • ② 、没有右孩子,(左-空-根p),p的后序前驱就是它的左孩子
    • ③ 、①和②虽然都得再一次分别对右子树、左子树进行后序遍历,但根据(左右根),最靠近p的还是左、右儿子结点

在这里插入图片描述

4.小结

在这里插入图片描述

在这里插入图片描述

4 树和森林

4.1 树的存储结构

在这里插入图片描述

1.树的逻辑结构回顾

在这里插入图片描述

2.双亲表示法 - 顺序

①算法思想

  • 双亲表示法,注意和之前二叉树的顺序存储(适用于完全二叉树)之间的区别

在这里插入图片描述

②增、删操作

 增:直接在存储数组的末尾添加元素即可,不需要按照层序遍历顺序

在这里插入图片描述

 删:直接将该结点的数据域清空,指针域设置为-1 这种删除方案有缺陷,如果删除过多元素的话,那么使得数组里的空数据较多,减低检索速率

在这里插入图片描述

  • 清空数组表里对于元素的数据域和指针域,再把最底下的元素上移填充空白
  • 查找操作:
    • ① 查找p的双亲时,十分简单
    • ② 查找p的子孙时,需要将数组遍历,查找双亲是p的结点,很不方便

在这里插入图片描述

  • 当删除分支结点的时候,不只是删除一个结点那么简单,得把子孙结点通通删除

在这里插入图片描述

在这里插入图片描述

3.孩子表示法 - 顺序 + 链式

  • 有几个结点,数组就有几个元素
  • 数组元素的指针域指向第一个孩子结点
  • 孩子结点结构(数据域,指针域)

在这里插入图片描述

// 链结点
struct CTNode {
    int child;		// 孩子结点在数组的位置
    struct CTNode *next;	// 下一个孩子
};
// 数组结点
typedef struct {
    ElemType data;
    struct CTNode *firstChild;	// 第一个孩子
}  CTBox;
// 孩子表示法的树结构
typedef struct {
    CTBox nodes[TREE_INIT_SIZE];
    int n, r;	// 记录结点数和根的位置
} CTree;
  • 找孩子方便,找双亲不太方便

在这里插入图片描述

4.孩子兄弟表示法

  • 乍一看孩子兄弟表示法的结点结构 和 二叉链表的结点结构,不能说毫不相关吧,只能说是一模一样!
  • 它们的区别就在与指针域所表示的意义不同
typedef struct BiTNode{  ElemType data;  struct BiTNode *lchild, *rchild;	// 左、右孩子指针} BiTNode, *BiTree;typdef struct CSNode{    ElemType data;    struct CSNode *firstChild,*nextSibling;	// 第一个孩子 和 右兄弟指针} CSNode, *CSTree;
  • 在这也可以初步认识 树 --> 二叉树 的转换问题

在这里插入图片描述

在这里插入图片描述

5.树、森林与二叉树的转换

  • 1、先将森林里的各棵树使用上面的 “孩子兄弟表示法” 转换一下
  • 2、在将转换后的各棵树再使用 “孩子兄弟表示法” 连接起来

在这里插入图片描述

  • 懂了森林如何转换成二叉树,那么反过来转换也差不多明白了

在这里插入图片描述

6.小结

在这里插入图片描述

4.2 树和森林的遍历

在这里插入图片描述

1.树的遍历

  • 回顾下树的逻辑结构
  • 注意!!!树没有中根遍历,和之前二叉树的中序遍历不一样,因为树可以有很多个分支,怎么中根呢?

在这里插入图片描述

①先根遍历

  • 重要结论:树的 先根遍历序列 和 对应的二叉树 的先序序列相同

在这里插入图片描述

②后跟遍历

  • 重要结论:树的 后根遍历序列 和 对应的二叉树 的中序序列相同 ,对!你没有看错,是中序序列

在这里插入图片描述

③层次遍历

  • 层次遍历就是广度优先遍历
  • 上面的先根、后根遍历就是深度优先遍历

在这里插入图片描述

2.森林的遍历

①先序遍历

  • 森林的先序遍历
  • 方法一:依次地对树进行先根遍历(就树比较奇葩,叫先跟,不是先序)

在这里插入图片描述

  • 森林的先序遍历 - 方法二:
  • 1、先将森林转换成对应的二叉树
  • 2、对二叉树进行先序遍历

在这里插入图片描述

②中序遍历

  • 方法一:依次地对树进行后序遍历注意!!!森林中序 = 树后根遍历

在这里插入图片描述

  • 森林的中序遍历 - 方法二:
  • 1、先将森林转换成对应的二叉树
  • 2、对二叉树进行中序遍历 (注意!!!森林中序 = 二叉树中序

在这里插入图片描述

3.小结

在这里插入图片描述

5 树和二叉树的应用

5.1 二叉排序树(BST)

在这里插入图片描述

1.定义

  • 二叉排序树:所有结点满足(左 < 根 < 右)

在这里插入图片描述

2.查找

  • ① 非递归方式实现二叉排序树的查找

在这里插入图片描述

在这里插入图片描述

  • ② 递归方式实现二叉排序树的查找
  • O(h)空间复杂度:树高几层 就会 递归几层

在这里插入图片描述

3.插入

  • 1、新插入的结点一定是叶子结点(规定的)
  • 2、二叉排序树不存在两个关键字相同的结点,如果插入这样的新结点,则插入失败
  • 3、为什么插入函数的参数是传引用呢?
  • 答:插入函数是先从根结点开始遍历,去寻找合适的位置插入,当找到“11结点”时,需要对该结点的右孩子指针进行修改,那他指向先插入的“12结点”,所以需要传引用参数
  • 递归插入最坏空间复杂度:O(h)和树高有关

在这里插入图片描述

4.构造

  • 如何构造二叉排序树呢?
  • 答:实际上就相当于不断插入新结点的过程

在这里插入图片描述

  • 有时不同的关键字序列 可以得到 相同的二叉排序树
  • 但也可能得到不同的二叉排序树

在这里插入图片描述

5.删除

  • ① 删除度为0(叶子结点),直接删除

在这里插入图片描述

  • ② 删除度为1的结点时,让其子树替代即可

在这里插入图片描述

  • ③ 删除度为2的结点时,有两种补救措施:
  • 1、选择直接后继,右子树最左下结点
  • 看下图,既然找到的p是最左下结点,那么它一定没有左子树,可知它的度为1
  • 那么p被移动到z除,原先的位置空着,这就相当于上面的删除度为1的结点

在这里插入图片描述

  • 2、选择之前前驱,左子树最右下结点
  • 看下图,既然找到的p是最右下结点,那么它一定没有右子树
  • 而且此时p的度为0,这就相当于上面的删除度为0的结点,直接删除即可

在这里插入图片描述

6.查找效率分析

  • 平衡二叉排序树的查找效率较高
  • 自己分析下如何得到最好、最坏情况下的平均查找长度

在这里插入图片描述

  • 查找失败的平均查找情况:
  • 查找失败时,指针会停留在最底下的空链域处

在这里插入图片描述

7.小结

在这里插入图片描述

5.2 平衡二叉树(AVL)

1.定义

  • 平衡二叉树经常在选择题里考察
  • 专有名词:平衡因子 = 左 - 右(-1,0,1)
typdef struct AVLNode{	int key;		// 数据域    int balance;	// 平衡因子    struct AVLNode *lchild, *rchild;	// 左、右孩子指针}

在这里插入图片描述

2.插入

  • 当平衡二叉树插入新结点的时候,可能会破坏原本二叉树的平衡,那么就需要去重新调整二叉树,使其恢复平衡
  • 解决办法:去调整**“最小”不平衡子树**即可
  • 那么怎么找到最小不平衡子树呢?
  • 答:以下图举例,才67开始从下往上找,当找到哪个结点的平衡因子不是(-1,0,1)时,也就是找到第一个不平衡结点,这个不平衡结点就是最小不平衡子树根结点

在这里插入图片描述

在这里插入图片描述

3.调整最小不平衡子树A

  • 根据新结点插入位置的不同,分成下面四种情况:

在这里插入图片描述

①LL

  • 本小结中的A,我们设定是当插入新结点导致不平衡,A就刚好是不平衡子树的根结点,也就是第一个不平衡结点
  • 下面分析下下图中的思考题:为什么要假定所有子树(AR,BL,BR)的高度都是H?
    • ① 如果AR为H+1,当BL插入新结点时,A = (H+2) - (H+1) = 1,仍平衡,矛盾
    • ② 如果BR为H-1,此时B = H -(H-1)= 1,BL再插入新结点,B=2,B变成了不平衡因子了
    • 其他情况类似,所以只有当(AR,BL,BR)的高度都为H时,才会发生LL不平衡
  • LL调整方法:
  • 还是看图吧,解释太麻烦了

在这里插入图片描述

②RR

  • 在A的右孩子的右子树插入导致的不平衡
  • LL和RR有点类似,可以搭配对比记忆

在这里插入图片描述

// LL情况
// 需要调整的指针:B->rChilde,A->lChild
// p孩子 f父亲 gf爷爷
// 注意下面代码顺序
f->lChild = p->rChild;
p->rchild = f;
pf->child = p;

// RR情况类似,自己看图
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在这里插入图片描述

③LR

  • 在A的左孩子的右子树中插入新结点导致不平衡
  • 在C的左、右插入新结点,情况都一样
  • 左旋 + 右旋

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

④RL

  • 在A的右孩子的左子树插入新结点导致不平衡
  • 右旋 + 左旋

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

⑤小小小结

  • 只有左孩子才能右旋
  • 只有右孩子才能左旋

在这里插入图片描述

⑥扩展

  • 解释:为什么只要处理最小不平衡子树,整棵二叉树就都平衡了?

在这里插入图片描述

  • 举例论证

在这里插入图片描述

在这里插入图片描述

  • 结论:插入操作导致“最小不平衡子树的高度+1”,经过平衡处理之后高度恢复

在这里插入图片描述

⑦练习

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • 做熟练的话就不需要每次都选择去得到平衡二叉树。
  • 1、60的左子树 --> 50的右子树
  • 2、60的右子树 --> 66的左子树
  • 3、60充当根结点,连接50和66(记得把55和66之间的线断掉)

在这里插入图片描述

4.平衡二叉树的查找

  • 现在推导高h的平衡二叉树最小有几个结点:
高h最小结点数n
00
11
22
34
47
hn(h-1)+ n(h-2)+ 1 【下面两层加一】

在这里插入图片描述

  • 一定要记住平衡二叉树最小结点数的递推式
  • 通过n(h)的表达式,我们可以得到平衡二叉树的最大深度、树高

在这里插入图片描述

5.小结

在这里插入图片描述

5.3 哈夫曼树

1.带权路径长度

  • 结点的权:结点里有个权值属性
  • 结点的带权路径长度:从根结点到该结点的路径长度 * 权值
  • 树的带权路径长度:所有叶结点的带权路径长度之和(注意!!!只算叶子结点

在这里插入图片描述

2.定义

  • 哈夫曼树:带权路径长度(WPL)最小的二叉树

在这里插入图片描述

3.构造

  • 构造哈夫曼树的步骤:
    • 1、选权值最小的两个结点
    • 2、在剩下的结点中挑一个最小的结点继续结合;或者挑两个结点先结合
  • 哈夫曼树的一些性质:
  • ① 结点总数为2 * n -1
  • ② 不存在度为1的结点
  • ③ 哈夫曼树不唯一

在这里插入图片描述

  • n个带权的叶子结点 可以 构造出多种不同形态的哈夫曼树

在这里插入图片描述

4.举例

  • 使用哈夫曼编码,减少发生二进制位长度

在这里插入图片描述

  • 再一次压缩,解码出问题了,有歧义

在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值