1.前言:
Fusion Tree,中文译作融合树,是一种亚log的数据结构,与1993年由Michael L.Fredman和Dan E.Willard提出。
用途:
O(logn/logw+logw)时间复杂度支持插入,删除,前驱,后继,min,max,以及用于整数排序。
信息论断言对
n个数的排序在最坏情况下需要
nlogn次比较,不过对这个界我们还需要一些研究。
有人证明了任意unit cost RAM算法,其中只包含加法,减法,乘法,和0比较(但是不包含除法和位运算)最坏情况下需要
Ω(nlogn)的时间去对
n个数排序。
如果允许使用除法和位运算,他们有一个线性时间复杂度的算法,但是这个算法对unit cost滥用。
这里我们规定我们使用的计算模型的字长是w,每个输入的数都是在
[0,2w−1]中的整数。
2.一些记号:
对于一个集合
S和一个整数
x,定义
rank(S,x)为S集合中
≤x的元素个数。
对于一些非负整数
a,定义
bin(a1,...,an)=2ai+...+2an。
对于两个非负整数
a,b,定义
msb(u,v)为
u和
v最高的不相同的位。
3.概述:
Fusion Tree大概可以看做是一棵特殊的B-Tree,特性:
-
叉数
B=O(w1/5)
-
在一次搜索时,每个在搜索路径上的节点的正确的儿子可以被
O(1)确定
从这些特性我们可以看出Fusion Tree单次操作的时间复杂度是
O(logw(n)+logw)=O(logn/logw+logw)的,比
O(logn)低。
但是由于其实现方式,Fusion Tree每次对内部节点的更新复杂度是
O(B4)的。
为了控制Fusion Tree均摊的更新复杂度,我们将这棵B-Tree的每对叶子节点之间的部分替换为一个大小大约为
O(B4)的Weight Balanced Tree,只在WBT根节点发生变化的时候更新Fusion Tree的内部节点。
具体来说,我们B-Tree维护的是一个排序后的数组的分块,其中每个块都由一棵平衡二叉搜索树维护,fusion tree上只维护一个值来表示块边界,用途是指引每次插入,删除,查询在哪个块中。
可以发现这样我们把内部节点的变化次数除掉了一个
B4。
4.压缩key的表示:
如何
O(1)确定搜索路径上的一个节点的正确的儿子:
考虑一个B-Tree的节点,其上面包含了
k个key,其中
B/2≤k≤B,记作
S=u1,u2,...uk。
然后我们定义出
B(S)表示"有区别的位的位置",用人话来说就是我们把这
k个key的trie建出来,然后所有有超过
1个儿子的节点的高度构成的集合
(当然这里我们不用把trie建出来,只是这么解释比较直观,而且更能反映出其的一些性质)。
再定义一个集合
K(S),为
S只保留
B(S)中那些位之后的值,记作
K(S)=u1′,u2′,...uk′,发现这个压缩操作对于原集合是保序的。
对于一个任意的
w−bit的数
u,我们记
u′(S)表示
u只保留
B(S)中那些位,即把非
B(S)中的位都置为
0之后的值。
下面引理表达了一个压缩key的重要性质:
引理1:
设
B(S)排序后为
c1<c2<...<cr,定义边界
c0=−1,cr+1=b。
定义
ui′为
K(S)中任意的一个压缩后的key。
对于一个任意的
w−bit的数
u,满足
u=ui,
设
msb(u′(S),ui′)=cm,即
u和
ui在bit位置
cm+1,...,cr位置处相等,但是在
cm处不相等,如果
u′(S)=ui′,则我们记
m=0。
如果
u和
ui不同的最高位
p满足
p>cm,那么我们可以通过:
-
唯一的一个区间
[cj−1,cj]满足
p属于这个区间
-
来确定
rank(S,u)的值。
证明平凡,把trie画出来,显然可以画成一个平面图,然后可以发现这两个可以唯一地确定出一个平面区域,这个区域中的
S集合元素个数就是
rank(S,u)(感觉这种东西光写一堆自然语言也不能说明正确性,需要形式化证明一下?)。
注意到这个引理虽然是对任意
ui成立的,但是要求
u和
ui不相同的最高位不是
B(S)中的一个点,可以发现这个
ui其实必须在
u"脱离"这个trie的位置,也就是
p的父亲子树中。
引理
1使得我们可以将
rank(S,u)的计算规模降低到
rank(K(S),u′(S)),通过计算
rank(K(S),u′(S)),我们可以确定
u′(S)在
K(S)中的前驱后继
uj′和
uj+1′(这两个值不一定存在,但经过平凡的讨论就可以解决。
如果
uj≤u≤uj+1,那我们已经解决了这个问题
否则我们令
i=j或者
i=j+1,计算出
msb(ui,u)=p,然后只要我们知道了包含
p的区间
[cj,cj+1],我们就可以通过引理
1来确定出
rank(S,u)的值。
这里如果我们
uj≤u≤uj+1,那我们已经达成了目的,不用继续考虑了。
否则如果不满足
uj≤u≤uj+1,也就是说我们在这个sketch的过程中丢失了信息,即说明保留
K(S)这些位的信息是不够的,那么
p一定不在
K(S)中,也就是说
i=j和
i=j+1中
p较小的
i满足
p>cm,故可以使用引理
1。
计算
K(S)和
u′(S):
我们发现没有平凡的方法可以将一个
w−bit的数
u在
O(1)时间把
B(S)那些位提取出来之后放到连续的一段中(可能可以通过硬件支持实现?),即使经过了一定预处理。
其实我们不需要做到这个,可以用具有:
-
将需要提取出的位提取出,并放到(可以不连续)的更短的一段中
-
保序性
的其他变化来实现我们需要的效果。
我们可以通过一次恰当的乘法和一次与运算来实现这个:
沿用引理
1的定义,设我们需要从
u中提取这些位,令
C=bin(c1,...,cr)。
假设我们已经算出了
C,我们先通过令
v=uANDC来将
u中不需要的那些位置
0。
然后我们将
v乘以一个量
M,从而把
v中我们需要的那些
bit转化到一个狭窄的范围内,然后再通过一次
AND来清除掉不需要的位置
这里给出对一个量
M的存在性证明和构造:
记
M=bin(m1,...,mr),如果我们暂时忽略交叉和进位造成的影响,那么可以认为
v乘
M是把
c1,...cr这些位置的位重新定位到了。
c1+m1,...,cr+mr上。
如果对任意
1≤i,j≤r,这
r2个
ci+mj都是不同的,那么就不会发生交叉和进位了。
我们现在的目标是构造一个整数集合
m1,...,mr,使得:
-
c1+m1<c2+m2<...<cr+mr
-
对任意
1≤i,j≤r,
ci+mj都是两两不同的。
-
变换后的区间
[c1+m1,cr+mr]"相对较小",这里的相对较小其实只要是
O(poly(r))的即可,因为这样我们可以通过调整树的叉数来满足后续的条件。
引理2:
给一个
r个整数的序列,
c1<...<cr,存在一个
r个整数的序列,
m1,...mr,满足:
-
c1+m1<c2+m2<...<cr+mr
-
对任意
1≤i,j≤r,
ci+mj都是两两不同的。
-
(cr+mr)−(c1+m1)≤r4
证明:
先考虑证明存在整数序列
m1′,...,mr′,使得对任意
i,j,a,b,
mi′+ca与
mj′+cb在模
r3的意义下不同余。
如果我们找到了这样的整数序列,那么所有
r2个
ci+mj′都是两两不同的,并且由于这个是在模
r3意义下两两不同的,所以我们可以对第
i个
ci+mi′加上
(i−1)∗r3,这样就可以保证对所有
i满足
ci+mi′<ci+1+mi+1′了。
关于
m1′,...,mr′的存在性:
使用数学归纳法来证明,显然我们可以找到
m1′,这个平凡。
假设结论对
t成立,即我们已经找到了
m1′,...,mt′,满足对任意
1≤i,j≤t,
a,b,有
mi′+ca与
mj′+cb在模
r3的意义下不同余。
可以观察到
mt+1′+ci≡ms′+cj(modr3),即
mt+1′≡ms′+cj−ci(modr3)。
我们可以令
mt+1′是
[0,r3)中最小的和所有
ms′+cj−ci不同余的数,这里
1≤s≤t,1≤i,j≤r。
由鸽巢原理,由于
t∗r2<r3,所以我们一定可以找到
mt+1′。
故当
t+1≤s时,结论对
t+1成立
由数学归纳法知结论对
s成立,同时我们这里给出了一个暴力的
O(r4)的构造算法(
r轮,每轮最坏枚举
O(r3)个位置)。
5.融合:
融合树的"融合"即指将每个节点上的key放到同一个
w−bit的word上,通过对这个word进行运算来一起处理这些key。
沿用之前
ui和
B(S)={ci}的记号:
我们这个B-Tree的每个节点存了
C=bin(c1,...cr)和
M=bin(m1,...,mr)这两个量,用于计算
u′(S),同时还存了
D=bin(c1+m1,...,cr+mr)这个量,用于清空
u′(S)的计算中不需要的位。
同时还需要两个数组,存排序后的
ui和
ui′,和一个表
f[i][j][2]表示引理
1中,如果知道了
ui和
j,还有
u和
ui的大小关系,我们唯一确定的答案是多少。
回顾之前的内容,当我们算出了
j=rank(K(S),u′(S))后,如果
u不在
[uj,uj+1]的区间中,那么我们把
u′(S)XORuj′和
u′(S)XORuj+1′比较一下,较小的值所对应的
uh′,
h=j或
j+1,和
u有更长的公共前缀,即
msb更小。
令
m=msb(u,uh),然后我们需要知道
m被哪个
B(S)中的区间
[ci,ci+1]包含,所以需要进行一次
i=rank(B(S),m)的计算
还需要进行一次
u和
uh的比较,这个平凡,当这些都做完了,我们查一下表
f即可得到
rank(S,u)。
可以发现fusion tree的每个内部节点需要存下
O(B2)大小的表,内部节点个数是
O(n/B4)个,所以是
O(n)空间的。
下面给出对
-
rank(K(S),u′(S))
-
rank(B(S),m),其中
m是在
[0,w]中的整数
-
两个
w−bit的整数
u,v,
msb(u,v)
的计算方法:
O(1)计算rank(K(S),u′(S)):
我们把每个
K(S)中的元素前面补一个
1,然后从小到大拼到一起来,这个拼起来的操作就是所谓的"融合"。
由于我们
K(S)中有
k个元素,每个元素有
r4位,所以这里总共用了
k(r4+1)位,由于
B/2≤k≤B,所以我们总的位数是
O(B5)的,由于
B=O(w1/5),所以总的位数是
O(w)的。
所以我们拼起来的这个东西是
O(1)个word的,这里将其定义为
A。
令
C=i=0∑B2(r4+1)i
通过
u′(S)×C,可以将
u′(S)前面补一个
0之后复制
B遍,然后拼到一起
通过
A−u′(S)×C,可以发现对每个
A中补
1的位置,其对应的那个
ui(S)如果
<u′(S),则这个
1变成
0,否则
1不变
所以我们通过
(A−u′(S)×C)&C,然后对这个word数
1的个数即可知道
rank(K(S),u′(S))。
由于这个word只在
2(r4+1)i这样的位置有
1,我们可以通过一次对
2r4+1−1的取模来得到其中
1的个数,虽然对常数取模可以用乘法和位运算
O(1)实现,但我们这里可以给出一个更合适的构造。
我们可以通过将其乘
C&(2(r4+1)k−1),这样相当于把其叠加了
k次之后加起来,可以发现其中有一个长为
r4+1的段,这段的二进制表示的值等于这个word在
2(r4+1)i这些位置的元素的和。
通过位移和
AND我们可以取出这个长
r4+1的段,于是就完成了。
答案即
((((A−u′(S)×C)&C)×(C&(2(r4+1)k−1)))&C)>>((k(r4+1)−1)
O(1)计算rank(B(S),m),m是在[0,w]中的整数:
由于我们可以
O(1)计算
rank(K(S),u′(S)),所以把这个查出来然后判断那一个数的大小,并且进行一次查表即可。
O(1)计算msb(u,v):
等价于求
uXORv的最高位
1的位置,设
A=uXORv。
我们将
A分为
rc大小的块,总共
r块,这里
c是一个常数,
c>1
令
C=(100...0100...0......)2,这里每两个
1之间有
r−1个
1,
C是一个常数。
注意到:
((100...0)2−0)&(1<<(rc)−1)=(1<<(rc)−1)
((100...0)2−y)&(1<<(rc)−1)=0,这里
y>0
我们用
A&∼C可以去掉每一块的首位。
然后用
C−(A&∼C)可以使得每一块中除首位外如果有
1,则其在该块首位为
0,否则为
1。
然后用
(C−(A&∼C))&C去掉了
C−(A&∼C)中每一块中除首位外的部分。
然后用
(C−((C−(A&∼C))&C))可以得到:如果一块中除首位外有
1,则块首位为
1,否则为
0,且块首位外所有位置都是
0的一个数
再考虑对每个块只保留首位,块内是否有
1。
最后
(A&C)∣(C−((C−(A&∼C))&C))可以得到:如果一块中有
1,则块首位为
1,否则为
0,且块首位外所有位置都是
0的一个数。
令
D=k=0∑r−12k(rc−1),
通过
(((A&C)∣(C−((C−(A&∼C))&C)))×D)>>(w−r)可以将每块首位的数字拼到一个长
r的二进制数中。
然后我们可以使用前面的
O(1)计算
rank的方法,令
B′(S)=2i,
i在
[0,r−1]间,是整数。
通过
rank(B′(S),(((A&C)∣(C−((C−(A&∼C))&C)))×D)>>(w−r))就可以得到这个长
r的二进制数中第一个非0的首位的位置了。
我们知道了第一个非
0位在哪个块中,然后查这个块里面第一个非
0位的位置就可以了。
由于我们每个块是
rc的大小,所以对一个大小为
rc,包含了
2i的集合用一次rank即找到了块内第一个非
0的首位位置。
取
c=4,r=w1/5,
rc=w4/5,我们便
O(1)查询,
O(w4/5)预处理时间复杂度解决了这个问题,由于预处理次数是
O(n/B4),所以这里也是线性的。
综上所述,我们得到了一个单次操作复杂度
O(logn/logw+logw)的数据结构,这里
据说可以通过一些优化做到
O(logn/logw),但在这里由于我还没看所以暂时不做介绍。
6.一些拓展
如果我们允许下列中的一个:
-
放松线性空间的限制
-
保留线性空间的限制,但是使用随机化和整数除法
那么我们可以得到一个
O(logn)的动态搜索的时间复杂度上界。
当
n超过
2(logw)2/36时(这里
1/36的常数是论文中给出的,由于我的部分细节和论文中不同,可能是不同的常数),
对于1的case,可以通过使用vEB树来实现,对于2的case,可以通过使用Y-fast trie实现。
对于这样的
n,这两个数据结构可以在
O(loglogU)=O(logw)=O(logn)的时间完成一次搜索操作。
对于较小的
n,我们使用fusion tree,通过调节
B=Θ(2logn)。
在这个
B下,我们的时间复杂度是
O(logn/logB+logB)=O(logn)。
综上所述,如果引入随机化和整数除法,可以
O(nlogn)时间,线性空间整数排序。
7.总结
由信息论可以证明基于比较的排序下界是
Ω(nlogn)的,但整数排序其实是非常复杂的一个问题,还有待研究。