平衡树学习笔记

例题

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

您需要动态地维护一个可重集合 \(M\),并且提供以下操作:

  1. \(M\) 中插入一个数 \(x\)
  2. \(M\) 中删除一个数 \(x\)(若有多个相同的数,应只删除一个)。
  3. 查询 \(M\) 中有多少个数比 \(x\) 小,并且将得到的答案加一。
  4. 查询如果将 \(M\) 从小到大排列后,排名位于第 \(x\) 位的数。
  5. 查询 \(M\)\(x\) 的前驱(前驱定义为小于 \(x\),且最大的数)。
  6. 查询 \(M\)\(x\) 的后继(后继定义为大于 \(x\),且最小的数)。

对于操作 3,5,6,不保证当前可重集中存在数 \(x\)

**【数据范围】 **
对于 \(100\%\) 的数据,\(1\le n \le 10^5\)\(|x| \le 10^7\)

平衡二叉树

平衡二叉树也叫AVL树

前置芝士:

  • 二叉搜索树
  • 线段树

二叉搜索树在一般情况下可以以 \(O(logn)\) 的效率查找、插入和删除数据。

但是当数据本来就有序(出题人故意想卡你)的时候,这颗二叉搜索树就会退化成一条类似于链表的东西,此时查找、插入和删除的效率都会退化成 \(O(n)\),甚至不如数组。

平衡二叉树的性质

它首先是一颗二叉搜索树,但是对于每一个节点,它的左子树和右子树的高度之差(平衡因子)的绝对值不超过 \(1\),且他的左子树和右子树都是一颗平衡二叉树,这样能尽可能的让树保持均衡。

注意:平衡因子是高度之差的绝而不是子节点个数的差

可以知道平衡因子只能取 \(-1, 0, 1\) 三个值,否则就会发生失衡

平衡二叉树的插入和删除操作

对于一颗平衡二叉树,有时插入新值或删除值后它就不再是一颗平衡二叉树了(失衡),这该怎么办?

我们把失衡的节点叫做 麻烦节点,因为插入/删除节点而导致这颗子树不再是平衡二叉树的根节点我们把它叫做 失衡节点,如果有多个被破坏节点,只讨论离麻烦节点最近的失衡节点。

其实平衡二叉树和二叉搜索树的插入删除操作非常相似,只是二叉平衡树每次插入或者删除以后都需要检查一下是否存在失衡,如果失衡,平衡二叉树就会做出一些神奇的旋转操作使得这棵树再次平衡。

旋转操作

旋转操作的基础分两种,分别是左旋和右旋。

图1

如图,我们可以发现节点 \(4\) 的平衡因子是 \(-2\) (图片错误),失衡了。

此时我们可以将这个失衡节点左旋,也就是逆时针旋转,变成这样一颗树:

图1

这样这棵树就变得平衡了,并且和调节之前的树等价,因为它们的中序遍历都是一样的。

也就是说在二叉搜索树的意义上,它们两个是等价的,但却变得更加均衡了。

查询操作(第k大/前驱/后继)

待更新。

恭喜你学会了AVL树。

FHQ TREAP

个人认为最简单的平衡树

FHQ TREAP和AVL树唯一的区别就是FHQ TREAP维护平衡的方式不同。

以下用FHQ简称FHQ TREAP

FHQ主要有两种操作,一个是split一个是merge。

这两个都很好理解,分裂就是在 \([l , r]\) 的区间内从 \(x\) 切开

不难发现分裂之后这两棵子树仍然满足二叉搜索树的性质

类似于这样:

无标题

合并就是把两个子树拼起来

代码实现:

void split(int u, int x, int &L, int &R)
{
    if(u == 0)
    {
        L = R = 0;
        return ;
    }
    if(tr[u].key <= x)
    {
        L = u;
        split(tr[u].rs, x, tr[u].rs, R);
    }
    else
    {
        R = u;
        split(tr[u].ls, x, L, tr[u].ls);
    }
    update(u);
}
int merge(int L, int R)
{
    if(L == 0 || R == 0)
    {
        return L + R;
    }
    if(tr[L].rnd > tr[R].rnd)
    {
        tr[L].rs = merge(tr[L].rs, R);
        update(L);
        return L;
    }
    else{
        tr[R].ls = merge(L, tr[R].ls);
        update(R);
        return R;
    }
}

插入操作

假如插入的是 \(x\),那么将整棵树从 \(x\) 切开,将 \(x\) 塞进去后合并即可

代码实现:

void Insert(int x)
{
    int L, R;
    split(root, x, L, R);
    newNode(x);
    int qwq = merge(L, cnt);
    root = merge(qwq, R);
}

删除操作

如果删除一个节点后直接将他的左右儿子合起来,那么这棵树可能变成一种神秘结构。

我们删除节点k,先在整棵树上把以 \(k\) 为根的子树分裂出来,再在 \(k\) 的子树上把 \(k\) 分裂出来。

接着把 \(k\) 丢掉,这时原先的一颗树已经被我们拆成三颗树了。

我们先把原来的 \(k\) 的左右儿子进行一次合并操作,继续对合并好的树和原来的树再合并一次,又可以得到完整的一棵树了。

代码实现:

void del(int x)
{
    int L, R, p;
    split(root, x, L, R);
    split(L, x - 1, L, p);
    p = merge(tr[p].ls, tr[p].rs);
    root = merge(merge(L, p), R);
}
posted @ 2025-11-23 15:37  Vodyanitsa  阅读(39)  评论(0)    收藏  举报