algorithm - 在 Haskell 中具有良好性能的简单循环

标签 algorithm performance pointers haskell time-complexity

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

给你一长串 1 和 0 的长度 n。对于长度为 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]

最佳答案

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 1。 O(1)
  3. 直到我们用完原始字符串:O(n)
    1. “滑动”窗口。 O(1)
      • 如果我们通过滑动 O(1) 获得 '1',则将 1 添加到 sum
      • 如果我们通过滑动 O(1) 丢失了一个 '1',则从 sum 中减去 1
    2. sum 推到结果上。 O(1)

因为 n > 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)

关于algorithm - 在 Haskell 中具有良好性能的简单循环,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37705664/

相关文章:

c++ - 为什么 list::push_back 在 VC++ 中比在 g++ 中慢得多?

java - 原始内存访问 Java/Python

algorithm - 找到所有最长递增子序列的最优化算法是什么?

c++ - 最大化平面值中的点

php - USSD(状态机)应用算法

python - 多维Newton Raphson的同时优化/时间复杂度

performance - 我可以让 MassTransit 重用消费者实例,而不是为每条消息创建一个新实例吗?

c - 卡在 C 指针结构中

c - 为什么一个困惑的字符会打印(C)?

c# - 用颜色填充点之间空间的算法