1.致歉:
最近事务过于繁多,更新的次数变得缓慢了不少,但是我至少在继续,继续努力
之后,由于田劲峰老师的论文的证明非常严谨,我看了很长时间仍然是不得精华,只能是将作为初学者该掌握的都掌握了,至于证明正确性和复杂度,实在是难以参透,日后等学习算法导论的时候,有能力再回来看证明,并且,终于可以开始步入正轨了
2.什么是BST:
BST全称是二叉查找树,字面意思很显而易见,这是我们为了查找而开发的一种优秀的查找数据结构
如果还没有理解过二叉查找树,
欢迎点击这里来进行初步学习
在这里我们先进行二叉查找树的一些简要的解析,进而引出我们的主要内容——自平衡二叉树之一的SBT
性质:
1.是一颗空树
2.非空树并且左子树所有的节点的数据都小于根节点
非空树并且右子树所有的节点的数据都大于根节点
3.BST内不存在相同键值的恋歌节点
4.根节点的左右子树都是BST
优劣:
优点:查找的时间复杂度:O(logn)非常高效的查找速度
缺点:对于存在部分有序的输入序列,会导致BST偏向于偏树,进而使得树的深度逐渐变大,导致查找速度不断退化,最坏的退化情况是退化成双向链表
引出:
对于BST的缺点,我们需要克服,我们最终需要的而是一种技能高效的查找数据又可以避免特殊的输入序列而导致的BST 的退化,所以这时候,我们引入了一种新的数据结构——自平衡二叉树,通过不同的操作,是的,这种进化过得BST可以拥有自动调节树高的能力,是的我们查找的时间复杂度稳定在O(logn)附近,避免特殊的序列而差生的退化
在众多的自平衡二叉树中——相对于好实现的,在算法竞赛中常用的要数我国大神陈启峰发明的SBT(size balance tree),相对于treap,红黑树,splay,AVL等一些自平衡二叉树来说,SBT好实现,并且其中的核心操作只有Maintain操作,Maintain操作的高效和灵活性成为SBT流行的原因
3.SBT的ADT
性质:
1.size[left[p]]>=size[left[right[p]]]/size[right[right[p]]]
2.size[right[p]]>=size[left[left[p]]]/size[right[left[p]]]
1.数据结构的选择:
SBT的实现来说,经过本博主的长时间的考虑,私以为,SBT用数组来实现是非常容易并且好考虑的,通过自己亲自实现链式的SBT发现,非常困难,而且对于算法的理解有所影响,所以说,我们这里采用数组来实现,在这里我们在C++代码中采用了引用来进行传址,这一点是非常的核心的,我们是必须要这么做的,在下面的代码中我会讲解这一点的必要性
2.ADT
成员变量:
//首先我们虽然不列举出来,但是实际上,在我们思考的时候我们要注意,我们要抽象出一段假想的空间,这一段空间是顺序的1-N,代表我们的输入的顺序
left[i] 第i次输入的节点的左节点的输入的次序(注意联系上面的那段假想的数据空间)
right[i] 第i次输入的节点的右节点的输入的次序
size[i] 第i次输入的节点为根的子树的节点的总数目,这个size域是SBT非常重要的核心数据
key[i] 第i次输入的节点的键值
num 该SBT的节点总数
root 该SBT的根节点的输入的次序
//我们根据不同的操作获得的输入次序为自变量,可以轻松的得到该输入次序的输入元素的键值和size域
函数:
最核心的两个:(根节点子树左右旋操作)
left_rotation(int&)
right_rotation(int&)
Maintain(int&,bool) //O(1)均摊时间复杂度
insert(int&,int);
delete_(int&,int,bool);
find(int&,int)
select(int&,int)
after(int&,int)
pre(int&,int)
3.核心旋转操作解析:
1.左旋:
其实这里的旋转操作就相当于是一系列的指针的操作
代码如下:
void SBT::left_rotation(int& p) //我们已经指定了x指针,要进行操作还需要指向y的指针
{
int k=right[p]; //确定指向y的指针k
right[p]=left[k]; //x右儿子指向b
left[k]=p; //y的左儿子指向x
size[k]=size[p]; //因为k这里操作完之后,相当于是y取代了x的位置,所以说,size域也应该是没变之前的x的size域
size[p]=size[left[p]]+size[right[p]]+1; //理所应当
p=k; //这里的转换牵扯到引用
}
ps:在这里,我对引用的必要性做一下解释,如果没有用引用,p在这里只是一个随机变量,p的任何改变是不会影响到上一层调用函数的形参表中的值得,所以我们之后需要将上一层的形参表中的指针的指向同步修改,就必须采用引用
2.右旋:
原理同左旋:
代码如下:
void SBT::right_rotation(int& p)
{
int k=left[p];
left[p]=right[k];
right[k]=p;
size[k]=size[p];
size[p]=size[left[p]]+size[right[p]]+1;
p=k;
}
4.最具活力的Maintain操作
maintain操作就是根据不同的不平衡情况从而采取不同的一系列的旋转操作
首先明确一点,在不平衡以前,始终是一颗SBT
1.Case1:
size[left[left[p]]]>size[right[p]]
这里就是size[A]>size[R]
首先这里A与R比较,我们为了保持平衡,通过右旋操作把A拉高
1.size[A]<size[B]
2.size[B]<size[C]或者size[B]<size[D]
有人这里可能会问,有没有可能
size[B]>=size[C],size[B]>=size[D]同时不成立,在这里通过反证法证明是不会的
证明如下:
条件:
size[A]>size[R]
size[B]<size[C]
size[B]<size[D]
size[B]>size[left[A]] //这两点的原因是,在不平衡之前,原树是一颗SBT
size[B]>size[right[A]]
结论:
size[left[A]]+size[right[A]]+1<size[C]+size[D]+1=size[R]
=>size[A]<size[R]
与条件相违背,说明两种情况不能同时成立
之后我们再次进行递归定义维护Maintain(T),因为Maintain(T)维护之后,L树的右儿子的左右子树就变得不可预料了,我们不知道其和A的size关系,所以说,还要再次用Maintain(L)维护以T为根的子树,之后维护停止,所有的操作都是常数时间的
2.Case2:
size[right[left[p]]]>size[right[p]]
在这里就是size[B]>size[R]
首先我们左旋,将不平衡性转化到Case1的情况
之后同Case1,先右旋:
因为多次操作之后边的情况过于复杂,我们直接先Maintain(L),再Maintain(T),然后Maintain(B)
3.Case3,Case4和Case1,2是镜像对称,这里操作原理都是一样的,就不过多赘述了
5.其他有用的操作:
在之后附上的代码中解析
4.代码:
#include"iostream"
#include"cstdio"
#include"cstring"
#include"cstdlib"
#define N 1000
using namespace std;
class SBT
{
public:
SBT()
{
memset(size,0,sizeof(size));
memset(left,0,sizeof(left));
memset(right,0,sizeof(right));
memset(key,0,sizeof(key));
num=root=0;
}
void left_rotation(int&);
void right_rotation(int&);
void insert(int&,int);
int delete_(int&,int,bool); //没有找到要烧出的元素,返回-1,否则返回被删除的点的
void preorder(int);
void midorder(int);
void maintain(int&,bool);
int& returnroot()
{
return root;
}
int find(int&,int); //返回查找到的点的输入的序号,根据序号可以得到该点的size以及key信息
int select(int&,int);
int after(int&,int);
int pre(int&,int);
private:
int size[N];
int left[N];
int right[N];
int key[N];
int num;
int root;
};
void SBT::left_rotation(int& p)
{
int k=right[p];
right[p]=left[k];
left[k]=p;
size[k]=size[p];
size[p]=size[left[p]]+size[right[p]]+1;
p=k;
}
void SBT::right_rotation(int& p)
{
int k=left[p];
left[p]=right[k];
right[k]=p;
size[k]=size[p];
size[p]=size[left[p]]+size[right[p]]+1;
p=k;
}
void SBT::maintain(int& p,bool judge)
{
if(judge==false)
{
if(size[left[left[p]]]>size[right[p]]) right_rotation(p);
else
{
if(size[right[left[p]]]>size[right[p]])
{
left_rotation(left[p]);
right_rotation(p);
}
else return ;
}
}
else
{
if(size[right[right[p]]]>size[left[p]]) right_rotation(p);
else
{
if(size[left[right[p]]]>size[left[p]])
{
right_rotation(right[p]);
left_rotation(p);
}
else return ;
}
}
maintain(left[p],false);
maintain(right[p],true);
maintain(p,false);
maintain(p,true);
}
void SBT::insert(int& p,int k)
{
if(p==0)
{
key[p=++num]=k;
size[num]=1;
}
else
{
size[p]++;
if(k<key[p]) insert(left[p],k);
else insert(right[p],k);
}
if(p!=0) maintain(p,k>=key[p]);
}
int SBT::delete_(int& p,int k,bool judge)
{
if(key[p]==k) judge=1;
if(key[p]==k||(left[p]==0&&k<key[p])||(right[p]==0&&k>key[p]))
{
if(((left[p]==0&&k<key[p])||(right[p]==0&&k>key[p]))&&judge==0)
{
return -1; //直接返回给主调函数为-1,代表查找失败
}
else //剩下的情况就是KEY[P]=K&&judge=1(代表之前已经找到过)
{
int r=key[p];
if(left[p]==0||right[p]==0) p=left[p]+right[p];
else key[p]=delete_(left[p],k+1,judge);
return r;
}
}
else
{
if(key[p]>k) return delete_(left[p],k,judge);
else return delete_(right[p],k,judge);
}
}
int SBT::find(int& p,int k)
{
if(p==0||key[p]==k) return p;
else
{
if(k<key[p]) return find(left[p],k);
else return find(right[p],k);
}
}
void SBT::preorder(int p)
{
if(p==0) return ;
else
{
cout<<key[p]<<' ';
preorder(left[p]);
preorder(right[p]);
}
}
void SBT::midorder(int p)
{
if(p==0) return ;
else
{
midorder(left[p]);
cout<<key[p]<<' ';
midorder(right[p]);
}
}
int SBT::select(int& p,int k) //选择第k大的元素
{
if(size[left[p]]+1==k) return p;
else
{
if(size[left[p]]+1<k) return select(right[p],k-size[left[p]]-1);
else return select(left[p],k);
}
}
int SBT::after(int& p,int k)
{
if(p==0) return -1; //-1代表查找失败的标记
else
{
if(key[p]==k) return key[right[p]];
else
{
if(k<key[p]) return after(left[p],k);
else return after(right[p],k);
}
}
}
int SBT::pre(int& p,int k)
{
if(p==0) return -1;
else
{
if(key[p]==k) return key[left[p]];
else
{
if(k<key[p]) return pre(left[p],k);
else return pre(right[p],k);
}
}
}
int main()
{
SBT my; //这里对left,right,size,key的含义可能不清楚,我来解释一下,每次输入一批数据的时候,我们嘉定对输入的数据进行编号,left[i]存储的是i次输入的数据的左儿子的输入的顺序编号
my.insert(my.returnroot(),5);
my.insert(my.returnroot(),2);
my.insert(my.returnroot(),9);
my.insert(my.returnroot(),4);
my.insert(my.returnroot(),6);
my.insert(my.returnroot(),8);
my.preorder(my.returnroot());
cout<<endl;
my.insert(my.returnroot(),7);
my.preorder(my.returnroot());
cout<<endl;
my.delete_(my.returnroot(),5,0);
my.preorder(my.returnroot());
cout<<endl;
cout<<my.after(my.returnroot(),8)<<endl;
return 0;
}
6.模板化SBT
#include"iostream"
#include"cstdio"
#include"cstring"
#include"cstdlib"
#define N 1000
using namespace std;
template<typename T>
class SBT
{
public:
SBT()
{
memset(size,0,sizeof(size));
memset(key,0,sizeof(key));
memset(left,0,sizeof(left));
memset(right,0,sizeof(right));
num=root=0;
}
void left_rotation(int&);
void right_rotation(int&);
void maintain(int&,bool);
void insert(int&,T);
T delete_(int&,T,bool);
void preorder(int);
void midorder(int);
T select(int&,int);
T after(int&,T);
T pre(int&,T);
int find(int&,T);
void afterdelete_();
int& returnroot()
{
return root;
}
private:
T key[N];
int left[N];
int right[N];
int size[N];
int num;
int root;
};
template<typename T>
void SBT<T>::left_rotation(int& p)
{
int k=right[p];
right[p]=left[k];
left[k]=p;
size[k]=size[p];
size[p]=size[left[p]]+size[right[p]]+1;
p=k;
}
template<typename T>
void SBT<T>::right_rotation(int& p)
{
int k=left[p];
left[p]=right[k];
right[k]=p;
size[k]=size[p];
size[p]=size[left[p]]+size[right[p]]+1;
p=k;
}
template<typename T>
void SBT<T>::maintain(int& p,bool judge)
{
if(judge==false)
{
if(size[left[left[p]]]>size[right[p]]) right_rotation(p);
else
{
if(size[right[left[p]]]>size[right[p]])
{
left_rotation(left[p]);
right_rotation(p);
}
else return ;
}
}
else
{
if(size[right[right[p]]]>size[left[p]]) left_rotation(p);
else
{
if(size[left[right[p]]]>size[left[p]])
{
right_rotation(right[p]);
left_rotation(p);
}
else return ;
}
}
maintain(left[p],false);
maintain(right[p],true);
maintain(p,true);
maintain(p,false);
}
template<typename T>
void SBT<T>::insert(int& p,T k)
{
if(p==0)
{
key[p=++num]=k;
size[p]=1;
}
else
{
size[p]++;
if(key[p]>k) insert(left[p],k);
else insert(right[p],k);
maintain(p,k>=key[p]);
}
}
template<typename T>
T SBT<T>::delete_(int& p,T k,bool judge)
{
if(key[p]==k) judge=1;
if(key[p]==k||(left[p]==0&&k<key[p])||(right[p]==0&&k>key[p]))
{
if(((left[p]==0&&k<key[p])||(right[p]==0&&k>key[p]))&&judge==0) return key[0];
else
{
T r=key[p];
if(left[p]==0||right[p]==0) p=left[p]+right[p];
else key[p]=delete_(left[p],k,1);
return r;
}
}
else
{
size[p]--;
if(key[p]>k) return delet_(left[p],k,judge);
else return delet_(right[p],k,judge);
}
}
template<typename T>
void SBT<T>::afterdelete_()
{
T r;
cout<<"输入你要删除的节点的关键字"<<endl;
cin>>r;
delete_(root,r,0);
num--;
}
template<typename T>
void SBT<T>::preorder(int p)
{
if(p==0) return ;
else
{
cout<<key[p]<<' ';
preorder(left[p]);
preorder(right[p]);
}
}
template<typename T>
void SBT<T>::midorder(int p)
{
if(p==0) return ;
else
{
midorder(left[p]);
cout<<key[p]<<' ';
midorder(right[p]);
}
}
template<typename T>
T SBT<T>::select(int& p,int k)
{
if(size[left[p]]+1==k) return key[p];
else
{
if(size[left[p]]+1>k) return select(left[p],k);
else return select(right[p],k-size[left[p]]-1);
}
}
template<typename T>
int SBT<T>::find(int& p,T k)
{
if(p==0||key[p]==k) return p;
else
{
if(key[p]>k) return find(left[p],k);
else return find(right[p],k);
}
}
template<typename T>
T SBT<T>::pre(int& p,T k)
{
if(p==0) return key[0];
if(key[p]==k) return key[left[p]];
else
{
if(key[p]>k) return pre(left[p],k);
else return pre(right[p],k);
}
}
template<typename T>
T SBT<T>::after(int& p,T k)
{
if(p==0) return key[0];
if(key[p]==k) return key[right[p]];
else
{
if(key[p]>k) return after(left[p],k);
else return after(right[p],k);
}
}
int main()
{
SBT<double> my;
my.insert(my.returnroot(),5);
my.insert(my.returnroot(),2);
my.insert(my.returnroot(),9);
my.insert(my.returnroot(),4);
my.insert(my.returnroot(),6);
my.insert(my.returnroot(),8);
my.insert(my.returnroot(),7);
my.insert(my.returnroot(),7.5);
my.preorder(my.returnroot());
return 0;
}
7.应用:
1.SBT的select函数可以求最大最小,第k大的元素,相对于我们用对来实现的话可能在建完树之后,操作会更快一点,我之后会出一个这个专题
2.SBT优化LCS问题的动态规划
6.鸣谢:
长时间没有读懂SBT
感谢:
田劲峰老师2011年的论文
感谢NOI百度贴吧的大牛的详解