Haskell向量C ++ push_back模拟

时间:2015-07-23 21:24:16

标签: haskell vector data-structures amortized-analysis

我发现Haskell Data.Vector.*错过了C ++ std::vector::push_back的功能。有grow / unsafeGrow,但它们似乎有 O(n)复杂性。

有没有办法在元素的 O(1)摊销时间内增长向量?

1 个答案:

答案 0 :(得分:11)

Data.Vector中确实没有这样的设施。使用像Data.Vector.Mutable这样的MutableArray从头开始实现这一点并不困难(参见下面的实现),但是存在一些明显的缺点。特别是,它的所有操作最终都发生在一些状态上下文STIO内。这有缺点

  1. 任何操纵这种数据结构的代码都必须是monadic
  2. 编译器不太可能优化。例如,像vector这样的库使用一些非常聪明的名为fusion来优化中间分配。在州情境中,这种事情是不可能的。
  3. 并行性将变得更加困难:在ST中我甚至不能拥有两个线程,在IO中我会在整个地方都有竞争条件。这里令人讨厌的是,任何共享都必须在IO
  4. 中进行

    好像所有这些都不够,垃圾收集在纯代码中也表现得更好。

    那我该怎么办?

    您不需要完全这种行为 - 通常您最好使用不可变数据结构(从而避免所有上述问题)做某事类似。仅限于GHC附带的containers,一些替代方案包括:

    • 如果你几乎总是只使用push_back,也许你只想要一个堆栈(一个普通的[a])。
    • 如果您预计会比查找更多push_backData.Sequence会为您提供O(1)附加到任意一端和O(log n)查询。
    • 如果您对许多操作感兴趣,尤其是类似hashmap,Data.IntMap已经过优化。即使这些操作的理论成本为O(log n),您也需要一个非常大的IntMap来开始感受这些成本。

    制作类似C ++ vector

    的内容

    当然,如果一个人不关心最初提到的限制,就没有理由没有像C ++这样的向量。为了好玩,我继续从头开始实现这一点(需要包data-defaultprimitive)。

    这个代码可能不在某个库中的原因是它违背了Haskell的大部分精神(我这样做是为了符合C ++样式向量)。

    • 实际制作新向量的唯一操作是newVector - 其他所有内容"修改"现有的矢量。由于pushBack没有返回新的GrowVector,因此必须修改现有的length(包括其长度和/或容量),因此capacitylength具有成为"指针"。反过来,这意味着即使获得vector也是一种单一操作。
    • 虽然这不是未装箱的,但复制module GrowVector ( GrowVector, newEmpty, size, read, write, pushBack, popBack ) where import Data.Primitive.Array import Data.Primitive.MutVar import Data.Default import Control.Monad import Control.Monad.Primitive (PrimState, PrimMonad) import Prelude hiding (length, read) data GrowVector s a = GrowVector { underlying :: MutVar s (MutableArray s a) -- ^ underlying array , length :: MutVar s Int -- ^ perceived length of vector , capacity :: MutVar s Int -- ^ actual capacity } type GrowVectorIO = GrowVector (PrimState IO) -- | Make a new empty vector with the given capacity. O(n) newEmpty :: (Default a, PrimMonad m) => Int -> m (GrowVector (PrimState m) a) newEmpty cap = do arr <- newArray cap def GrowVector <$> newMutVar arr <*> newMutVar 0 <*> newMutVar cap -- | Read an element in the vector (unchecked). O(1) read :: PrimMonad m => GrowVector (PrimState m) a -> Int -> m a g `read` i = do arr <- readMutVar (underlying g); arr `readArray` i -- | Find the size of the vector. O(1) size :: PrimMonad m => GrowVector (PrimState m) a -> m Int size g = readMutVar (length g) -- | Double the vector capacity. O(n) resize :: (Default a, PrimMonad m) => GrowVector (PrimState m) a -> m () resize g = do curCap <- readMutVar (capacity g) -- read current capacity curArr <- readMutVar (underlying g) -- read current array curLen <- readMutVar (length g) -- read current length newArr <- newArray (2 * curCap) def -- allocate a new array twice as big copyMutableArray newArr 1 curArr 1 curLen -- copy the old array over underlying g `writeMutVar` newArr -- use the new array in the vector capacity g `modifyMutVar'` (*2) -- update the capacity in the vector -- | Write an element to the array (unchecked). O(1) write :: PrimMonad m => GrowVector (PrimState m) a -> Int -> a -> m () write g i x = do arr <- readMutVar (underlying g); writeArray arr i x -- | Pop an element of the vector, mutating it (unchecked). O(1) popBack :: PrimMonad m => GrowVector (PrimState m) a -> m a popBack g = do s <- size g; x <- g `read` (s - 1) length g `modifyMutVar'` (+ negate 1) pure x -- | Push an element. (Amortized) O(1) pushBack :: (Default a, PrimMonad m) => GrowVector (PrimState m) a -> a -> m () pushBack g x = do s <- readMutVar (length g) -- read current size c <- readMutVar (capacity g) -- read current capacity when (s+1 == c) (resize g) -- if need be, resize write g (s+1) x -- write to the back of the array length g `modifyMutVar'` (+1) -- increase te length s data family approach并不太难 - 这只是单调乏味的 1

    随着说:

    grow

    grow

    的当前语义

    我认为github issue在解释语义方面做得非常好:

      

    我认为预期的语义是它可以执行realloc,但不能保证,并且所有当前实现都执行更简单的复制语义,因为对于堆分配,成本应该大致相同。

    基本上,当你想要一个增加大小的新的可变向量时,你应该使用GrowVector,从旧向量的元素开始(不再关心旧向量)。这非常有用 - 例如,可以使用MVectorgrow来实现data instance

    1 方法是,对于您想要拥有的每种新类型的未装箱的矢量,您可以创建一个data family来扩展&#34;您的类型为固定数量的未装箱数组(或其他未装箱的数据)。这是data instance的要点 - 允许类型的不同实例化具有完全不同的运行时表示,并且也可以是可扩展的(如果需要,可以添加自己的import re s = '{"a":"x","b":1,"c":"{"a":"x","b":1,"c":"{"a":"x","b":1,"c":"xa"}"}"}' m = re.search(r'([^"]*"){9}(.*)"', s) print(m.group(2)) 。)