Size Balanced Tree

 

今天上网搜索关于红黑树的资料时,发现一种新的平衡二叉树(SBT),据说各方面性能很好,先摘录在此,以后再细看。

 

Size Balanced Tree(SBT)是一种平衡二叉查找树。它的论文由中国广东中山纪念中学的陈启峰于2006年底完成,并在Winter Camp 2007中发表。由于SBT的拼写很容易找到中文谐音,它常被中国的OIer们戏称为“傻X树”、“Super BT”等。但它的性能并不SB,编写起来也并不BT。恰恰相反,SBT易于实现,且据陈启峰论文中所言,“这是目前为止速度最快的高级二叉搜索树”。它能在O(logn)的时间内完成所有BST的相关操作。而且由于SBT赖以保持平衡的是Size域而不是其他“无用”的域,它可以很方便地实现动态顺序统计中的select和rank。

目录

[隐藏]
<script type=text/javascript> if (window.showTocToggle) { var tocShowText = "显示"; var tocHideText = "隐藏"; showTocToggle(); } </script>

[编辑] 性质

Size Balanced Tree(SBT)是一种通过大小(Size)域来保持平衡的二叉搜索树,它也因此得名。它总是满足:
对于SBT的每一个结点 t:

  1. 性质(a) s[right[t]]≥s[left[left[t]]], s[right[left[t]]]
  2. 性质(b) s[left[t]]≥s[right[right[t]]], s[left[right[t]]]

即每棵子树的大小不小于其兄弟的子树大小。

Image:Sbt1.PNG
图1

如图(圈代表结点,三角代表SBT,下同):

  1. s[R] ≥ s[A] , s[B]
  2. s[L] ≥ s[C] , s[D]

[编辑] 旋转

SBT的旋转(Rotations)与其他高级BST相同。它是下面提到的Maintain操作的基础。

image:sbt2.png
图2

[编辑] 左旋转

Left-Rotate (t)

1     k ← right[t]
2     right[t] ← left[k]
3     left[k] ← t
4     s[k] ← s[t]
5     s[t] ← s[left[t]] + s[right[t]] + 1
6     t ← k

[编辑] 右旋转

Right-Rotate(t)

1     k ← left[t]
2     left[t] ← right[k]
3     right[k] ← t
4     s[k] ← s[t]
5     s[t] ← s[left[t]] + s[right[t]] + 1
6     t ← k

[编辑] 保持性质(Maintain)

当我们插入或删除一个结点后,SBT的大小就发生了改变。这种改变有可能导致性质(a)或(b)被破坏。这时,我们需要用Maintain操作来修复这棵树。Maintain操作是SBT中最具活力的一个独特过程;Maintain(T)用于修复以T为根的 SBT。调用Maintain(T)的前提条件是T的子树都已经是SBT了。
我们需要讨论的有4种情况。由于性质a和性质b是对称的,所以我们仅仅详细的讨论性质a。

  1. 第一种情况:s[left[left[t]]>s[right[t]]
    image:sbt1.PNG
    图3(同图1)
    如图3,执行完Insert(left[t],v)后发生S[A]>S[R],我们可以执行以下的指令来修复SBT:
    1. 首先执行Right-Ratote(t),这个操作让图3变成图4;
      image:sbt4.PNG
      图4
    2. 在这之后,有时候这棵树还仍然不是一棵SBT,因为 或者 也是可能发生的。所以就有必要继续调用Maintian(t)。
    3. 结点L的右子树有可能被连续调整,因为有可能由于性质的破坏需要再一次运行Maintain(L)。

  2. 第二种情况:s[right[left[t]]>s[right[t]]
    image:sbt5.PNG
    图5
    在执行完Insert(left[t],v)后发生s[B]>s[R],如图5,这种调整要比情况1复杂一些。我们可以执行下面的操作来修复:
    1. 在执行完Left-Ratote(L)后,图5就会变成下面图6那样了。
      image:sbt6.PNG
      图6
    2. 然后执行Right-Ratote(T),最后的结果就会由图6转变成为下面的图7。
      image:sbt7.PNG
      图7
    3. 在第1步和第2步过后,整棵树就变得非常不可预料了。万幸的是,在图7中,子树A、E、F和R仍就是SBT,所以我们可以调用Maintain(L)和Maintain(T)来修复结点B的子树。
    4. 在第3步之后,子树都已经是SBT了,但是在结点B上还可能不满足性质a或性质b,因此我们需要再一次调用Maintain(B)。

  3. 第三种情况:s[right[right[t]]>s[left[t]]
    与情况1对称。
  4. 第四种情况:s[left[right[t]]>s[left[t]]
    与情况2对称。



通过前面的分析,很容易写出一个普通的Maintain。

Maintain (t)

01     If s[left[left[t]]>s[right[t]] then    //case1
02          Right-Rotate(t)
03          Maintain(right[t])
04          Maintain(t)
05          Exit
06     If s[right[left[t]]>s[right[t]] then   //case2
07          Left-Rotate(left[t])
08          Right-Rotate(t)
09          Maintain(left[t])
10          Maintain(right[t])
11          Maintain(t)
12          Exit
13     If s[right[right[t]]>s[left[t]] then   //case1'
14          Left-Rotate(t)
15          Maintain(left[t])
16          Maintain(t)
17          Exit
18     If s[left[right[t]]>s[left[t]] then    //case2'
19          Right-Rotate(right[t])
20          Left-Rotate(t)
21          Maintain(left[t])
22          Maintain(right[t])
23          Maintain(t)


前面的标准过程的伪代码有一点复杂和缓慢。通常我们可以保证性质a和性质b的满足,因此我们只需要检查情况1和情况2或者情况3和情况4,这样可以提高速度。所以在那种情况下,我们需要增加一个布尔(boolean)型变量:flag,来避免毫无意义的判断。如果flag是false,那么检查情况1和情况2;否则检查情况3和情况4。

Maintain (t,flag)

01     If flag=false then
02          If s[left[left[t]]>s[right[t]] then      //case1
03               Right-Rotate(t)
04          Else
05               If s[right[left[t]]>s[right[t]] then   //case2
06                    Left-Rotate(left[t])
07                     Right-Rotate(t)
08          Else                                   //needn’t repair
09               Exit
10     Else
11          If s[right[right[t]]>s[left[t]] then      //case1'
12               Left-Rotate(t)
13          Else
14               If s[left[right[t]]>s[left[t]] then     //case2'
15                    Right-Rotate(right[t])
16                    Left-Rotate(t)
17          Else                                    //needn’t repair
18               Exit
19     Maintain(left[t],false)                     //repair the left subtree
20     Maintain(right[t],true)                     //repair the right subtree
21     Maintain(t,false)                           //repair the whole tree
22     Maintain(t,true)                            //repair the whole tree

为什么Maintain(left[t],true)和Maintain(right[t],false)被省略了呢?您可以在陈启峰论文第六部分的分析中找到答案。
其他可以从论文中获得的信息:每次SBT后树的总深度递减的证明;Maintain的平摊运行时间是O(1)的证明(也就是说你不必担心Maintain这个递归过程是否会永不停止)等。

[编辑] 基本操作

[编辑] 查找

SBT的查找操作与普通BST完全相同。下面的过程将返回指向目标节点的指针。

Search(t,k)

1     if x=NIL or k=key[t]
2        then return x
3     if k<key[x]
4        then return Search(left[x],k)
5        else return Search(right[x],k)

[编辑] 取大/取小

由于SBT本身已经维护了size,因此这两项可用Select操作完成。

[编辑] 后继

SBT的后继操作与普通BST完全相同。

[编辑] 前趋

SBT的前趋操作与普通BST完全相同。它与上面的后继操作对称。

[编辑] 插入

SBT的插入操作很简单。它仅仅比普通BST的多出了一个Maintain操作和对s的简单维护。下面这个过程将一个节点v插入SBT中。

Insert (t,v)

1     If t=0 then
2        t ← v
3     Else
4        s[t] ← s[t]+1
5         If v<key[t] then
6              Simple-Insert(left[t],v)
7         Else
8             Simple-Insert(right[t],v)
9     Maintain(t,v≥key[t])

[编辑] 删除

与普通维护size域的BST删除相同。
关于无需Maintain的说明by sqybi:
在删除之前,可以保证整棵树是一棵SBT。当删除之后,虽然不能保证这棵树还是SBT,但是这时整棵树的最大深度并没有改变,所以时间复杂度也不会增加。这时,Maintain就显得是多余的了。

[编辑] 动态顺序统计操作

由于SBT本来就是靠着size域来维持平衡的,当我们进行动态顺序统计操作时,我们就无需去“额外”维护一个size域来进行数据结构的扩张。这样,以下操作就与其他高级BST扩张后的动态顺序统计操作完全一样了。

[编辑] 检索具有给定排序的元素

下面这个过程将返回一个指向以x为根的子树中包含第i小关键字的节点的指针。

Select(x,i)

1     r ← size[left[x]] + 1
2     if(i=r)
3          then return x
4     else if i<r
5          then return Select(left[x],r)
6     else return Select(right[x],i-r)

[编辑] 求元素的秩

SBT的rank操作与普通BST完全相同。

[编辑] 性能分析

SBT的高度是O(logn),Maintain是O(1),所有主要操作都是O(logn)。

[编辑] 源码

C
C++
Pascal

[编辑] 参考资料

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
首先,SizeBalancedTree是一种基于平衡树的数据结构,它的特点是除了具备平衡树的特性之外,还能够在节点中记录子树大小。这样可以方便地查询某个节点的排名或者寻找某个排名的节点。 以下是Java实现SizeBalancedTree的完整源码: ```java import java.util.Random; public class SizeBalancedTree { private static final int MAXN = 100005; private static final Random random = new Random(); static class Node { int key, size, cnt; Node ch[] = new Node[2]; Node(int key) { this.key = key; size = cnt = 1; } } private Node nullNode = new Node(0); private Node root = nullNode; private Node newNode(int key) { Node x = new Node(key); x.ch[0] = x.ch[1] = nullNode; return x; } private void pushup(Node x) { x.size = x.ch[0].size + x.ch[1].size + x.cnt; } private Node rotate(Node x, int k) { Node y = x.ch[k^1]; x.ch[k^1] = y.ch[k]; y.ch[k] = x; pushup(x); pushup(y); return y; } private Node maintain(Node x, boolean flag) { if (x == nullNode) return x; if (flag && x.ch[0].size > x.ch[1].size * 3) { x = rotate(x, 1); } else if (!flag && x.ch[1].size > x.ch[0].size * 3) { x = rotate(x, 0); } return x; } private Node insert(Node x, int key) { if (x == nullNode) return newNode(key); if (key == x.key) { x.cnt++; pushup(x); return x; } boolean k = key > x.key; x.ch[k] = insert(x.ch[k], key); pushup(x); return maintain(x, k); } private Node remove(Node x, int key) { if (x == nullNode) return x; if (key == x.key) { if (x.cnt > 1) { x.cnt--; pushup(x); return x; } if (x.ch[0] == nullNode && x.ch[1] == nullNode) { return nullNode; } else if (x.ch[0] == nullNode) { x = x.ch[1]; } else if (x.ch[1] == nullNode) { x = x.ch[0]; } else { boolean k = x.ch[0].size > x.ch[1].size; x = rotate(x, k ? 0 : 1); x.ch[k^1] = remove(x.ch[k^1], key); pushup(x); x = maintain(x, k^1); } } else { boolean k = key > x.key; x.ch[k] = remove(x.ch[k], key); pushup(x); x = maintain(x, k); } return x; } private Node getKth(Node x, int k) { if (x == nullNode) return nullNode; int rank = x.ch[0].size + 1; if (k < rank) { return getKth(x.ch[0], k); } else if (k > rank + x.ch[1].size) { return getKth(x.ch[1], k - rank - x.cnt); } else { return x; } } public void insert(int key) { root = insert(root, key); } public void remove(int key) { root = remove(root, key); } public int getRank(int key) { int rank = 0; Node x = root; while (x != nullNode) { if (key == x.key) { return rank + x.ch[0].size + 1; } else if (key < x.key) { x = x.ch[0]; } else { rank += x.ch[0].size + x.cnt; x = x.ch[1]; } } return rank; } public int getKth(int k) { Node x = getKth(root, k); return x.key; } public static void main(String[] args) { SizeBalancedTree sbt = new SizeBalancedTree(); sbt.insert(1); sbt.insert(2); sbt.insert(3); sbt.insert(4); sbt.insert(5); System.out.println(sbt.getRank(3)); // output: 3 System.out.println(sbt.getKth(3)); // output: 3 sbt.remove(3); System.out.println(sbt.getRank(3)); // output: 0 System.out.println(sbt.getKth(3)); // output: 4 } } ``` 在这里,我们定义了一个内部类Node来表示SizeBalancedTree的节点,包含了键值key、子树大小size和数量cnt。同时,我们也定义了一个nullNode,用于代表空节点。在这里,我们将SizeBalancedTree视作一个二叉搜索树,因此左子树的值都小于当前节点,右子树的值都大于当前节点。 在插入和删除节点的时候,我们需要维护平衡和子树大小,因此需要编写maintain和pushup方法。maintain方法用于在插入或删除节点后,对树进行平衡调整。如果左子树的大小大于右子树的大小的三倍,就进行右旋转;如果右子树的大小大于左子树的大小的三倍,就进行左旋转。pushup方法用于更新节点的子树大小。 在插入节点的时候,我们首先查找要插入的位置,如果该节点已经存在,则只需要增加它的数量即可。如果要插入的节点不存在,则将其插入到相应的位置,并对树进行平衡调整。 在删除节点的时候,我们同样需要查找要删除的节点,如果该节点数量大于1,则只需要减少其数量即可。如果要删除的节点数量为1,则需要考虑该节点的子树情况。如果节点没有子树,则直接删除即可;如果节点只有一个子树,则将子树接到该节点的父节点上;如果节点有两个子树,则选择左子树的大小大于右子树的大小,则在左子树中找到最大值,将其替换为当前节点,并从左子树中删除该最大值。 在查询排名和寻找第k小的节点时,我们需要使用getRank和getKth方法。getRank方法首先查找要查询的节点,如果该节点存在,则返回左子树的大小加上1,否则继续向下查找。getKth方法则根据当前节点的左子树大小和数量,以及右子树的大小,来判断第k小的节点在哪个子树中,然后递归到相应的子树中查找。 最后,我们在main方法中进行测试,插入一些节点并查询排名和第k小的节点。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值