简介
二叉搜索树(BST)用于快速增删改查数据,对外部可以看作一个集合。BST 的平均时间复杂度是 ,但是遗憾的是其最坏时间复杂度是 的。为了解决 BST 的这个问题,之后会介绍多种平衡树。平衡树通过不同的维护方式,保证了其始终有 的时间复杂度,BST 是所有平衡树的基础,同时其多数操作在多数平衡树中是通用的。
模板题链接
只有最后一组数据会 TLE。
核心思想
定义: 对于一棵有根树的任意节点,如果满足左子树所有节点的权值均小于该节点的权值,且右子树均大于该值,则称之为 BST。即对于任意节点 有权 ,其中 表示节点 的权值, 表示 左子树权值的集合。一般不允许 BST 包含相同权值的元素。
性质:
-
。
-
BST的中序遍历即为树上所有元素的升序排列。 -
对于树上任意节点,其子树表示升序排列中的一段连续区间。
根据 BST 的定义,在 BST 上查找元素时,只需从根搜索,遇到当节点大于时向左儿子搜索,小于时向右儿子搜索,直到等于或没有儿子时返回,时间复杂度即为元素在树上的深度,而最坏时间复杂度即为最大节点深度。由于二叉树每层可以容纳的节点数是指数增长的,因此树的平均深度为 。但又由于 BST 无法保证树深稳定为 因此无法保证稳定的时间复杂度,有此引出了平衡树。
平衡树的定义: 对于一棵二叉搜索树的任意节点,如果其左右子树大小差始终不超过 ,则称之平衡树。由于维护一棵平衡树的时间代价十分高,通常称所有左右子树高度差具有一定约束的都为平衡树。
代码实现
结构定义
// 二叉搜索树
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 函数用于维护树上的一些值,此处用于维护子树求和,用于求元素排名。
删除元素
删除元素是最为复杂的操作。如果元素存在,要将其分为 类:
- 元素为叶子节点。直接删除即可。
- 元素为链节点,即没有左儿子或右儿子。删除节点后将其唯一儿子连接到父节点。
- 元素左右儿子均存在。则其前驱或后继一定在其子树中,与其前驱或后继交换值后,删除其前驱或后继。前驱或后继一定属于上述两种情况。
前驱后继
以查找后继为例,需要分为 类:
- 如果节点存在右儿子,则后继一定在右子树上,为右子树最小节点。
- 否则,后继一定是其祖先节点,不断向父节点搜索,找到第一个大于的点即为后继。
- 最后一个节点可能没有后继,依然按照上一种情况处理。
前驱同理。
快速建树
在已知初始状态包含哪些元素时,可以 快速建树(除去排序)。每次将区间中位数作为根节点即可建一棵相对平衡的树。
元素排名
求元素排名需要维护子树求和 tr[idx].sum,通过与查找元素相同的方式查找,只需在当前指针每次向右儿子转移时,累加左子树求和与当前节点大小即可,最终得到的数即为排名。
第 k 元素
与元素排名相似,需要维护子树求和 tr[idx].sum。查找时如果 rk 大于左子树与当前节点求和,则向右子树查找,rk 减去已经抵消的排名即可;如果 rk 仅大于左子树求和,则返回当前节点;否则,向左子树查找。