1. 程式人生 > >資料結構--伸展樹(伸展樹構建二叉搜尋樹)-學習筆記

資料結構--伸展樹(伸展樹構建二叉搜尋樹)-學習筆記

/*-----------------------伸展樹----------------------------*/ 
伸展數(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 
	}
}