树形结构 伸展树

除了拥有二叉查找树的性质之外,伸展树还具有的一个特点是:当某个节点被访问时,伸展树会通过旋转使该节点成为树根。这样做的好处是,下次要访问该节点时,能够迅速的访问到该节点。假设想要对一个二叉查找树执行一系列的查找操作。为了使整个查找时间更小,被查频率高的那些条目就应当经常处于靠近树根的位置。于是想到设计一个简单方法,在每次查找之后对树进行重构,把被查找的条目搬移到离树根近一些的地方。伸展树应运而生,它是一种自调整形式的二叉查找树,它会沿着从某个节点到树根之间的路径,通过一系列的旋转把这个节点搬移到树根去。

Splaying是Splay Tree中的基本操作,为了让被查询的条目更接近树根,Splay Tree使用了树的旋转操作,同时保证二叉排序树的性质不变。

Splaying的操作受以下三种因素影响:

  • 节点x是父节点p的左孩子还是右孩子

  • 节点p是不是根节点,如果不是

  • 节点p是父节点g的左孩子还是右孩子

同时有三种基本操作:

Zig Step

当p为根节点时,进行zip step操作。

当x是p的左孩子时,对x右旋;

当x是p的右孩子时,对x左旋。

Zig-Zig Step

当p不是根节点,且x和p同为左孩子或右孩子时进行Zig-Zig操作。

当x和p同为左孩子时,依次将p和x右旋;

当x和p同为右孩子时,依次将p和x左旋。

Zig-Zag Step

当p不是根节点,且x和p不同为左孩子或右孩子时,进行Zig-Zag操作。

当p为左孩子,x为右孩子时,将x左旋后再右旋。

当p为右孩子,x为左孩子时,将x右旋后再左旋。

#include<stdio.h>

typedef struct SplayTreeNode {
	int value;
	node *father;
	node *son[2];//0-left 1-right

	SplayTreeNode(int v = 0, node * f = NULL)
	{
		value = v;
		father = f;
		son[0] = NULL;
		son[1] = NULL;
	}
}node;

SplayTreeNode *root;
bool which_son(SplayTreeNode *f, SplayTreeNode *s)//判断子节点在父节点左还是右
{
	return f->son[1] == s;
}

void rotate(SplayTreeNode *tree)//函数会自行判断x实在父节点的左儿子上还是右儿子上,并自动左旋或右旋
{
	SplayTreeNode *father = tree->father;
	SplayTreeNode *grandf = father->father;
	bool a = which_son(father, tree);
	bool b = !a;
	father->son[a] = tree->son[b];
	if (tree->son[b] != NULL)
		tree->son[b]->father = father;
	tree->son[b] = father;
	father->father = tree;
	tree->father = grandf;
	if (grandf != NULL)
		grandf->son[which_son(grandf, father)] = tree;
	else
		root = tree;

}

//Splay(x, y)用于将x结点旋转到y结点的某个儿子上
void Splay(SplayTreeNode *t, SplayTreeNode *p)
{
	while (t->father != p)
	{
		SplayTreeNode *father = t->father;
		SplayTreeNode *grandf = father->father;
		if (grandf == p)
			rotate(t);
		else
		{
			if (which_son(grandf, father) ^ which_son(father, t))
				rotate(t), rotate(t);//^异或相同0,不同1
			else
				rotate(father), rotate(t);
		}
	}
}
//这里值得注意的是两种双旋。如果t(该节点),f(父亲节点),g(祖父节点)形成了一条单向的链,即[右→右]或[左→左]这样子,
//那么就先对父亲结点进行rotate操作,再对该节点进行rotate操作;否则就对该节点连续进行两次rotate操作。据称单旋无神犇,双旋o(logn),这句话我也没有考证,
//个人表示不想做什么太多的探究,毕竟splay的复杂度本来就挺玄学的了,而且专门卡单旋splay的题也没怎么听说过。
//对了,这个双旋操作和avl的双旋是不是有那么几分相似啊,虽然还是不太一样的吧,好吧其实也不怎么像╮(╯ - ╰)╭。

void insert(int val) {
	if (root == NULL)
		root = new node(val, NULL);
	for (node *t = root; t; t = t->son[val >= t->value]) {
		if (t->value == val) { Splay(t, NULL); return; }
		if (t->son[val >= t->value] == NULL)
			t->son[val >= t->value] = new node(val, t);
	}
}

void erase(int val) {
	node *t = root;
	for (; t; ) {
		if (t->value == val)
			break;
		t = t->son[val > t->value];
	}
	if (t != NULL) {
		Splay(t, NULL);
		if (t->son[0] == NULL) {
			root = t->son[1];
			if (root != NULL)
				root->father = NULL;
		}
		else {
			node *p = t->son[0];
			while (p->son[1] != NULL)
				p = p->son[1];
			Splay(p, t); root = p;
			root->father = NULL;
			p->son[1] = t->son[1];
			if (p->son[1] != NULL)
				p->son[1]->father = p;
		}
	}
}
//与之对应的就是删除操作,相对的复杂一些。删除一个元素,需要先在树中找到这个结点,然后把这个结点Splay到根节点位置,开始分类讨论。
//如果这个结点没有左儿子(左子树),直接把右儿子放在根的位置上即可;
//否则的话就需要想方设法合并左右子树:在左子树种找到最靠右(最大)的结点,把它旋转到根节点的儿子上,此时它一定没有右儿子,
//因为根节点的左子树中不存在任何一个元素比它更大,那么把根节点的右子树接在这个结点的右儿子上即可。

猜你喜欢

转载自blog.csdn.net/qq_40061421/article/details/81350069