【问题标题】:Simple loop with good performance in Haskell在 Haskell 中具有良好性能的简单循环
【发布时间】:2016-06-08 14:39:23
【问题描述】:

我从 Haskell 开始,对如何为我通常用 C 或 Python 编写的简单代码获得匹配的性能感兴趣。考虑以下问题。

给你一个长度为 n 的长字符串 1 和 0。我们想要为每个长度为m 的子字符串输出该窗口中 1 的数量。也就是说,输出在0m 之间具有n-m+1 不同的可能值。

在 C 中,这很简单,时间与n 成正比,并使用与m 位成正比的额外空间(在存储输入所需的空间之上)。您只需计算长度为m 的第一个窗口中 1 的数量,然后维护两个指针,一个指向窗口的开头,一个指向结尾,并根据一个指向 1 和其他点来递增或递减为 0 或发生相反的情况。

是否有可能在 Haskell 中以纯函数的方式获得相同的理论性能?

一些糟糕的代码:

chunkBits m = helper
  where helper [] = []
        helper xs = sum (take m xs) : helper (drop m xs)

main = print $ chunkBits 5 [0,1,1,0,1,0,0,1,0,1,0,1,1,1,0,0,0,1]

【问题讨论】:

  • 请注意 Haskell 使用惰性编程。在某些情况下,例如,如果您只对前五个数字感兴趣,则时间复杂度可能为 O(1)。
  • @WillemVanOnsem 在这种情况下,您需要读取 m+4 位,不是吗?
  • @eleanora:考虑到您的窗户面向未来,是的。如果它回顾过去,它只需要前 4 个(或 m,如果 m 更小)。
  • 好吧,这不是一件坏事,但您应该从自己的一些实现开始,并演示它们如何提供所需的性能。
  • @MattJordan m 可能不依赖于 int 字符串的长度,但如果算法不包含固定的m 作为其定义的一部分。

标签: algorithm performance pointers haskell time-complexity


【解决方案1】:

C 代码

这是您描述的 C 代码:

int sliding_window(const char * const str, const int n, const int m, int * result){
  const char * back  = str;
  const char * front = str + m;
  int sum = 0;
  int i;

  for(i = 0; i < m; ++i){
     sum += str[i] == '1';
  }

  *result++ = sum;

  for(; i < n; ++i){
    sum += *front++ == '1';
    sum -= *back++  == '1';
    *result++ = sum;
  }
  return n - m + 1;
}

算法

上面的代码显然是O(n),因为我们有n 迭代。但让我们退后一步,看看底层算法:

  1. 对第一个 m 元素求和。将此保留为sumO(m)
  2. 我们的第一个窗口有sum 1s。 O(1)
  3. 直到我们用完原始字符串:O(n)
    1. “滑动”窗口。 O(1)
      • 如果我们通过滑动获得'1',则在sum 上加1O(1)
      • 如果我们通过滑动丢失'1',则从sum 中减去1O(1)
    2. sum 推送到结果上。 O(1)

由于n &gt; m(否则没有窗口),O(n)成立。

塑造一个 Haskell 变体

这基本上是一个左扫描 (scanl),可以在 (2.1.) 中获取这些差异的列表。所以我们需要的只是一种以某种方式滑动的方法:

slide :: Int -> [Char] -> [Int]
slide m xs = zipWith f xs (drop m xs)
  where
    f '1' '0' = -1  -- we lose a one
    f '0' '1' =  1  -- we gain a one
    f  _   _  =  0  -- nothing :/

这是 O(n),其中 n 是我们列表的长度。

slidingWindow :: Int -> [Char] -> [Int]
slidingWindow m xs = scanl (+) start (slide m xs)
 where
   start = length (filter (== '1') (take m xs))

这是 O(n),与 C 中相同,因为两者使用相同的算法。

注意事项

在现实生活中的应用程序中,您将始终使用TextByteString 而不是String,因为后者是Char 的列表,开销很大。由于只使用'1''0'的字符串,所以可以使用ByteString

import           Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as BS
import           Data.List (scanl')

slide :: Int -> ByteString -> [Int]
slide m xs = BS.zipWith f xs (BS.drop m xs)
  where
    f '1' '0' = -1
    f '0' '1' =  1
    f  _   _  =  0

slidingWindow :: Int -> ByteString -> [Int]
slidingWindow m xs = scanl' (+) start (slide m xs)
 where
   start = BS.count '1' (BS.take m xs)

【讨论】:

  • 这看起来真不错。谢谢。
  • @dfeuer 你的意思是代替ByteStringText?首先,OP 是“从 Haskel 开始”,我不想引入额外的包。其次,您可以将zipWithdropByteString.(Lazy.)Char8 的变体进行交换,这样就完成了,所以代码几乎 保持不变。最后但并非最不重要的一点是,这个问题没有详细说明。结果应该被打印出来,还是应该是一个列表?无论哪种方式,我都会添加一些关于 String 的内容。
  • @Zeta,不,我的意思是。为什么不使用Bool,或data Bit = Zero | One 之类的?
  • @dfeuer: “给你一个长度为 n 的 1 和 0 的长字符串”。但是,是的,Input -&gt; BoolString 后跟 slidingWindow :: Int -&gt; BoolString -&gt; … 会更好。
  • 对于 C 程序员来说,“字符串”只是一个 8 位数组。 OP 似乎是一位经验丰富的 C 程序员。
【解决方案2】:

更新

在仔细阅读问题后,我注意到 C 程序从数组中读取其输入。

所以这里有一个等效的 Haskell “纯”函数来执行任务。

 import qualified Data.Vector as V
 import Data.List
 import Control.Monad

 count :: Int -> V.Vector Int -> [Int]
 count m v = 
   let c0 = V.sum (V.take m v)
       n = V.length v
       results = scanl' go c0 [0..n-m-1]
         where go r i = r - (v V.! i) + (v V.! (i+m))
   in results

 test1 = let v = V.fromList [0,0,1,1,1,1,1,0,0,0,0]
         in print $ count 3 v

即使count 返回一个列表,它也会延迟生成。此外,如果它被另一个列表操作消耗,它可以通过各种融合技术之一进行优化。

原答案

这是一个很好的练习,但为什么它必须是“纯粹的功能性”(这意味着什么)?

您可以在 Haskell 中编写 C 算法 - 它不是那么简洁,但它会 生成基本相同的代码。

 import Data.Vector.Unboxed.Mutable as V

 count m = do
   v <- V.replicate m '0'
   let toInt ch = if ch == '1' then 1 else 0
   let loop c i = do
         ch <- getChar
         oldch <- V.read v i
         let c' = c + toInt ch - toInt oldch
         V.write v i ch
         let i' = mod (i+1) m 
         putStrLn $ show c
         loop c' i'
   loop 0 0

 main = count 3

(为简单起见,这会生成 n 个结果。)

如果您对此进行了基准测试,请注意您还包括 getCharputStrLnshow,所以可能很难做到公平 与 C 程序比较。但是,它具有 O(n) 复杂度和常数 我认为您要求的内存使用情况。

【讨论】:

  • 谢谢。 en.wikipedia.org/wiki/Purely_functional 有一个可能有用的定义,但我的意思是只使用纯函数 en.wikipedia.org/wiki/Pure_function
  • 好吧——那篇文章说 Haskell 是一种纯粹的函数式语言。那么这是否意味着任何 Haskell 函数都是纯函数式的?
  • 我觉得我不是合适的专家来回答这个问题。但是,我可以说,当我问这个问题时,我想到了不变性。
  • 关于问题的问题:C 程序似乎从数组中读取输入 - 对吗? “输出”是什么意思?结果是放在另一个数组中还是发送到stdout 或...?
  • 我很高兴输入以任何方便的形式出现,并且输出直接发送到标准输出。
【解决方案3】:

最基本的级别是使用手写递归函数重新实现酷炫的基于 HOF 的算法来表达循环。

Banged 模式将参数标记为严格,因此可以在没有不必要的延迟的情况下计算简单的值(例如,在使用 scanl' 时会隐式处理这一点)。这也表明“指针”只是名称:

{-# LANGUAGE BangPatterns #-}

-- assumes xs has only 0s and 1s
counts :: Int -> [Int] -> [Int]
counts m xs = g 0 m xs
  where
    g !c    0      ys  = h c ys xs
    g !c    _      []  = []                  -- m > |xs|
    g !c    m   (y:ys) = g (c+y) (m-1) ys
    h !c    []     _   = [c]
    h !c (y:ys) (x:xs) = c : h (c+y-x) ys xs

测试,

 > counts [1,1,0,0,1,1,0,1] 2
[2,1,0,1,2,1,1]
 > counts [1,1,0,0,1,1,1,1] 3
[2,1,1,2,3,3]

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-10-18
    • 1970-01-01
    • 2013-08-19
    • 1970-01-01
    • 2022-01-12
    • 2011-12-19
    • 1970-01-01
    相关资源
    最近更新 更多