自举手指树结构


16

在使用2-3个手指树工作了相当多的时间后,我对它们在大多数操作中的速度印象深刻。但是,我遇到的一个问题是与大型手指树的初始创建相关的大量开销。因为构建被定义为一系列串联操作,所以最终构建了大量不需要的手指树结构。

由于2-3个手指树的复杂性质,我看不到用于引导它们的直观方法,而且我所有的搜索都为空。所以问题是,您如何才能以最少的开销引导2-3根手指的树呢?

明确地说:给定已知长度的序列,可以用最少的操作生成的手指树表示。小号ñ小号

天真的方法是连续调用cons操作(在文献中为' '运算符)。然而,这将创造代表的所有切片不同手指的树结构为。ñ小号[1 ..一世]


1
手指树: Hinze和Paterson 的简单通用数据结构是否提供答案?
戴夫·克拉克

@Dave我实际上是在他们的论文之外实现的,他们没有解决有效的创建问题。
jbondeson 2011年

我想了很多。
戴夫·克拉克

在这种情况下,您能否更具体地说明“构建”的含义?这是一个文件夹吗?
jbapple 2011年

@jbapple-我进行了更明确的编辑,对于造成的混乱,我们深表歉意。
jbondeson 2011年

Answers:


16

GHC的Data.Sequencereplicate功能 建立在fingertree 的时间和空间,但是这是由明知去从一开始走的手指树的右侧脊椎的元素启用。该库是由原始论文的作者在2-3个手指树上编写的。Ølgñ

如果要通过重复级联来构建手指树,则可以通过更改刺的表示来减少构建时的瞬时空间使用。2-3棵手指树上的刺被巧妙地存储为同步的单链列表。相反,如果将刺存储为双端队列,则在合并树时可以节省空间。这个想法是,通过重用树的棘刺,将两棵相同高度的树连接起来会占用空间。如最初所述,当串联2-3棵手指树时,新树内部的棘刺将不能再原样使用。Ø1个

卡普兰(Kaplan)和塔里安(Tarjan)的“可链接排序列表的纯函数表示”描述了一种更为复杂的手指树结构。本文(第4节)还讨论了与我上面提出的双端队列建议类似的构造。我相信他们描述的结构可以在时空中连接两棵等高的树。对于建造手指树,这是否为您节省了足够的空间?Ø1个

注意:他们使用“ bootstrapping”一词的意思与您上面的用法有些不同。它们意味着使用同一结构的简单版本存储数据结构的一部分。


一个非常有趣的想法。我将不得不对此进行研究,看看对整体数据结构的权衡是什么。
jbondeson 2011年

我的意思是在这个答案中有两个想法:(1)复制想法(2)Faster可以连接几乎相等大小的树。我认为,如果输入是数组,则复制思想可以在很少的额外空间中构建手指树。
jbapple

是的,我都看过。抱歉,我都没有对此发表评论。我首先研究复制代码-尽管我肯定会尽我所能扩展我的Haskell知识。乍一看,只要您具有快速的随机访问权限,它看起来就可以解决我遇到的大多数问题。在没有随机访问的情况下,快速连接可能是更通用的解决方案。
jbondeson 2011年

10

嘲笑jbapple关于的出色答案replicate,但改为使用replicateAreplicate基于)构建,我想到了以下内容:

--Unlike fromList, one needs the length explicitly. 
myFromList :: Int -> [b] -> Seq b
myFromList l xs = flip evalState xs $ Seq.replicateA l go
    where go = do
           (y:ys) <- get
            put ys
            return y

myFromList(在效率稍高的版本中)(已在内部定义)并在内部Data.Sequence用于构建作为排序结果的手指树。

通常,直觉replicateA很简单。replicateAapplicativeTree函数的顶部构建。applicativeTree的大小的树m,并生成包含一个均衡树n的这个副本。对于箱子n多达8(单个Deep手指)被硬编码。在此之上的任何东西,它都会递归调用。“应用”元素只是简单地它通过诸如状态(例如,在上述代码的情况下)之类的线程效果交错树的构造。

go复制的函数只是一个操作,该操作获取当前状态,将元素弹出顶部,然后替换其余部分。因此,在每次调用时,它会进一步降低作为输入提供的列表。

一些更具体的笔记

main = print (length (show (Seq.fromList [1..10000000::Int])))

在一些简单的测试中,这产生了有趣的性能折衷。myFromList的上述主要功能比的低了近1/3 fromList。另一方面,myFromList使用了2MB的恒定堆,而标准fromList使用了926MB。926MB是由于需要立即将整个列表保存在内存中而产生的。同时,该解决方案myFromList能够以惰性流方式消耗结构。速度问题是由于myFromList必须执行大约两倍的分配(由于状态monad的成对构造/销毁)导致的,fromList。我们可以通过转移到CPS转换后的状态monad来消除这些分配,但这会导致在任何给定时间保留更多的内存,因为懒惰的损失要求以非流方式遍历列表。

另一方面,如果我myFromList不想强迫整个演出进行表演,而是移至仅提取head或last元素,立即带来更大的胜利-提取head元素几乎是即时的,而提取last元素为0.8s 。同时,使用standard fromList,提取head或last元素花费约2.3秒。

这是所有细节,是纯洁和懒惰的结果。在发生突变和随机访问的情况下,我想该replicate解决方案绝对更好。

但是,这确实提出了一个问题,即是否存在一种重写的方法,applicativeTree这种myFromList方法严格效率更高。我认为问题在于,应用操作的执行顺序与自然遍历树的顺序不同,但是我还没有完全研究其工作方式,或者是否有解决此问题的方法。


4
(1)有趣。这看起来像是执行此任务正确方法。令我惊讶的是,这比fromList强制执行整个序列要慢。(2)也许对于cstheory.stackexchange.com,此答案过于繁琐且与语言有关。如果您可以添加一个解释说明如何replicateA以独立于语言的方式工作,那将是很棒的。
伊藤刚(Tsuyoshi Ito)

9

当您使用大量的中间手指树结构结束时,它们会彼此共享绝大部分结构。最后,您分配的内存最多是理想情况下的两倍,其余的将与第一个集合一起释放。它的渐近性与它们获得的效果一样好,因为您最终需要一个用n个值填充的手指树。

您可以使用Data.FingerTree.replicate和建立手指树,方法FingerTree.fmapWithPos是在充当有限序列角色的数组中查找值,或者使用traverseWithPos将它们从列表或其他已知大小的容器中剥离的方法。

Ø日志ñØñØ日志ñ

Ø日志ñreplicateAmapAccumL

TL; DR如果必须这样做,则可能会使用:

rep :: (Int -> a) -> Int -> Seq a 
rep f n = mapWithIndex (const . f) $ replicate n () 

和索引到一个固定大小的数组编号只是提供(arr !)f上方。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.