数据结构--伸展树(伸展树构建二叉搜索树)-学习笔记

/*-----------------------伸展树----------------------------*/ 
伸展数(Splay Tree),又叫分裂树,是一种二叉排序树,能在O(log n)内完成插入,查找,删除操作;
伸展树上的一般操作都基于伸展操作:
假设要对一个二叉查找树执行一系列查找操作,为使整个查找时间更小,被查效率高的那些条目应当经常处于靠近树根的位置;
于是想设计一个简单方法,在每次查找之后对树进行重构,把被查找的条目搬移到离树根近一些的地方。
伸展树是一种自调形式的二叉查找树,他会沿着从某个节点到树根之间的路径,通过一系列旋转的把这个节点搬移到树根去。
它的优势在于不需要记录用于平衡树的冗余信息。

关键词:
二叉排序树    平衡树
因此我决定先学这两个数据结构:
//--------------------二叉排序树:
又称 二叉查找树||二叉搜索树
定义:
二叉排序树或者一颗空树,或者具有下列性质的二叉树:
1.若左子树不空,则左子树上所有节点的值均小于它的根节点的值;
2.若右子树不空则右子树上所有节点的值均大于它的根节点的值;
3.左右子树分别为二叉排序树;
4.没有 键值 相等的节点 
//键值(key):
//键值是 window 中注册表中的概念。键值位于注册表结构链末端,和文件系统的文件类似; 
//键值包含集中数据类型,以适应不同环境的使用需求。
// 注册表中,是通过键和子键来管理各种信息;
简单的说 二叉排序树就是一棵从左往右越来越大的树。
//---查找:
若根节点的关键字等于查找的关键字,成功
否则,判断查找关键字值,递归进入左子树或右子树
子树为空,查找不成功 
//---插入删除:
二叉排序树是一种动态树表,其特点是:树的结构通常不是一次生成的,而是在查找过程中,当树中不存在关键字等于给定值时在进行插入。
新插入的节点一定是一个新添加的叶子节点而且查找不成功时查找路径上访问的最后一个节点的左||右子节点 
插入算法:
首先执行查找算法,找出被插节点的父节点;
判断被插节点是其父节点的左||右子节点,被插节点作为子节点插入。
若二叉树为空,则首先单独生成根节点。
新插入的节点总是叶子节点。
struct bitree{
	int data;
	bitree *lchild,*rchild;
}; 
//在二叉排序树中插入查找关键字key
bitree* insertbst(bitree *t,int key)
{
	if(t==NULL)
	{
		t=new bitree();
		t->lchild=t->rchild=NULL;
		t->data=key;
		return t;
	}
	if(key<t->data)
		t->lchild=insertbst(t->lchild,key);
	else
		t->rchild=insertbst(t->rchild,key);
	return t;
}
//n个数据在数组d中,tree为二叉排序树 树根 
bitree* create_bitree(bitree *tree,int d[],int n)
{
	for(int i=0;i<n;++i)
		tree=insertbst(tree,d[i]);
}

//---删除节点
在二叉排序树中删除一个节点,分成三种情况:
1.若*p节点为叶子节点,即PL(左子树)和PR(右子树)均为空树,由于删去叶子节点不破坏整棵树的结构,则可以直接删除此子节点。
2.若*p节点只有左子树PL或右子树PR,此时只要令PL||PR直接成为其双亲节点*f的左子树(当*p是左子树)或右子树(当*p是右子树) ,此时也不破坏二叉排序树的特性 
3. 若*p节点的左子树和右子树均不为空 。在删去*p后,为保持其他元素的相对位置不变,课按中序遍历保持有序进行调整。
有两种做法:
一:令*p左子树为*f的左||右子树(依照*p是*f左子树还是右子树而定),*s为*p左子树的最右下的节点,而*p的右子树为*s的右子树;
二:令*p的直接前驱(或直接后继)替代*p,然后再从二叉排序树中删去他的直接前驱(直接后继)
--即让*f的左子树(如果有的话)成为*p左子树的最坐下节点(如果有的话),再让*f成为*p的左右节点的父节点。
直白的说就是:因为二叉查找树的原因,被删节点*p左子树的最右边的节点的值必定小于*p右子树根节点的值 ,
因此可以直接把*p的右子树拼接到*p左子树的最右边的子节点上 
算法如下:
 
bool Delete(bitree*);
bool Deletebst(bitree &tparent,bitree &T,keytype key)
{//若二叉排序树T中存在关键字等于key的数据元素时,则删除该数据元素,并返回true否则返回false 
	if(!T)
		return false;
	else
	{
		if(key==T->data.key)//找到关键字等于key的数据元素 
			return Delete(parent_t,t);
		else if(key<T->lchild.key)
			return Deletebst(T,T->lchild,key);
		else
			return Deletebst(T,T->rchild,key);
	}
	return true;
}

bool Delete(bitree &fp,bitree &p)
{//从二叉排序树中删除节点p,并重接它的左||右子树 
	if(!p->rchild)//只需要连接一棵树即可 
	{
		fp->lchild=p->lchild;
		delete(p);
	}
	else if(!p->lchild)
	{
		fp->rchild=p->rchild;
		delete(p);
	}
	else//连接两棵树 
	{
		q=p;
		fp->lchild=p->lchild;
		s=p->lchild;//转左 
		while(s->rchild)//向右到尽头 
		{
			q=s;
			s=s->rchild;
		}//此时q是s的父节点 
		s->rchild=p->rchild;//将s的左子树作为q的右子树 
		delete(p);
	}
	return true;
}


//----------------平衡树
平衡二叉树(Balanced Binary Tree)具有以下性质:
它是一颗空树||它的左右两个子树高度差的绝对值不超过1,并且左右两个子树都是一颗平衡二叉树。
平衡树的实现方法有:
红黑树,AVL,替罪羊树,Treap,伸展树等
最小平衡二叉树节点的公式:F(n)=F(n-1)/*左子树树节点数量*/+F(n-2)/*右子树节点数量*/+1; 
平衡树的维持方法:
二叉左旋:
//待学习。。。 
二叉右旋:
//待学习。。。 
 
//---------------------------了解完基础知识,开始学习伸展树-------- 
如何构造一个伸展树:
//---方法一:
访问到X节点时,从X处单旋转将X移动到根节点处,
也就是将访问路径上的每个节点和他们的父节点都实施旋转 。
这种旋转的效果是将访问节点X一直推向树根,
但是不好的地方是可能将其他节点推向更深的位置,
这种效果并不好,因为它没有改变访问路径上其他节点的后序访问状况。

//---方法二:
和方法一类似, 
在访问到节点X时,根据X节点和其父节点(P)以及祖父节点(G)之间的形状做相应的单选转或双旋转。
如果三个节点构成LR或RL时(即之字型),则进行相应的双旋转;
如果构成LL或者RR时进行对应的单选转(一字型)。
这样展开的效果是将X推向根节点的同时,
访问路径上其他节点的深度大致都减少了一半
(某些浅的节点最多向后退后了两层)。
这样做对绝大多数访问路径上的节点的后续访问都是有益的。

//-----伸展树的基本特性:
当访问路径太长而导致超出正常查找时间的时候,这些旋转将对未来的操作(下一次访问)有益;
当访问耗时很少的时候,这些旋转则不那么有益甚至有害。
/*--一个(舍弃)的 解释 
//------伸展树的基本操作
伸展树的伸展方式有两种,一种是自下向上的伸展;另一种是自上向下的伸展。
比较容易理解的是自下向上的伸展,我们会重点解释自顶向下的实现方式。
//---自下向上 
先自上向下搜寻X节点,
当搜索到X节点时,从X节点开始到根节点的路径上所有的节点进行旋转 ,
最终将X节点推向根节点,将访问路径上的大部分节点的深度都降低。
 
具体旋转需根据不同的情形进行,在X节点处有三种情形需要考虑
(假设X的父节点是P,X的祖父节点为G):
1.X的父节点P就是树根的情形,这种情形比较简单,只需要将X和P进行旋转即可,
X和P构成LL就是左单选转,构成RR就右单旋转
2.X和P和G之间构成"之"字型的情形,即LR||RL类型。
如果是LR则进行左双旋转,如果是RL进行右双旋转 。
3.X和P和G之间构成"一"字形的情形,即RR||LL类型;
如果LL则执行两次单左旋转,如果是RR则执行两次单右旋转;

代码先不写了:效率据说不高 

//--自顶向下的伸展

*/

自顶向下的伸展:

换成图片就是这样:

然后是运行的代码:

#include<stdio.h>
#include<stdlib.h>
#include<iostream>
using namespace std;

struct splaytree_node{
	int key;
	splaytree_node *left,*right;
};

splaytree_node *splaytree_search(splaytree_node *x,int key)//递归查找 
{
	if(x==NULL||x->key==key)
		return x;
	if(key<x->key)
		return splaytree_search(x->left,key);
	else
		return splaytree_search(x->right,key);
}

splaytree_node *splaytree_splay(splaytree_node *tree,int key)//旋转 
{
	splaytree_node N,*l,*r,*c;
	if(tree==NULL)
		return tree;
	N.left=N.right=NULL;
	l=r=&N;
	while(1)//开始旋转调整 
	{
		//cout<<tree->key<<endl;
		if(key<tree->key)//向l方向调整 
		{
			if(tree->left==NULL)//左边没东西了 
				break;
			if(key<tree->left->key)//左边仍有值 && key仍小于 
			{
				c=tree->left;
				tree->left=c->right;
				c->right=tree;
				tree=c;
				//现在已经调整过节点 向左旋转一个节点 
				if(tree->left==NULL)
					break;//如果左边没有值了,就结束循环 
			}
			r->left=tree;
			r=tree;
			tree=tree->left;
			
		}
		else if(key>tree->key)//向r方向调整 
		{
			if(tree->right==NULL)
				break;
			if(key>tree->right->key)
			{
				c=tree->right;
				tree->right=c->left;
				c->left=tree;
				tree=c;
				if(tree->right=NULL)
					break;
			}
			l->right=tree;
			l=tree;
			tree=tree->right;
			
		}
		else// 已经是该节点了
		{
			break;
		}
	}
	//当亲位置  tree为目标点||最接近目标点 
//	cout<<tree<<" "<<tree->key<<" "<<tree->left<<" "<<tree->right<<endl;
	l->right=tree->left;
	r->left=tree->right;
	tree->left=N.right;
	tree->right=N.left;
//	cout<<tree<<" "<<tree->key<<" "<<tree->left<<" "<<tree->right<<endl;
	//翻上去 
	return tree;
}

splaytree_node *creat_splaytree_node(int key,splaytree_node *left,splaytree_node*right)
{
	splaytree_node *p;
	if((p=(splaytree_node*)malloc(sizeof(splaytree_node)))==NULL)
		return NULL;
	p->key=key;
	p->left=left;
	p->right=right;
	return p;
}

splaytree_node *insert_node(splaytree_node* tree,splaytree_node *z)
{
	splaytree_node *y=NULL;//要插入的目标位置的上一个位置 
	splaytree_node *x=tree;//要插入的目标位置 
	while(x!=NULL)
	{
		y=x;
		if(z->key<x->key)
			x=x->left;
		else if(z->key>x->key)
			x=x->right;
		else
		{
			cout<<"Error:此节点已存在!"<<endl;
			free(z);
			return tree;
		}
	}
	if(y==NULL)//此时是一颗空树,直接返回z即可 
		tree=z;
	else if(z->key<y->key)//y的左节点 
		y->left=z;
	else
		y->right=z;//y的右节点 
	return tree;
}

splaytree_node *splaytree_insert(splaytree_node *tree,int key)//插入 
{
	//cout<<tree<<" "<<key<<endl;
	splaytree_node *z;
	z=creat_splaytree_node(key,NULL,NULL);
	//cout<<z<<" "<<z->key<<" "<<key<<endl;
	if(z==NULL)//创建失败 
		return tree;
	tree=insert_node(tree,z);//插入节点 
	//cout<<tree->key<<" "<<tree<<endl; 
	tree=splaytree_splay(tree,key);//旋转节点(维护该树) 
	return tree;
}

splaytree_node *splaytree_delete(splaytree_node *tree,int key)//删除 
{
	splaytree_node *x;
	if(tree==NULL)
		return NULL;
	if(splaytree_search(tree,key)==NULL)
		return tree;//没有找到
	tree=splaytree_splay(tree,key);//旋转为根节点
	if(tree->left!=NULL)
	{
		x=splaytree_splay(tree->left,key);
		//将根节点左子树最大的节点旋转为根节点 
		
		x->right=tree->right;
	}
	else
		x=tree->right;
	free(tree);
	return x; 
}

void print_splaytree(splaytree_node *tree,int key,int direction)//打印树 
{
	if(tree!=NULL)
	{
		if(direction==0)
			cout<<tree->key<<" is root"<<endl;
		else
			cout<<tree->key<<" is "<<key<<" 's "<<direction<<" child"<<endl;
		print_splaytree(tree->left,tree->key,-1);
		print_splaytree(tree->right,tree->key,1);
	}
}
/*
10
1 2 3 4 5 6 7 8 9 10
7
*/
int main()
{
	//插入 
	int n;
	cin>>n;
	splaytree_node *root=NULL;
	int data;
	for(int i=1;i<=n;++i)
	{
		cin>>data;
		root=splaytree_insert(root,data);
		//cout<<root<<endl;
		print_splaytree(root,root->key,0);
	}
	cout<<"delete a node"<<endl;
	cin>>data;
	root=splaytree_delete(root,data);
	print_splaytree(root,root->key,0);
}







由于自顶向下伸展会导致,节点刷新的时候需要重新刷新,因此,在比较自下至上伸展之后,发现自下向上伸展对节点的维护更加方便,因此,下面学习伸展树的自下向上的伸展:

其实自下而上的伸展和自上而下的类似,直接上板子了:

//#pragma comment(linker, "/STACK:1024000000,1024000000") 

#include<stdio.h>
#include<string.h>  
#include<math.h>  
  
//#include<map>   
//#include<set>
#include<deque>  
#include<queue>  
#include<stack>  
#include<bitset> 
#include<string>  
#include<fstream>
#include<iostream>  
#include<algorithm>  
using namespace std;  

#define ll long long  
//#define max(a,b) (a)>(b)?(a):(b)
//#define min(a,b) (a)<(b)?(a):(b) 
#define clean(a,b) memset(a,b,sizeof(a))// 水印 
//std::ios::sync_with_stdio(false);
const int MAXN=2e5+10;
const int INF=0x3f3f3f3f;
const ll mod=1e9+7;

struct node{
	int id,data,maxval;
	node *l,*r,*f;
	/*
	使用从下到上的伸展方式,多了一个f指针记录父节点的位置
    找到要旋转的目标点,
    旋转目标点,直到目标点的父节点是目标父节点
	*/
};
node *root;

void show(node *x)
{
	if(x==NULL)
		return;
	cout<<x<<" : "<<x->id<<" "<<x->data<<" "<<x->l<<" "<<x->r<<endl;
	show(x->l);
	show(x->r);
}

void rotate(node *x,bool oper)
{
	node *y=x->f;
	if(y==NULL)
		return ;
	if(oper)
	{
		y->r=x->l;
		if(x->l!=NULL)
			x->l->f=y;
		x->l=y;
	}
	else
	{
		y->l=x->r;
		if(x->r!=NULL)
			x->r->f=y;
		x->r=y;
	}
	x->f=y->f;
	if(y->f!=NULL)
	{
		if(y==y->f->l)
			y->f->l=x;
		else
			y->f->r=x;
	}
	y->f=x;
	if(y==root)
		root=x;
	updata(y);
	updata(x);
}

void splay(node *x,node *f)
{
	node *y=x->f,*z=NULL;
	while(y!=f)
	{
		z=y->f;
		if(z==f)
		{
			rotate(x,x==y->r);
		}
		else
		{
			if((x==y->l ^ y==z->l)==0)//一字型 
			{
				rotate(y,y==z->r);
				rotate(x,x==y->r);
			}
			else//之字型 
			{
				rotate(x,x==y->r);
				rotate(x,x==z->r);
			}
		}
		y=x->f;
	}
	
}

void insert(int id,int data)
{
	node *z;
	z=(node*)malloc(sizeof(node));
	if(z==NULL)
		return ;
	z->data=data;
	z->id=id;
	z->maxval=data;
	z->l=z->r=z->f=NULL;
	node *y=NULL;
	node *x=root;
	while(x!=NULL)
	{
		y=x;
		if(z->id<x->id)
			x=x->l;
		else if(z->id>x->id)
			x=x->r;
		else
			return ;
	}
	if(y==NULL)
		root=z;
	else if(z->id<y->id)
	{
		y->l=z;
		z->f=y;
	}
	else
	{
		z->f=y;
		y->r=z;
	}
	splay(z,NULL);
}

void delete_node(node *x)
{
	if(x==NULL)
		return ;
	delete_node(x->l);
	delete_node(x->r);
	free(x);
}

int main()
{
	std::ios::sync_with_stdio(false);
	int n;
	while(cin>>n)
	{
		root=NULL;//每次重置根节点 
		int data;
		insert(0,0);//插入0个 
		for(int i=1;i<=n;++i)//按照下标建树 
		{
			cin>>data;
			insert(i,data);
		}
		insert(n+1,0);//插入n+1个,防止边界 
		show(root);
		delete_node(root);//注意最后的时候一定要释放空间,否则会MLE 
	}
}

猜你喜欢

转载自blog.csdn.net/qq_40482358/article/details/83305383
今日推荐