Algorithm Haskell中具有良好性能的简单循环
我从Haskell开始,对如何为我通常用C或Python编写的简单代码获得匹配的性能感兴趣。考虑下面的问题。 您将获得一个长度为Algorithm Haskell中具有良好性能的简单循环,algorithm,performance,pointers,haskell,time-complexity,Algorithm,Performance,Pointers,Haskell,Time Complexity,我从Haskell开始,对如何为我通常用C或Python编写的简单代码获得匹配的性能感兴趣。考虑下面的问题。 您将获得一个长度为n的1和0的长字符串。我们希望为长度为m的每个子字符串输出该窗口中的1s数。也就是说,输出在0和m之间有不同的可能值 在C语言中,这很容易做到与n成比例的时间,并使用与m位成比例的额外空间(在存储输入所需的空间之上)。您只需计算长度为m的第一个窗口中的1s数,然后保持两个指针,一个指向窗口的开始,一个指向窗口的结束,并根据一个指向1,另一个指向0或相反的情况递增或递减
n
的1和0的长字符串。我们希望为长度为m
的每个子字符串输出该窗口中的1s数。也就是说,输出在0
和m
之间有不同的可能值
在C语言中,这很容易做到与n
成比例的时间,并使用与m
位成比例的额外空间(在存储输入所需的空间之上)。您只需计算长度为m
的第一个窗口中的1s数,然后保持两个指针,一个指向窗口的开始,一个指向窗口的结束,并根据一个指向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程序从数组中读取其输入 这里有一个等价的Haskell“pure”函数,它执行这个任务
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
导入Data.Vector.unbox.Mutable作为V
计数m=do
v 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是列表的长度
slidingWindow :: Int -> [Char] -> [Int]
slidingWindow m xs = scanl (+) start (slide m xs)
where
start = length (filter (== '1') (take m xs))
这是O(n),与C中相同,因为两者使用相同的算法
警告
在实际应用程序中,您总是使用Text
或ByteString
而不是String
,因为后者是开销很大的Char
列表。由于您只使用字符串'1'
和'0'
,因此可以通过testring使用:
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)
最基本的一层是用手工编写的递归函数来表示循环,从而重新实现基于cool HOF的算法
碰撞模式将参数标记为严格的,因此可以计算简单的值,而不会产生不必要的延迟(例如,在使用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]
请注意,Haskell使用惰性编程。在某些情况下,例如,如果你只对前五个数字感兴趣,那么时间复杂度可能是O(1)。@WillemVanOnsem在这种情况下,你需要以m+4位来阅读,不是吗?@eleanora:考虑到你对未来的展望,是的。如果回顾过去,它只需要前4个字符(或者,如果m小于m,则需要m)。这不是一件坏事,但您应该从自己的一些实现开始,并演示它们如何不能提供所需的性能。@MattJordanm
可能不取决于int字符串的长度,但是,如果算法没有将固定的m
作为其定义的一部分,则它是输入大小的一部分(贡献O(lgm)位)。谢谢。有一个可能有用的定义,但我的意思是只使用纯函数。那篇文章说Haskell是一种纯函数语言。那么这是否意味着任何Haskell函数都是纯函数的呢?我觉得我不是给这个问题一个好答案的合适专家。然而,我可以说,当我问这个问题时,我脑子里有不变性。关于这个问题的问题:看起来C程序从数组中读取输入-这是正确的吗?“产出”意味着什么?结果是放在另一个数组中还是发送到stdout
或…?我很高兴输入可以以任何方便的形式出现,并且输出可以直接发送到stdout。这看起来非常好。谢谢。@d您是说用testring或Text代替?首先,OP是“从Haskel开始”,我不想引入额外的软件包。其次,您可以使用ByteString.(Lazy.)Char8
中的变量来交换zipWith
和drop
,这样就完成了,代码几乎保持不变。最后但并非最不重要的一点是,这个问题没有明确说明。结果应该打印出来,还是应该是一个列表?无论哪种方式,我都会添加一些关于String
@Zeta的内容,不,我的意思是完全没有。为什么不使用Bool
,或者data Bit=Zero | One
或者其他什么呢?@dfeuer:“给你一个长度为n的1s和0s的长字符串”。但是是的,一个Input->BoolString
后跟slidingWindow::Int->BoolString->……
会更好。对于C程序员来说,“字符串”只是一个8位的数组。OP似乎是一位经验丰富的C程序员。
> 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]