【问题标题】:How is insert O(log(n)) in Data.Set?如何在 Data.Set 中插入 O(log(n))?
【发布时间】:2012-12-19 09:55:52
【问题描述】:

查看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))?

【问题讨论】:

  • 它不需要复制整个树——它只需要将节点从根复制到插入节点的位置。在平衡二叉树中会有 log(n) 个。
  • 平衡功能,供参考,在这里:hackage.haskell.org/packages/archive/containers/latest/doc/html/… - 注意上面的文档列出了一个更简单的版本。
  • (:) 根据定义是 O(1) --- 尽管名字很有趣,但它只是一个简单的构造函数。

标签: haskell complexity-theory referential-transparency


【解决方案1】:

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

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

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

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

当您声称这是编译器优化而不是语言级别时,您完全错了。此行为直接遵循列表数据类型的语言级别定义。

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

递归地应用这个推理应该告诉你实际上根本不需要复制数据元素;在向下插入元素的最终位置的路径上只需要新的父节点。每个新节点存储 3 样东西:一个项目(直接与原始树中的项目引用共享)、一个未更改的子树(直接与原始树共享)和一个新创建的子树(与原始树共享几乎所有的结构树)。在平衡树中会有 O(log(n)) 个。

【讨论】:

    【解决方案2】:

    无需制作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))

    【讨论】:

    • 并且插入路径的长度为 O(log N) -- 因此创建了 O(log N) 个新节点。
    • 有关自平衡二叉搜索树如何在 O(log n) 中进行插入和重新平衡的更详细分析,可以查看红黑树文章 (@987654322 @)。除了红黑树之外,还有其他自平衡二叉搜索树,但思路类似。
    猜你喜欢
    • 2011-12-12
    • 2021-12-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多