平衡树与二叉搜索树 - 二叉搜索树

199 阅读4分钟

简介

二叉搜索树(BST)用于快速增删改查数据,对外部可以看作一个集合。BST 的平均时间复杂度是 O(logn)O(\log{n}),但是遗憾的是其最坏时间复杂度是 O(n)O(n) 的。为了解决 BST 的这个问题,之后会介绍多种平衡树。平衡树通过不同的维护方式,保证了其始终有 O(logn)O(\log{n}) 的时间复杂度,BST 是所有平衡树的基础,同时其多数操作在多数平衡树中是通用的。

模板题链接

只有最后一组数据会 TLE。

洛谷 P3369 【模板】普通平衡树

核心思想

定义: 对于一棵有根树的任意节点,如果满足左子树所有节点的权值均小于该节点的权值,且右子树均大于该值,则称之为 BST。即对于任意节点 xx 有权 max{kl}<k<min{kr}\max\{k_l\} < k < \min\{k_r\},其中 kk 表示节点 xx 的权值,{kl}\{k_l\} 表示 xx 左子树权值的集合。一般不允许 BST 包含相同权值的元素。

性质:

  • max{kl}<k<min{kr}\max\{k_l\} < k < \min\{k_r\}

  • BST 的中序遍历即为树上所有元素的升序排列。

  • 对于树上任意节点,其子树表示升序排列中的一段连续区间。

根据 BST 的定义,在 BST 上查找元素时,只需从根搜索,遇到当节点大于时向左儿子搜索,小于时向右儿子搜索,直到等于或没有儿子时返回,时间复杂度即为元素在树上的深度,而最坏时间复杂度即为最大节点深度。由于二叉树每层可以容纳的节点数是指数增长的,因此树的平均深度为 logn\log{n}。但又由于 BST 无法保证树深稳定为 logn\log{n} 因此无法保证稳定的时间复杂度,有此引出了平衡树。

平衡树的定义: 对于一棵二叉搜索树的任意节点,如果其左右子树大小差始终不超过 11,则称之平衡树。由于维护一棵平衡树的时间代价十分高,通常称所有左右子树高度差具有一定约束的都为平衡树。

代码实现

结构定义

// 二叉搜索树
struct BST {
    int key;      // 键
    int cnt;      // 计数
    int father;   // 父节点
    int next[2];  // 左右子节点
    int sum;      // 子树求和, 用于求元素排名
} tr[MAX];
int cnt_tr = 1;
int root = 0;

#define fa(idx) tr[idx].father
#define ls(idx) tr[idx].next[0]
#define rs(idx) tr[idx].next[1]
#define who(idx) ((idx) == rs(fa(idx)))  // 是否为父的右儿子

// 维护子树求和 (单步)
inline void up(int idx) { tr[idx].sum = tr[ls(idx)].sum + tr[rs(idx)].sum + tr[idx].cnt; }

// 维护子树求和
inline void maintain(int idx) {
    while (idx) {
        up(idx);
        idx = fa(idx);
    }
}

查找元素

从根搜索,遇到当节点大于时向左儿子搜索,小于时向右儿子搜索,直到等于或没有儿子时返回。

插入元素

搜索元素在树上的位置,如果元素存在则直接返回,否则当无法继续向下搜索时插入元素到搜索的方向。

其中 maintain 函数用于维护树上的一些值,此处用于维护子树求和,用于求元素排名。

删除元素

删除元素是最为复杂的操作。如果元素存在,要将其分为 33 类:

  • 元素为叶子节点。直接删除即可。
  • 元素为链节点,即没有左儿子或右儿子。删除节点后将其唯一儿子连接到父节点。
  • 元素左右儿子均存在。则其前驱或后继一定在其子树中,与其前驱或后继交换值后,删除其前驱或后继。前驱或后继一定属于上述两种情况。

前驱后继

以查找后继为例,需要分为 22 类:

  • 如果节点存在右儿子,则后继一定在右子树上,为右子树最小节点。
  • 否则,后继一定是其祖先节点,不断向父节点搜索,找到第一个大于的点即为后继。
  • 最后一个节点可能没有后继,依然按照上一种情况处理。

前驱同理。

快速建树

在已知初始状态包含哪些元素时,可以 O(n)O(n) 快速建树(除去排序)。每次将区间中位数作为根节点即可建一棵相对平衡的树。

元素排名

求元素排名需要维护子树求和 tr[idx].sum,通过与查找元素相同的方式查找,只需在当前指针每次向右儿子转移时,累加左子树求和与当前节点大小即可,最终得到的数即为排名。

第 k 元素

与元素排名相似,需要维护子树求和 tr[idx].sum。查找时如果 rk 大于左子树与当前节点求和,则向右子树查找,rk 减去已经抵消的排名即可;如果 rk 仅大于左子树求和,则返回当前节点;否则,向左子树查找。