bookstack

基本定义和术语

  • 树是n个节点的有限集合
  • 空树:节点数为0的树
  • 非空树:节点数大于0的树

非空树

  • 有且仅有一个根节点
  • 没有后继的节点称为 叶子节点
  • 有后继的节点称为 分支节点
  • 除了根节点外,任何一个节点有且仅有一个前驱
  • 每个节点可以有0个或多个后继

术语

  • 祖先节点:比自己深度低的节点
  • 子孙节点:比自己深度高的节点
  • 父节点:自己的根节点
  • 孩子节点:自己作为根节点的儿子
  • 兄弟节点:与自己拥有相同父节点的节点
  • 堂兄弟节点:与自己同一层的节点
  • 路径:能从上往下前往的两个节点被称为有路径
  • 路径长度:经过几条边
  • 深度:从上往下数,默认从1开始
  • 节点高度:从下往上数
  • 数的高度:总过有多少层
  • 节点的度:有几个孩子(分之)
  • 树的度:各节点的度的最大值

树的性质

  • 树的总节点数 = 树的总度数 + 1
  • 度为m的树第i层至多有 $m^i -1$ 个节点(i >= 1)
  • m叉树第i层至多有 $m^i -1 $个节点(i>=1)
  • 高度为h的m叉树之多有 $\frac{(m^h - 1)} { m } -1 $个节点
  • 高度为h的m叉树之少有h个节点
  • 高度为h,度为m的树之少有h + m - 1个节点
  • 具有n个节点的m叉树的最小高度为 $\log_m(n(m-1) + 1)$
  • 高度最小的情况:所有节点都有m个孩子

二叉树的定义和基本术语

  • 每个节点至多只有两棵子树,且子树有左右之分

特殊的二叉树

满二叉树:每层都是满的。假设高度为3,满二叉树节点就是7个

     1
    / \
   2   3     <== 这就是满二叉树
  / \ / \
 4  5 6  7

完全二叉树:满二叉树删除最后x个节点,比如一个完全二叉树高度是3,下面都是完全二叉树

     1           1           1
    / \         / \         / \
   2   3       2   3       2   3
  / \ /       / \         /
 4  5 6      4  5        4

二叉排序树: 任何一个节点,左子树比它小,右子树比它大

     5           5             5
    /           / \           / \
   3           3   7         3   7   ......好多好多
  /           /     \       /   /
 1           1       10    1   6

平衡二叉树: 树上任一节点的左子树和右子树的深度之差不超过1

     5       👈这是二叉排序树😋                3
    /        但不是平衡二叉树                  / \
   3         你可以理解为左轻右重             1   5
  /          很不美观,调整后👉
 1

二叉树性质

  • 设非空二叉树中度为0,1和2的节点个数分别为n0, n1, n2,则n0 = n2 + 1(叶子节点比二分支节点多一个)
  • 二叉树第i层至多有 $2^{n-1}$ 个节点
  • 高度为h的二叉树至多有 $2^h - 1$ 个节点(满二叉树)

完全二叉树性质

  • 具有n个(n>0)结点的完全二叉树的高度h为 $\log_2(n + 1)$ 或 $[log_2n] + 1$
  • 对于完全二叉树,可以由的结点数n 推出度为0、1和2的结点个数为n0、n1和n2(突破点:完全二叉树最多只会有一个度为1的结点)

二叉树的存储结构

顺序存储

  • i的左孩子:2i
  • i的右孩子:2i + 1
  • i的父节点:i / 2
  • i所在的层次:$\log_2(n + 1)$ 或者 $\log_2n + 1$

完全二叉树有n个节点

  • 判断i是否有左孩子:2i <= n
  • 判断i是否有右孩子: 2i + 1 <= n
  • 判断i是否是叶子/分支节点: i > n / 2

链式存储

  • 可以简单找到p节点的左右节孩子,但只能通过从根开始遍历找到p的父节点
  • 可以多定义一个父节点指针来方便查找父节点

二叉树遍历

  • 层序遍历:基于树的层次特性确定的次序规则
  • 前序遍历:根左右
  • 中序遍历:左根右
  • 后序遍历:左右根

前序遍历

中序遍历

后序遍历

层序遍历

  • 初始化辅助队列
  • 根节点入队列
  • 若队列非空,则队头节点处队,访问该节点,并将左右孩子插入队尾
  • 重复第三步,直到队列为空

构造二叉树

  • (先序 + 中序) => 唯一的二叉树
  • (后序 + 中序) => 唯一的二叉树
  • (层序 + 中序) => 唯一的二叉树

二叉排序树

  • 二叉排序树,又称二叉查找树
  • 一棵二叉树或者是空二叉树,或者是具有如下性质二叉树 左子树节点值 < 根节点 < 右子树节点值
  • 中序遍历,可以得到一个递增的有序序列

查找

  • 目标值与根节点的值比较
  • 相等,则查找成功
  • 小于根节点,则在左子树查找,否则在右子树查找
  • 查找成功,返回节点指针
  • 查找失败,返回NULL
  • 递归实现最坏空间复杂度为O(h)

非递归

// 在二叉排序树中查找值为key的结点
BSTNode *BST_Search(BSTree T, int key) {
    // 若树空或等于根结点值,则结束循环
    while (T != NULL && key != T->key) {
        // 小于,则在左子树上查找
        if (key < T->key) T = T->lchild;
        // 大于,则在右子树上查找
        else T = T->rchild;
    }
    return T;
}

递归方式

// 在二叉排序树中查找值为key的结点(递归实现)
BSTNode *BSTSearch(BSTree T, int key) {
    if (T == NULL)
        // 查找失败
        return NULL;
    if (key == T->key)
        // 查找成功
        return T;
    else if (key < T->key)
        // 在左子树中找
        return BSTSearch(T->lchild, key);
    else
        // 在右子树中找
        return BSTSearch(T->rchild, key);
}

插入

  • 二叉树为空,直接插入节点
  • 若关键字k小于根节点,插入左子树,否则插入右子树
// 在二叉排序树插入关键字为k的新结点(递归实现)
int BST_Insert(BSTree &T, int k) {
    // 原树为空,新插入的结点为根结点
    if (T == NULL) {
        T = (BSTree)malloc(sizeof(BSTNode));
        T->key = k;
        T->lchild = T->rchild = NULL;
        // 返回1,插入成功
        return 1;
    }
    // 树中存在相同关键字的结点,插入失败
    else if (k == T->key)
        return 0;
    // 插入到T的左子树
    else if (k < T->key)
        return BST_Insert(T->lchild, k);
    // 插入到T的右子树
    else
        return BST_Insert(T->rchild, k);
}

构造

// 按照 str[] 中的关键字序列建立二叉排序树
void Creat_BST(BSTree &T, int str[], int n) {
    // 初始时T为空树
    T = NULL;
    int i = 0;
    // 依次将每个关键字插入到二叉排序树中
    while (i < n) {
        BST_Insert(T, str[i]);
        i++;
    }
}

删除

  • 先搜索找到目标节点
  • 如果删除节点是叶节点,直接删除
  • 如果只有一棵左子树或右子树,则让节点的子树称为z的父节点的子树
  • 如果有两棵子树,令z的直接后继(或直接前驱)代替z,然后从二叉树中删除这个直接后继(或直接前驱)
  • 如果是后继,z的右子树最左下节点是右子树最小的节点
  • 如果是前驱,z的左子树最右下节点是左子树最大的节点

查询效率分析

  • 平均查找长度:各层(层数*层节点个数)相加 / 总节点个数
  • 最坏情况:每个节点只有一个分支,树高h = 节点数n。平均查找长度O(n)
  • 最好情况:n个节点的二叉树最小高度为 $\log_2n + 1$。 平均查找长度 = O($\log_2n$)

平衡二叉树

定义

  • 节点的平衡因子 = 左子树高度 - 右子树高度
  • 平衡二叉树节点的平衡因子只可能是 -1, 0, 1
  • 只要有任一节点的平衡因子绝对值大于1,就不是平衡二叉树

平均查找长度为O($\log_2n$)

插入

  • 查找路径上所有节点都有可能受到影响
  • 从插入点往回找到第一个不平衡节点,调整以该节点为根的子树
  • 每次调整对象都是最小不平衡子树
  • 在插入操作中,只要将最小不平衡子树调整平衡,其他祖先节点都会恢复平衡
  • 插入操作导致最小不平衡子树高度+1,经过调整后,高度恢复

调整最小不平衡树

  • LL:在A的左孩子的左子树中插入导致不平衡
  • RR:在A的右孩子的右子树中插入导致不平衡
  • LR:在A的左孩子的右子树中插入导致不平衡
  • RL:在A的右孩子的左子树中插入导致不平衡

LL平衡旋转

  • 由于在A的左孩子的左子树上插入了新节点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要一次右旋转操作
  • 将A的左孩子B向右上旋转代替A称为根节点
  • 将A节点向右下旋转称为B的右子树的根节点
  • B的原右子树则作为A节点的左子树

RR平衡旋转

  • 由于在节点A的右孩子的右子树上插入了新节点,A的平衡因子由-1减至-2,导致以A为根的子树失去平衡,需要一次向左旋转操作
  • 将A的右孩子B向左上旋转代替A称为根节点
  • 将A节点向左下旋转称为B的左子树的根节点
  • B的原左子树作为A节点的右子树

LR平衡旋转

  • 由于在A的左孩子的右子树插入了新节点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要进行两次旋转操作,先左旋转后右旋转
  • 将A节点的左孩子的右子树的根节C点向左上旋转提升至节B节点的位置
  • 再把该C节点向右上旋转提升到A节点的位置

RL平衡旋转

  • 由于在A的右孩子(R)的左子树(L)上插入新结点,A的平衡因子由-1减至-2,导致以A为根的子树失去平衡,需要进行两次旋转操作,先右旋转后左旋转
  • 将A结点的右孩子B的左子树的根结点C向右上旋转提升到B结点的位置
  • 然后再把该C结点向左上旋转提升到A结点的位置

哈夫曼树

定义

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

构造

  • 将这n个节点分别作为n棵仅含一个节点的二叉树,构成森林F
  • 构造一个新节点,从F中选取两棵根节点权值最小的树作为新节点的左右子树
  • 将新节点的权值置为左右子树上根节点的权值之和
  • 从F中删除刚才选的两棵树,同时将新的到的树加入F中
  • 重复2~4,直到F中只剩一颗树为止

特点

  • 每个初始节点最终都称为叶节点,且权值越小的节点到根节点的路径长度越大
  • 哈夫曼树的节点总数为2n-1
  • 哈夫曼树中不存在度为1的节点
  • 哈夫曼树并不惟一,但WPL必然相同并最优

哈夫曼编码

  • 固定长度编码:每个字符用相等长度的二进制表示
  • 可变长度编码:允许对不同字符用不等长的二进制表示
  • 若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码。如果没有前缀编码容易出现歧义
  • 由哈夫曼树的到哈夫曼编码:字符集中的每个字符作为一个叶子节点,各个字符出现的频率作为节点的权值,根据之前的方法构建哈夫曼树

并查集

定义

  • 逻辑结构集合的一种具体实现,只进行并和查两种操作

初始化

实际上就是树的双亲表示法,里面的值就是自己对应的根节点下标

并、查

并的优化

  • 每次union操作构建树的时候,尽量让树不长高
  • 用根节点的绝对值表示树的节点总数
  • union操作让小树合并到大树
  • 查的最坏时间复杂度变为O($\log_2n$)

压缩路径

  • 核心思想就是让树越来越矮
  • Find操作先找到根节点,再将查找路径上所有节点都挂到根节点下
// Find "查"操作优化,先找到根节点,再进行"压缩路径"
int Find(int S[], int x) {
    int root = x;
    // 循环找到根
    while (S[root] >= 0) root = S[root];
    // 压缩路径
    while (x != root) {
        // t指向x的父节点
        int t = S[x];
        // x直接挂到根节点下
        S[x] = root;
        x = t;
    }
    // 返回根节点编号
    return root;
}
  • 这样可以让树的高度不超过O($\alpha(n)$)
  • O($\alpha(n)$)是一个增长很慢的函数,对于常见的n值,O($\alpha(n)$)通常<=4
  • 因此优化后的并查集操作时间开销都比较低