如何在Data.Set中插入O(log(n))?

时间:2013-01-04 22:23:57

标签: haskell complexity-theory referential-transparency

在浏览Data.Set的文档时,我看到了insertion of an element into the tree is mentioned to be O(log(n))。但是,我会直观地期望它是O(n * log(n))(或者可能是O(n)?),因为参照透明度需要在O(n)中创建前一个树的完整副本。

我理解,例如(:)可以是O(1)而不是O(n),因为这里不必复制完整列表;新的列表可以由编译器优化为第一个元素加上指向旧列表的指针(请注意,这是一个编译器 - 而不是语言级别 - 优化)。但是,将值插入Data.Set涉及重新平衡,这对我来说非常复杂,我怀疑有类似于列表优化的东西。我试过阅读the paper that is referenced by the Set docs,但无法回答我的问题。

那么:如何在一个(纯粹的)函数式语言中将元素插入到二叉树中是O(log(n))?

2 个答案:

答案 0 :(得分:16)

为了在其中插入元素,无需制作Set的完整副本。在内部,元素存储在树中,这意味着您只需要沿插入路径创建新节点。可以在Set的插入前和插入后版本之间共享未触及的节点。正如Deitrich Epp指出的那样,在平衡树中O(log(n))是插入路径的长度。 (很抱歉忽略这个重要的事实。)

假设您的Tree类型如下所示:

data Tree a = Node a (Tree a) (Tree a)
            | Leaf

...并说你有Tree看起来像这样

let t = Node 10 tl (Node 15 Leaf tr')

...其中tltr'是一些命名的子树。现在假设您要将12插入此树中。嗯,这看起来像这样:

let t' = Node 10 tl (Node 15 (Node 12 Leaf Leaf) tr')

子树tltr'tt'之间共享,您只需要构建3个新的Nodes来执行此操作,即使t的大小可能远大于3。


编辑:重新平衡

关于重新平衡,请考虑这样,并注意我在这里没有严格要求。假设你有一棵空树。已经平衡了!现在说你插入一个元素。已经平衡了!现在说你插入另一个元素。嗯,有一个奇数,所以你不能做那么多。

这是棘手的部分。假设您插入另一个元素。这可以有两种方式:左或右;平衡或不平衡。如果它不平衡,您可以清楚地执行树的旋转以平衡它。如果它是平衡的,已经平衡了!

这里需要注意的重要一点是,经常重新平衡。它不像你有一堆树,决定插入一个元素,但在你这样做之前,你重新平衡,然后在你完成插入后留下一团糟。

现在说你继续插入元素。树的变得不平衡,但不是很多。当确实发生这种情况时,首先你要立即纠正它,其次,沿着插入路径进行校正,在平衡树中O(log(n))。您链接到的纸张中的旋转最多触摸树中的三个节点以执行旋转。所以你在重新平衡时正在做O(3 * log(n))工作。那还是O(log(n))

答案 1 :(得分:7)

为了更加强调dave4420在评论中所说的内容,使(:)在恒定时间内运行不涉及编译器优化。您可以实现自己的列表数据类型,并在一个简单的非优化Haskell解释器中运行它,它仍然是O(1)。

列表被定义为一个初始元素加一个列表(或者它在基本情况下为空)。这是一个等同于本机列表的定义:

data List a = Nil | Cons a (List a)

因此,如果你有一个元素和一个列表,并且你想用Cons构建一个新的列表,那就是直接从构造函数所需的参数创建一个新的数据结构。甚至不需要检查尾部列表(更不用说复制它),而不是在执行Person "Fred"之类的操作时检查或复制字符串。

当你声称这是编译器优化而不是语言级优化时,你就错了。此行为直接来自列表数据类型的语言级别定义。

类似地,对于定义为项目加上两棵树(或空树)的树,当您将项目插入非空树时,它必须位于左侧或右侧子树中。您需要构造包含该元素的该树的新版本,这意味着您需要构造一个包含新子树的新父节点。但其他子树根本不需要遍历;它可以按原样放入新的父树中。在平衡树中,这是可以共享的树的完整一半

递归地应用这种推理应该表明实际上根本不需要复制数据元素;在路径上只需要新的父节点,直到插入元素的最终位置。每个新节点存储3件事:一个项目(直接与原始树中的项目引用共享),一个未更改的子树(直接与原始树共享),以及一个新创建的子树(几乎所有子结构都与原始子树共享)树)。在平衡树中会有O(log(n))。