【平衡树总结 Ⅱ】【SBT】Size Balanced Tree | E

今天来种一种个人最喜欢的SBTree

平衡树总结Ⅱ:SBT



概述

SBTSize Balanced Tree,顾名思义是尽量维护子树大小(结点个数)平衡的树。

也就是当每次插入新结点后,如果发现左右子树失衡(某个结点的size比它兄弟的孩子的size都要小)就得进行旋转操作,来使子树的size始终保持在一个较为平衡的状态。



自平衡操作

① SBT的自平衡维护比较简单,只在插入时进行自平衡维护,且只有两种最基本的旋转操作:简单左旋简单右旋

  • 左旋操作:(点击可以看大图哦)在这里插入图片描述
  • 右旋操作:(点击可以看大图哦)在这里插入图片描述

一个值得注意的地方是,在设计旋转函数的时候,参数 i 应该是引用类型(可以发现在第四步修改了这个引用)。这样我们在第四步i 进行的赋值才会真的修改 i 的值(即父节点指针的值)


② 那么啥时候需要旋转呢?什么时候叫做 “左右子树size失衡” 了呢?

我们定义某个结点是平衡的当且仅当他的size不小于其兄弟结点的任一子节点的size:(如图。为了表示方便,每个结点或子树都用一个大写字母起了名字,并将其真实下标标注在正下方)
在这里插入图片描述
SBT在每次插入结点后会检查当前结点一直到根结点这一条链上的各个结点是否满足平衡条件,如果不满足就进行旋转。

③ 具体调整逻辑:

  • 1.YD失衡(D太重,要左旋X (即i号) )在这里插入图片描述

  • 2.YC失衡(C太重,要先右旋Z (即t[i].ri号) ,再左旋X (即i号) )在这里插入图片描述在这里插入图片描述

  • 3.ZA失衡(对称于YD失衡(图就不画啦):A太重,要右旋X (即i号) )

  • 4.ZB失衡(对称于YC失衡(图就不画啦):B太重,要先左旋Y (即t[i].li号),再右旋X (即i号) )

是不是很简单呢 当然,最后为了写代码方便,我们还可以把调整平衡的函数加一个 bool 型参数,告知函数我刚刚是改了左子树还是右子树,如果改了右子树就只用判断是否发生了YD失衡YC失衡,如果改了左子树就只用判断是否发生了ZA失衡ZB失衡。并且可以在函数的最后暴力地检查当前结点、当前结点左孩子、当前结点右孩子是否需要再递归调整(稍微降低一点点运行效率,但大幅减少出 bug 概率)


④ 关键函数代码:(基本上把上面的分析翻译一下就行)

struct SBN	// SBT结点
{
	xint li, ri, sz;	// 左孩子指针(下标)、右孩子指针(下标)、该子树的size
	vint v;				// 点权
} t[MN];

inline void l_rotate(xint &i)	// 左旋
{
	const xint ri = t[i].ri;								// 记住ri
	t[i].ri = t[ri].li, t[ri].li = i, t[ri].sz = t[i].sz;	// 修改t[i]的右指针、t[ri]的左指针,同时修改t[ri]的size
	t[i].sz = t[t[i].li].sz + t[t[i].ri].sz + 1;			// 修改t[i]的size
	i = ri;													// 修改i的父节点的指针
	/*	改引用i是因为旋转后i的父节点就不应该指向i了,而应该指向i的右孩子
		所以要把i父节点指向i的指针改成指向ri的指针,所以需要传引用改i(调用时传的正是i父节点指向i的指针的引用)*/
}

inline void r_rotate(xint &i)	// 右旋
{
	const xint li = t[i].li;								// 记住li
	t[i].li = t[li].ri, t[li].ri = i, t[li].sz = t[i].sz;	// 修改t[i]的左指针、t[li]的右指针,同时修改t[li]的size
	t[i].sz = t[t[i].li].sz + t[t[i].ri].sz + 1;			// 修改t[i]的size
	i = li;													// 修改i的父节点的指针
}

void maintain(xint &i, const bool rr)	// 维护平衡
{
	if (rr)	// 如果刚才改的是右子树
	{
		if (t[t[t[i].ri].ri].sz > t[t[i].li].sz)		// YD失衡,D太重
			l_rotate(i);								// 要左旋X(即i号)
		else if (t[t[t[i].ri].li].sz > t[t[i].li].sz)	// YC失衡,C太重
			r_rotate(t[i].ri), l_rotate(i);				// 要先右旋Z(即t[i].ri号);然后再左旋X(即i号)
		else											// 没有失衡
			return;
	}
	else	// 刚才改的是左子树
	{
		if (t[t[t[i].li].li].sz > t[t[i].ri].sz)		// ZA失衡,A太重
			r_rotate(i);								// 要右旋X(即i号)
		else if (t[t[t[i].li].ri].sz > t[t[i].ri].sz)	// ZB失衡,B太重
			l_rotate(t[i].li), r_rotate(i);				// 要先左旋Y(即t[i].li号);然后再右旋X(即i号)
		else											// 没有失衡
			return;
	}

	// 懒得具体分析了,直接暴力递归所有可能需要维护平衡的情况(最多就造成几次额外的if判断,不用担心会产生无用的旋转操作)
	maintain(t[i].li, false), maintain(t[i].ri, true), maintain(i, false), maintain(i, true);
}



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


分析:没什么好说的…把SBT最基本的功能实现即可

代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define LL long long
#define _BS 1048576
char _bf[_BS];
char *__p1=_bf,*__p2=_bf;
#define _IO char *_p1=__p1,*_p2=__p2;
#define _OI __p1=_p1,__p2=_p2;

#ifdef _KEVIN
#define GC getchar()
#else
#define GC (_p1==_p2&&(_p2=(_p1=_bf)+fread(_bf,1,_BS,stdin),_p1==_p2)?EOF:*_p1++)
#endif
#define PC putchar
#define sc(x) {register char _c=GC,_v=1;for(x=0;_c<48||_c>57;_c=GC)if(_c==45)_v=-1;for(;_c>=48&&_c<=57;x=(x<<1)+(x<<3)+_c-48,_c=GC);if(_v==-1)x=-x;}

template<class T>
void PRT(const T _){if(_<0){PC(45),PRT(-_);return;}if(_>=10)PRT(_/10);PC(_%10+48);}

constexpr int MN(1e5+7);

template <typename vint, typename xint = int>
class SBT
{
private:

	xint root, tot;
	struct SBN
	{
		xint li, ri, sz;
		vint v;
	} t[MN];

	inline void l_r(xint &i)	// 左旋
	{
		const xint ri = t[i].ri;
		t[i].ri = t[ri].li, t[ri].li = i, t[ri].sz = t[i].sz;
		t[i].sz = t[t[i].li].sz + t[t[i].ri].sz + 1;
		i = ri;
	}

	inline void r_r(xint &i)	// 右旋
	{
		const xint li = t[i].li;
		t[i].li = t[li].ri, t[li].ri = i, t[li].sz = t[i].sz;
		t[i].sz = t[t[i].li].sz + t[t[i].ri].sz + 1;
		i = li;
	}

	void mt(xint &i, const bool rr)	// 维护平衡
	{
		if (rr)	// 如果刚才改的是右子树
		{
			if (t[t[t[i].ri].ri].sz > t[t[i].li].sz)		// YD失衡
				l_r(i);
			else if (t[t[t[i].ri].li].sz > t[t[i].li].sz)	// YC失衡
				r_r(t[i].ri), l_r(i);
			else											// 没有失衡
				return;
		}
		else	// 刚才改的是左子树
		{
			if (t[t[t[i].li].li].sz > t[t[i].ri].sz)		// ZA失衡
				r_r(i);
			else if (t[t[t[i].li].ri].sz > t[t[i].ri].sz)	// ZB失衡
				l_r(t[i].li), r_r(i);
			else											// 没有失衡
				return;
		}

		mt(t[i].li, 0), mt(t[i].ri, 1), mt(i, 0), mt(i, 1);
	}

	void _insert(xint &i, const vint v)
	{
		if (i)
		{
			++t[i].sz;
			if (v < t[i].v)
				_insert(t[i].li, v), mt(i, 0);
			else
				_insert(t[i].ri, v), mt(i, 1);
		}
		else
		{
			i = ++tot;
			t[i].v = v;
			t[i].sz = 1;
		}
	}

	vint _erase(xint &i, const vint v)
	{
		--t[i].sz;
		if (v==t[i].v || (!t[i].li && v<t[i].v) || (!t[i].ri && v>t[i].v))
		{
			vint tmp = t[i].v;
			if (!t[i].li || !t[i].ri)
				i = t[i].li + t[i].ri;
			else
				t[i].v = _erase(t[i].li, t[i].v+1);
			return tmp;
		}
		if (v<t[i].v)
			return _erase(t[i].li, v);
		else
			return _erase(t[i].ri, v);
	}

	xint _lower_rk(xint i, const vint v) const
	{
		xint rk = 1;
		while (i)
		{
			if (t[i].v >= v)
				i = t[i].li;
			else
				rk += t[t[i].li].sz + 1, i = t[i].ri;	// 先+=再改i
		}

		return rk;
	}

	xint _upper_rk(xint i, const vint v) const
	{
		xint rk = 0;
    	while (i)
    	{
    		if (t[i].v > v)
    			i = t[i].li;
    		else
    			rk += t[t[i].li].sz + 1, i = t[i].ri;	// 先+=再改i
		}

		return rk;
	}

	vint _nth(xint i, xint k) const
	{
		xint lz;
		while (i)
		{
			lz = t[t[i].li].sz;
	        if (k == lz+1)
	        	break;
	        if (k <= lz)
	            i = t[i].li;
	        else
	            i = t[i].ri, k -= lz + 1;
	    }

	    return t[i].v;
	}

	vint _prev(const xint i, const vint v) const
	{
		if (!i)
			return v;
		vint tp;
		if (v <= t[i].v)
			tp = _prev(t[i].li, v);
		else
		{
			tp = _prev(t[i].ri, v);
			if (tp == v)
				tp = t[i].v;
		}
		return tp;
	}

	vint _next(const xint i, const vint v) const
	{
		if (!i)
			return v;
		vint tp;
		if (v >= t[i].v)
			tp = _next(t[i].ri, v);
		else
		{
			tp = _next(t[i].li, v);
			if (tp == v)
				tp = t[i].v;
		}
		return tp;
	}

// interfaces
public:

	inline void init(void)
	{
		root = tot = 0;
	}

	inline void insert(const vint v)
	{
		_insert(this->root, v);
	}

	inline vint erase(const vint v)
	{
		return _erase(this->root, v);
	}

	inline xint lower_rk(const vint v) const
	{
		return _lower_rk(this->root, v);
	}

	inline xint upper_rk(const vint v) const
	{
		return _upper_rk(this->root, v);
	}

	inline vint nth(const xint k) const
	{
		return _nth(this->root, k);
	}

	inline vint prev(const vint v) const
	{
		return _prev(this->root, v);
	}

	inline vint next(const vint v) const
	{
		return _next(this->root, v);
	}
};

SBT<int> sbt;

int main()
{
	_IO

	sbt.init();
	int T;
	sc(T)
	while (T--)
	{
		int op, v;
		sc(op)sc(v)
		switch (op)
		{
			case 1:
				sbt.insert(v);
				break;
			case 2:
				sbt.erase(v);
				break;
			case 3:
				PRT(sbt.lower_rk(v)), PC(10);
				break;
			case 4:
				PRT(sbt.nth(v)), PC(10);
				break;
			case 5:
				PRT(sbt.prev(v)), PC(10);
				break;
			case 6:
				PRT(sbt.next(v)), PC(10);
				break;
		}
	}

	return 0;
}



论文

最后贴出陈启峰巨巨的论文原文:https://wenku.baidu.com/view/a96cef5abe23482fb4da4cb1.html


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
首先,SizeBalancedTree是一种基于平衡树的数据结构,它的特点是除了具备平衡树的特性之外,还能够在节点中记录子树大小。这样可以方便地查询某个节点的排名或者寻找某个排名的节点。 以下是Java实现SizeBalancedTree的完整源码: ```java import java.util.Random; public class SizeBalancedTree { private static final int MAXN = 100005; private static final Random random = new Random(); static class Node { int key, size, cnt; Node ch[] = new Node[2]; Node(int key) { this.key = key; size = cnt = 1; } } private Node nullNode = new Node(0); private Node root = nullNode; private Node newNode(int key) { Node x = new Node(key); x.ch[0] = x.ch[1] = nullNode; return x; } private void pushup(Node x) { x.size = x.ch[0].size + x.ch[1].size + x.cnt; } private Node rotate(Node x, int k) { Node y = x.ch[k^1]; x.ch[k^1] = y.ch[k]; y.ch[k] = x; pushup(x); pushup(y); return y; } private Node maintain(Node x, boolean flag) { if (x == nullNode) return x; if (flag && x.ch[0].size > x.ch[1].size * 3) { x = rotate(x, 1); } else if (!flag && x.ch[1].size > x.ch[0].size * 3) { x = rotate(x, 0); } return x; } private Node insert(Node x, int key) { if (x == nullNode) return newNode(key); if (key == x.key) { x.cnt++; pushup(x); return x; } boolean k = key > x.key; x.ch[k] = insert(x.ch[k], key); pushup(x); return maintain(x, k); } private Node remove(Node x, int key) { if (x == nullNode) return x; if (key == x.key) { if (x.cnt > 1) { x.cnt--; pushup(x); return x; } if (x.ch[0] == nullNode && x.ch[1] == nullNode) { return nullNode; } else if (x.ch[0] == nullNode) { x = x.ch[1]; } else if (x.ch[1] == nullNode) { x = x.ch[0]; } else { boolean k = x.ch[0].size > x.ch[1].size; x = rotate(x, k ? 0 : 1); x.ch[k^1] = remove(x.ch[k^1], key); pushup(x); x = maintain(x, k^1); } } else { boolean k = key > x.key; x.ch[k] = remove(x.ch[k], key); pushup(x); x = maintain(x, k); } return x; } private Node getKth(Node x, int k) { if (x == nullNode) return nullNode; int rank = x.ch[0].size + 1; if (k < rank) { return getKth(x.ch[0], k); } else if (k > rank + x.ch[1].size) { return getKth(x.ch[1], k - rank - x.cnt); } else { return x; } } public void insert(int key) { root = insert(root, key); } public void remove(int key) { root = remove(root, key); } public int getRank(int key) { int rank = 0; Node x = root; while (x != nullNode) { if (key == x.key) { return rank + x.ch[0].size + 1; } else if (key < x.key) { x = x.ch[0]; } else { rank += x.ch[0].size + x.cnt; x = x.ch[1]; } } return rank; } public int getKth(int k) { Node x = getKth(root, k); return x.key; } public static void main(String[] args) { SizeBalancedTree sbt = new SizeBalancedTree(); sbt.insert(1); sbt.insert(2); sbt.insert(3); sbt.insert(4); sbt.insert(5); System.out.println(sbt.getRank(3)); // output: 3 System.out.println(sbt.getKth(3)); // output: 3 sbt.remove(3); System.out.println(sbt.getRank(3)); // output: 0 System.out.println(sbt.getKth(3)); // output: 4 } } ``` 在这里,我们定义了一个内部类Node来表示SizeBalancedTree的节点,包含了键值key、子树大小size和数量cnt。同时,我们也定义了一个nullNode,用于代表空节点。在这里,我们将SizeBalancedTree视作一个二叉搜索树,因此左子树的值都小于当前节点,右子树的值都大于当前节点。 在插入和删除节点的时候,我们需要维护平衡和子树大小,因此需要编写maintain和pushup方法。maintain方法用于在插入或删除节点后,对树进行平衡调整。如果左子树的大小大于右子树的大小的三倍,就进行右旋转;如果右子树的大小大于左子树的大小的三倍,就进行左旋转。pushup方法用于更新节点的子树大小。 在插入节点的时候,我们首先查找要插入的位置,如果该节点已经存在,则只需要增加它的数量即可。如果要插入的节点不存在,则将其插入到相应的位置,并对树进行平衡调整。 在删除节点的时候,我们同样需要查找要删除的节点,如果该节点数量大于1,则只需要减少其数量即可。如果要删除的节点数量为1,则需要考虑该节点的子树情况。如果节点没有子树,则直接删除即可;如果节点只有一个子树,则将子树接到该节点的父节点上;如果节点有两个子树,则选择左子树的大小大于右子树的大小,则在左子树中找到最大值,将其替换为当前节点,并从左子树中删除该最大值。 在查询排名和寻找第k小的节点时,我们需要使用getRank和getKth方法。getRank方法首先查找要查询的节点,如果该节点存在,则返回左子树的大小加上1,否则继续向下查找。getKth方法则根据当前节点的左子树大小和数量,以及右子树的大小,来判断第k小的节点在哪个子树中,然后递归到相应的子树中查找。 最后,我们在main方法中进行测试,插入一些节点并查询排名和第k小的节点。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值