1、左偏树
1.1定义
左偏数是一颗二叉树,并具有堆性质。左偏树具有两个属性:键值(key)和距离(dist)。
键值(key):用于节点比较大小的属性,类似于堆中节点的键值
外节点:左子树或右子树为空的节点称为外节点,左子树和右子树可以同时为空。所以叶子节点必是外节点,定义外节点的距离值为0,空节点的距离值为-1。空节点即树中不存在的节点,表示为NULL,dist(NULL)=-1
距离(dist):一个不是外节点的距离定义为其到子树中最近的外节点的距离,即两个节点之间路径的权值之和,可以理解外两个节点之间的边数。左右子树均不为空的节点不是外节点。左偏数的距离定义数中根节点的距离。左偏树的深度和距离无必然联系,一颗向左偏的链也是左偏树,其距离为0,深度为n(树的深度:根节点为第一层次,根的左右儿子为第二层次,定义树中的最大层次为树的深度或高度).如下图所示为一颗左偏树。
如上图所示,圆圈内的数值是键值key,圆圈左上角的数值是此节点的距离值,即dist值。
1.2 性质
符号如下:
T(n):一颗具有n个节点的左偏数
x:节点x
lson:节点x的左儿子节点
rson:节点x的右儿子节点
parent(x):节点x的父节点
left(x):节点x的左子树
right(x):节点y的右子树
一个树是左偏树,需满足以下基本性质
(基本性质1)节点的键值小于或等于它左右儿子的键值,也称为堆性质。即key(x)<=key(lson),且key(x)<=key(rson)。此处定义的是小根堆。由于堆性质,则左偏数取最小值的时间复杂度为
(基本性质2)每个节点的左儿子的dist值大于等于右儿子的dist值。即左偏性质。dist(lson)>=dist(rson)。
左偏树的左右儿子也是左偏树,除了两条基本性质外,还有如下性质:
(扩展性质1):任意节点,其距离等于其右儿子的距离加1。即 dist(i)=dist(rson)+1.
当一个节点距离越大时,代表其离外节点的距离越远,由于基本性质2,左儿子的距离>=右儿子的距离,所以节点到其最近的外节点是通过右儿子找到的,根据距离的定义,节点的距离等于其右儿子的距离加1
(扩展性质2)左偏树的距离是定值(大于0),则节点数最少的左偏数是完全二叉树(实为满二叉树):
当左偏数距离定值时,节点数最少的时候是每个节点x,dist[lson]=dist[rson] ,则这样的二叉树是一个完全二叉树(实为满二叉树),同时对于一颗是完全二叉树的左偏树来说存在如下关系:树的深度(高度)=树的距离+1。树的深度:根节点为第一层次,根的左右儿子为第二层次,定义树中的最大层次为树的深度(高度)。
(扩展性质3):若左偏数的距离为k,则其节点数不小于
当一个左偏树距离是一个定值d,节点最少的时候它是一颗完全二叉树(实为满二叉树),则完全二叉树的节点数为,满二叉树的节点数等于
,所以节点数大于等于
(扩展性质4):一个左偏树有n个节点,其距离不超过,即dist(T(n))<=
一颗左偏数节点数是个定值n,距离为k,由扩展性质2得到,所以
。当节点数是个定值,距离最大的左偏树会满足如下条件:叶子节点只在层次最大的两层上出现;设树的深度为x,则前x-1层是满二叉树
1.3 左偏树的操作
左偏树除了有堆的基本操作(插入元素、删除元素、建堆)外,还有一个它重要的操作:合并。
1.3.1 合并操作
合并左偏树x和y操作的过程如下:
(1)在x、y中取较小的值最为合并后的根,假设x的值较小,则合并后x的根为新树的根,x的左子树为新树的左子树
(2)递归地合并x的右子树和y,若合并后不满足左偏性质(dist(lson)<dist[rson]),则交换左右儿子,即swap(lson,rson);
(3)x的右子树或y 为空时,合并算法结束。
每递归一层,会有一个堆的距离减1,每递归一次,会将分解的右子树参加合并,而根据扩展性质4,一个n个节点的左偏数的距离不会超过,设两个左偏树的距离为n和m,所以最坏情况下,需要递归
+
次,所以需要的时间复杂度
.伪代码如下:
def merge(A,B)
{
if (A==NULL) return B
if (B==NULL) return A
if (key(A)>key(B))
swap(A,B)
right(A)=merge(right(A),B)
if (dist(left(A))<dist(right(A))) //不满足左偏性质,进行交换
swap(left(A),right(A))
if(righr(A)==NULL) dist(A)=0
dist(A)=dist(right(A))+1
return A
}
个人认为,按上述方法合并,设C=merge(A,B),则min(dist(A),dist(B))<=dist(C)<=max(dist(A),dist(B))+1。还有一种不需要交换左右儿子的做法,即将dist较大的作为作儿子,dist较小的作为右儿子 。
1.3.2 插入一个新节点
插入一个新节点,将相当于插入一颗只有一颗节点的左偏树,伪代码如下:
由于合并操作的复杂度为 ,而只有一个节点左偏树的距离为0,所以操作复杂度为
def insert(x,B)
{
A=MakeIntoTree(x)
return merge(A,B)
}
1.3.3 删除最小节点
删除最小节点即删除根节点,删除根节点后,将左右子树合并即可得到新树.伪代码如下:
def delete(A)
{
t=key(root(A))
A=merge(left(A),reight(A))
return t
}
删除根节点后,左子树的距离为不大于,右子树的距离为
,所以合并左右子树的复杂度为
1.3.3 左偏树的构建
左偏树的构建有两种方法,一种是逐个节点插入法,另外一种是采用合并的方法建堆。具体方法如下。
(1)逐个插入法,在节点依次为1、2、3...n-1的左偏树上,合并一个只有一个节点的左偏树,复杂度为log1+log2+...log(n)=
(2)合并法。算法步骤如下:
(a)将n个节点放入先进先出队列,每个节点作为一颗左偏树
(b)从队首取出两颗左偏数,合并后放入队尾
(C)当队列中只有一颗左偏树时,算法结束
算法的实现过程如下图
构建堆的伪代码如下:
def create(queue q)
{
while(queue.count()!=1)
{
t1=q.pop();
t2=q.pop();
t1=merge(t1,t2)
q.push(t1)
}
return q.pop()
}
第二种算法的复杂度如下:首先是对只有1节点的左偏树进行合并次,其次对2个节点的左偏树进行合并
次,再次是4个节点的左偏树进行合并
次,...
个节点的左偏树合并
次,...最后对
、
个节点(
在
与
两个整数之间取绝对值差值最小的,若n=7,
为4;n=11,
为4)的左偏树进行合并1次,所以总的复杂度为
1.3.4 删除任意已知节点
“已知”的含义是节点存在于左偏数中,但左偏数除了去最值外,不能有效搜索指定键值的结点。删除任意已知节点,首先要找到这个已知节点,左偏数具有堆性质,可以在的时间里找到这个节点。找到这个结单后,设要删除的节点为x,合并x的左右子树,然后自底向上维护距离值,不满足左偏性质则交换左右儿子,直至距离值无需更新时,算法结束。
具体而言,设q是x的父节点。合并左右儿子后的新树设为p,即p=merge(left(x),right(x)),设节点x的父节点为q,具体分析如下:
(1)x是原树中的根节点,则删除x节点后,合并左右子树后,算法结束
(2)x不是原树中的根节点,合并后的树为p,新的距离为dis(p),dist(p)与dist(q)的值相比有如下几种
(a) dist(p)=dist(q)-1。即新树中q的左右儿子距离相等,无法新树p是q的左子树还是右子树,满足左偏条件,不需进行交换
(b)dist(p)<dist(q)-1。如此时p是q的右子树,则需要更新q的距离值,并一直更新到距离值无需再更新时为止,期间不满足左偏条件的要进行交换;如果p是q的左子树,则需要交换p和right(q),并更新q的距离 值,并一直更新到距离值无需再更新时为止,期间不满足左偏条件的要进行交换
(c)dist(p)>dist(q)-1。新树的p的距离大于原x节点的距离。如果新树p在q的左子树上,则满足左偏条件,不需要交换和更新,如果新树p在q的右子树上,如果新树p的距离大于q的左子树的距离,则需要交换,同时更新其q的距离,并一直更新到距离值无需再更新时为止,期间不满足左偏条件的要进行交换
删除任意节点的伪代码如下:
def deleteany(x)
{
q=parent(x)
p=merge(left(x),right(x))
parent(p)=q;
if(q!=NULL&&rson(q)==x)
right(q)=p
if(q!=NULL&&lson(q)==x)
left(q)=p
while(q!=NULL){
if(dist(left(q)>right(q))
swap(left(q),right(q))
if(dist(q)==dist(right(q))+1)
break;
dist(q)=dist(q)+1
q=parent(q)
}
}
上述删除任意节点的复杂度分析如下。不需要向上递推的情况有两种,一是删除的是原树的根节点,即q==NULL,而合并的复杂为不超过,二是删除后dist(p)=dist(q)-1。而需要向上递推的情况也有两种:
(1)dist(p)<dist(q)-1。即删除后新树的p的距离小于right(q)的距离,则需要向上递推更新,而根据扩展性质4,其向上递推的层次为
(2)dist(p)>dist(q)-1。删除后新树的p的距离大于right(q)的距离,且新树p位于q的右子树上,也需要向上递推更新,根据扩展性质4,向上递推不会超过
由上述分析,合并的复杂为不超过,向上递推的复杂为不超过
,所以删除任意节点的复杂度不超过
1.3.5 整个堆加/减一个值,或乘上一个整数
在整个堆上加上一个值,首先在根上打上标记,然后删除根、合并左右子树时,将标记下传即可。上述合并的算法伪代码修改为
def merge(A,B)
{
if (A==NULL) return B
if (B==NULL) return A
if (key(A)>key(B))
swap(A,B)
pushdown(A) //下传标记
right(A)=merge(right(A),B)
if (dist(left(A))<dist(right(A))) //不满足左偏性质,进行交换
swap(left(A),right(A))
if(righr(A)==NULL) dist(A)=0
dist(A)=dist(right(A))+1
return A
}
标记函数pushdown里,可以做加法,也可以做减法,或者可以乘上一个正数。
2 总结
左偏数是同时具有左偏性和堆性质的二叉树。左偏树的两种极端情况,一种是有序表,此时的左偏树是一个向左的链,且树的距离为0,深度=n;另外一种是平衡二叉树(完全二叉树是平衡树)。
上述操作的复杂度见下表:
左偏树操作 | 合并 | 插入节点 | 构建操作 | 删除任意节点 |
复杂度 |
参考资料:
1 左偏树的特点及其应用,黄源河 .https://wenku.baidu.com/view/20e9ff18964bcf84b9d57ba1.html
2OiWII 左偏树 http://oi-wiki.com/ds/leftist-tree/