什么容器真正模仿了 Haskell 中的 std::vector?

Posted

技术标签:

【中文标题】什么容器真正模仿了 Haskell 中的 std::vector?【英文标题】:What container really mimics std::vector in Haskell? 【发布时间】:2014-07-30 05:04:00 【问题描述】:

问题

我正在寻找一个用于保存n - 1 问题的部分结果的容器,以便计算nth 问题。这意味着容器的大小最终将始终为n

容器的每个元素 i 取决于至少 2 到 4 个以前的结果。

容器必须提供:

在开头或结尾处插入恒定时间(两者之一,不一定同时) 中间的恒定时间索引

或者(给定O(n) 初始化):

恒定时间单个元素编辑 中间的恒定时间索引

什么是std::vector 以及它为什么相关

对于那些不懂 C++ 的人,std::vector 是一个动态大小的数组。它非常适合这个问题,因为它能够:

在施工时预留空间 在中间提供恒定时间索引 在末尾提供恒定时间插入(保留空间)

因此,这个问题可以在O(n) 复杂性中用 C++ 解决。

为什么Data.Vector 不是std::vector

Data.VectorData.Array 提供与std::vector 类似的功能,但并不完全相同。当然,两者都在中间提供恒定时间索引,但它们既不提供恒定时间修改(例如,(//) 至少为 O(n)),也不提供在任一端的开始处插入恒定时间。

结论

什么容器真正模仿了 Haskell 中的 std::vector?或者,我最好的镜头是什么?

【问题讨论】:

"恒定时间修改" O(1) 中的非破坏性修改...基本上,算了。如果您想进行破坏性修改,请尝试Data.Vector.Mutable 您应该阅读以下内容:haskell.org/haskellwiki/Dynamic_programming_example 以获得不需要可修改数据结构的功能解决方案。 感谢您告诉我。 我认为这应该重新打开。链接的答案并不令人满意,并且模糊地暗示了一个不存在的解决方案。 【参考方案1】:

来自reddit的建议使用Data.Vector.constructN

O(n) 通过重复将生成器函数应用于向量的已构造部分来构造具有 n 个元素的向量。

 constructN 3 f = let a = f <> ; b = f <a> ; c = f <a,b> in f <a,b,c>

例如:

λ import qualified Data.Vector as V
λ V.constructN 10 V.length
fromList [0,1,2,3,4,5,6,7,8,9]
λ V.constructN 10 $ (1+) . V.sum
fromList [1,2,4,8,16,32,64,128,256,512]
λ V.constructN 10 $ \v -> let n = V.length v in if n <= 1 then 1 else (v V.! (n - 1)) + (v V.! (n - 2))
fromList [1,1,2,3,5,8,13,21,34,55]

这似乎完全可以解决您上面描述的问题。

【讨论】:

【参考方案2】:

我首先想到的数据结构要么是来自Data.Map 的地图,要么是来自Data.Sequence 的序列。

更新

Data.Sequence

序列是持久的数据结构,它允许大多数操作高效,同时只允许有限的序列。如果您有兴趣,他们的实现基于finger-trees。但它有哪些品质呢?

O(1)计算长度 O(1) 在前面/后面插入,分别使用运算符 &lt;||&gt;O(n) 从带有fromlist 的列表创建 O(log(min(n1,n2))) 连接长度为 n1 和 n2 的序列。 O(log(min(i,n-i))) 在长度为 n 的序列中为位置 i 处的元素建立索引。

此外,此结构还支持许多已知和方便的功能,您希望从类似列表的结构中获得:replicatezipnullscans、sorttakedropsplitAt 等等。由于这些相似之处,您必须进行限定导入或隐藏Prelude 中具有相同名称的函数。

Data.Map

Maps 是实现“事物”之间对应关系的标准主力,你可能称之为 Hashmap 或其他编程语言中的关联数组在 Haskell 中称为 Maps;除了说 Python Maps 是纯的 - 所以更新会给你一个新的 Map 并且不会修改原始实例。

地图有两种形式 - strict 和 lazy。

引用文档

严格

这个模块的API对key和value都是严格的。

懒惰

这个模块的API在键上是严格的,但在值上是惰性的。

因此,您需要选择最适合您的应用程序的内容。您可以使用criterion 尝试这两个版本和基准测试。

我不想列出Data.Map的功能,而是想传递给

Data.IntMap.Strict

这可以利用键是整数的事实来挤出更好的性能 引用我们首先注意到的文档:

许多操作的最坏情况复杂度为 O(min(n,W))。这意味着该操作可以在最大为 W 的元素数量(即 Int 中的位数(32 或 64))中变为线性。

那么IntMaps的特点是什么

O(min(n,W)) 用于(不安全)索引(!),如果键/索引不存在,您将收到错误。这与Data.Sequence 的行为相同。 O(n) 计算size O(min(n,W)) 用于安全索引lookup,如果未找到密钥则返回Nothing,否则返回Just aO(min(n,W)) 对于insertdeleteadjustupdate

所以你看到这个结构比Sequences 效率低,但是如果你实际上不需要所有条目,例如稀疏图的表示,其中节点是整数,它会提供更多的安全性和很大的好处.

为了完整起见,我想提一个名为 persistent-vector 的包,它实现了 clojure 样式的向量,但似乎已被放弃,因为最后一次上传来自 (2012)。

结论

因此,对于您的用例,我强烈推荐Data.SequenceData.Vector,不幸的是我对后者没有任何经验,因此您需要自己尝试一下。据我所知,它提供了一种强大的功能,称为流融合,可以优化在一个紧密的“循环”中执行多个功能,而不是为每个功能运行一个循环。 Vector的教程可以在here找到。

【讨论】:

很遗憾我现在没有时间 - 如果有必要,我会在晚上详细说明(gmt+1) 感谢您的提醒,工作使我发挥了最大的作用,所以我没有头绪来写答案。【参考方案3】:

在寻找具有特定渐近运行时间的函数式容器时,我总是选择Edison。

请注意,在具有不可变数据结构的严格语言中,在它们之上实现可变数据结构总是会出现对数减速。隐藏在懒惰背后的有限突变能否避免这种放缓是一个悬而未决的问题。还有持久性与瞬态的问题......

Okasaki 仍然是一本很好的背景读物,但手指树或更复杂的东西(如 RRB 树)应该是“现成的”可用并解决您的问题。

【讨论】:

我之前听说过这个对数减速。它有名字吗,还是更适合谷歌搜索? @ocharles 我认为这有所有正确的参考,***没有:***.com/a/1990580/2008899【参考方案4】:

我正在寻找一个容器,用于保存 n - 1 个问题的部分结果,以便计算第 n 个问题。

容器的每个元素 i 取决于至少 2 到 4 个先前的结果。

让我们考虑一个非常小的程序。计算斐波那契数。

fib 1 = 1
fib 2 = 1 
fib n = fib (n-1) + fib (n-2)

这对于小 N 来说很好,但对于 n > 10 来说很糟糕。此时,您偶然发现了这个宝石:

fib n = fibs !! n where fibs = 1 : 1 : zipWith (+) fibs (tail fibs)

你可能会惊呼这是黑魔法(无限的、自我引用的列表构建和压缩?哇!),但这确实是一个很好的例子,可以打结,并使用惰性来确保值被计算为 -需要。

同样,我们也可以使用数组来打结。

import Data.Array

fib n = arr ! 10
  where arr :: Arr Int Int
        arr = listArray (1,n) (map fib' [1..n])
        fib' 1 = 1
        fib' 2 = 1
        fib' n = arr!(n-1) + arr!(n-2)

数组的每个元素都是一个 thunk,它使用数组的其他元素来计算它的值。这样,我们就可以构建一个单独的数组,不需要进行拼接,随意从数组中调出值,只需要支付计算到那个点的费用。

这种方法的美妙之处在于,您不仅需要看身后,还可以看前面。

【讨论】:

以上是关于什么容器真正模仿了 Haskell 中的 std::vector?的主要内容,如果未能解决你的问题,请参考以下文章

为啥极简主义,例如 Haskell 快速排序不是“真正的”快速排序?

用于 STL 容器的 std::string_view

C++ std::vector 容器 是什么

如何在 C++ 模板容器中实现 erase() 方法

模仿std::vector写线性表的几点感想

Miriam Haskell的珍珠项链