前言
暴力即是优雅 —— 香sister
替罪羊树
如前言所说,替罪羊树是一种暴力优雅的数据结构。
作为一棵平衡树,替罪羊树和其他树都有着共同的使命:维护二叉查找树的平衡使得树高,即树上操作时间复杂度为 O ( l o g ( n ) ) O(log(n)) O(log(n))
but,其他的平衡树(例如有旋Treap、AVL、Splay…),几乎都是用旋转操作维护树平衡的。而替罪羊树则是一种不需要旋转,而是采用一种更为暴力手段解决问题的树。
因为它的暴力,所以有一些其他平衡树可以做的事它不行,但它的优秀之处在于:好码好理解。加上之后要学习的KD树也大多是依靠替罪羊树,因此学一发替罪羊树势在必行。
平衡机制
讲操作之前,我们先来了解一下替罪羊树的平衡机制。
首先我们设定一个平衡因子 α ( 0.5 < = α < 1 ) \alpha(0.5<=\alpha<1) α(0.5<=α<1),如果子树大小小于等于根子树大小乘上 α \alpha α,则判定该子树为平衡,反之则将其拍扁重建。通常,这个 α \alpha α取值为 0.7 0.7 0.7
重建操作看起来很diao,其实很暴力。就是将超重子树的序列提取出来,因为是二叉平衡树,这个序列一定是有序的,所以直接提出序列的中点为根节点,然后左边的建成左子树,右边的建成右子树即可。很显然,这个拍平操作是 O ( n ) O(n) O(n)的——这样的时间复杂度,是如何保证替罪羊树的平均时间复杂度为 O ( l o g ( n ) ) O(log(n)) O(log(n))的呢?
然而我并不会证明,所以请转移知乎
插入
我们还是先讲插入吧。替罪羊的插入操作和普通的二叉搜索树没什么区别。但是,在插入后,需要从插入位置往根走,判断当前子树是否超重。找到最浅的一层超重子树,然后对这层根的子树进行拍扁重构。 因为一个节点的加入,导致整棵子树被拍扁重构,或许就是"替罪羊"的真谛吧。
删除
看到上面的插入操作似乎要抢走“替罪羊”的来源,不得不讲讲真正的大佬——删除操作。这才是“替罪羊树”真正的来由,也是替罪羊树比较巧妙的一个部分。
替罪羊树的删除节点不是直接删除,而是给待删除的节点赋上一个懒惰标记(和线段树的懒标记有些相似),即给这个节点赋上一个“待删除”的记号,它与其他节点没什么区别,只是在操作的时候直接忽略罢了。如果有删除标记的节点超过子树节点的一半——直接重构!!!(太暴力了)。
时间复杂度同意可以证明为 O ( l o g ( n ) ) O(log(n)) O(log(n))
然而上述是一种比较公认的删除方法,我下面打的删除方法则是:用被删除节点的左子树的最后一个节点或者右子树的第一个节点来顶替被删除节点的位置。这个我也不知道靠不靠谱(看的第一篇博客就是这样写的)
其他操作
其他操作就不做过多赘述了,一是其与Treap和Splay的相关操作都很类似,二是看下代码都能理解(主要是懒)。
那么,话不多说,直接上一道例题吧普通平衡树
Code:
#include <map>
#include <set>
#include <cmath>
#include <stack>
#include <queue>
#include <cstdio>
#include <vector>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#define ST sort
#define MAXN 2000005
#define DB double
#define LL long long
#define INF 0x3f3f3f3f3f3f3f3f
#define Int register int
#define XM XiangMeiHaoGuai
#define Pa pair<LL,LL>
#define Lson Son[x][0]
#define Rson Son[x][1]
#define Mod 1000000007
#define INF 0x3f3f3f3f3f3f3f3f
using namespace std;
const DB Alpha