问题
让我们假设我们有一个列表 xs
(可能是一个非常大的列表),我们想检查它的所有元素是否相同 .
我想出了各种各样的想法:
解决方案0
检查 tail xs
中的所有元素是否等于 head xs
:
allTheSame :: (Eq a) => [a] -> Bool
allTheSame xs = and $ map (== head xs) (tail xs)
解决方案1
检查 length xs
是否等于从 xs
获取元素所获得的列表长度,而它们等于 head xs
allTheSame' :: (Eq a) => [a] -> Bool
allTheSame' xs = (length xs) == (length $ takeWhile (== head xs) xs)
解决方案2
递归解决方案: allTheSame
如果 xs
的前两个元素相等则返回 True
而 allTheSame
返回 xs
的其余部分 True
allTheSame'' :: (Eq a) => [a] -> Bool
allTheSame'' xs
| n == 0 = False
| n == 1 = True
| n == 2 = xs !! 0 == xs !! 1
| otherwise = (xs !! 0 == xs !! 1) && (allTheSame'' $ snd $ splitAt 2 xs)
where n = length xs
解决方案3
分而治之:
allTheSame''' :: (Eq a) => [a] -> Bool
allTheSame''' xs
| n == 0 = False
| n == 1 = True
| n == 2 = xs !! 0 == xs !! 1
| n == 3 = xs !! 0 == xs !! 1 && xs !! 1 == xs !! 2
| otherwise = allTheSame''' (fst split) && allTheSame''' (snd split)
where n = length xs
split = splitAt (n `div` 2) xs
解决方案4
我在写这个问题的时候就想到了这个:
allTheSame'''' :: (Eq a) => [a] -> Bool
allTheSame'''' xs = all (== head xs) (tail xs)
问题
-
我认为解决方案0效率不高,至少在内存方面,因为
map
将在将and
应用于其元素之前构造另一个列表 . 我对吗? -
解决方案1仍然不是很有效,至少在内存方面,因为
takeWhile
将再次构建一个额外的列表 . 我对吗? -
解决方案2是尾递归(对吗?),它应该非常有效,因为只要
(xs !! 0 == xs !! 1)
为False它就会返回False
. 我对吗? -
解决方案3应该是最好的,因为它的复杂性应该是O(log n)
-
解决方案4看起来很Haskellish给我(是吗?),但它可能与解决方案0相同,因为
all p = and . map p
(来自Prelude.hs) . 我对吗? -
还有其他更好的写作方式
allTheSame
吗?现在,我希望有人会回答这个问题告诉我,我已经用hoogle搜索了这个问题,我还没有学习Haskell,我相信这对我来说是一个很好的练习:)
欢迎任何其他评论 . 谢谢!
9 回答
gatoatigrado的回答为衡量各种解决方案的性能提供了一些很好的建议 . 这是一个更具象征性的答案 .
我认为解决方案0(或者,完全相同,解决方案4)将是最快的 . 请记住,Haskell是惰性的,因此
map
将不必在应用and
之前构造整个列表 . Build 直觉的好方法是玩无限 . 例如:这会询问是否所有数字都小于1,000 . 如果
map
在应用and
之前构建了整个列表,那么这个问题永远无法回答 . 即使你给列表一个非常大的右 endpoints (即Haskell没有做任何"magic",取决于列表是否是无限的),表达式仍然会快速回答 .为了开始我的例子,让我们使用这些定义:
以下是
allTheSame [7,7,7,7,8,7,7,7]
的评估顺序 . 写下来会有额外的分享,这太痛苦了 . 我还会比简洁性更早地评估head
表达式(无论如何它都会被评估,所以它几乎没有什么不同) .看看我们怎么做't even have to check the last 3 7'?这是一种懒惰的评估,使列表更像循环 . 所有其他解决方案都使用昂贵的函数,如
length
(它们必须一直走到列表末尾才能给出答案),因此它们的效率会降低,而且它们也无法在无限列表上运行 . 在无限列表上工作并且有效率通常在Haskell中一起使用 .首先,我认为您不想使用列表 . 很多算法都依赖于计算长度,这很糟糕 . 您可能需要考虑vector包,它将为O(1)提供长度,而O(n)则为列表 . 向量也具有更高的内存效率,特别是如果您可以使用Unboxed或Storable变体 .
话虽这么说,你真的需要考虑代码中的遍历和使用模式 . Haskell的列表非常有效,如果它们可以按需生成并消耗一次 . 这意味着您不应该继续引用列表 . 像这样的东西:
要求整个列表保留在内存中(通过
sum
或length
),直到两个遍历都完成 . 如果您可以一步完成列表遍历,那么效率会更高 .当然,您可能还需要保留列表,例如检查所有元素是否相等,如果不相同,请对数据执行其他操作 . 在这种情况下,对于任何大小的列表,您可能最好使用更紧凑的数据结构(例如矢量) .
现在,这是他们的方式,这里看看这些功能 . 在我展示核心的地方,它是用
ghc-7.0.3 -O -ddump-simpl
生成的 . 此外,在使用-O0编译时,不要打扰判断Haskell代码性能 . 使用实际用于 生产环境 代码的标志进行编译,通常至少为-O,也可能是其他选项 .Solution 0
GHC产生这个核心:
实际上,这看起来很不错 . 它会产生一个带有空列表的错误,但这很容易修复 . 这是
case xs_abH of _ { [] ->
. 在GHC执行worker / wrapper转换之后,递归worker函数是letrec { go_sDv
绑定 . Worker 检查其论点 . 如果[]
,则它到达列表的末尾并返回True . 否则,它将剩余的头部与第一个元素进行比较,并返回False或检查列表的其余部分 .其他三个功能 .
map
已完全融合,并未分配临时列表 .在定义顶部附近注意
Cheap=True
语句 . 这意味着GHC认为函数"cheap",因此是内联的候选者 . 在调用站点,如果可以确定具体的参数类型,GHC可能会内联allTheSame
并产生一个非常紧密的内循环,完全绕过Eq
字典查找 .worker函数是尾递归的 .
判决:非常有力的竞争者 .
Solution 1
即使不看核心我也知道这不会那么好 . 该列表不止一次遍历,首先是
length xs
,然后是length $ takeWhile
. 您不仅有多次遍历的额外开销,这意味着列表必须在第一次遍历后保留在内存中,并且可以't be GC' d . 对于一个大清单,这是一个严重的问题 .看看核心并不能说明这一点 . 但请注意以下几行:
这是列表遍历发生的地方 . 第一个获取外部列表的长度并将其绑定到
ww_aC6
. 第二个获取内部列表的长度,但绑定不会发生,直到接近底部,在长度(均为
Int
s)可以拆箱并通过primop进行比较,但是已经引入了's a small consolation after the overhead that' .判决结果:不好 .
Solution 2
这与解决方案1具有相同的问题 . 列表被遍历多次,并且不能进行GC . 但这里更糟糕,因为现在计算每个子列表的长度 . 我希望这对任何重要大小的列表都有最糟糕的表现 . 另外,当你期望列表很大时,你为什么要特别设置1和2元素的套管列表?
判决:甚至不要考虑它 .
Solution 3
这与解决方案2具有相同的问题 . 即,列表被
length
遍历多次 . 我不确定分而治之的方法对于这个问题是一个很好的选择,它最终可能比简单的扫描花费更长的时间 . 这取决于数据,值得测试 .结论:也许,如果你使用了不同的数据结构 .
Solution 4
这基本上是我的第一个念头 . 让我们再次检查核心 .
好的,还不错 . 与解决方案1一样,这将在空列表上出错 . 列表遍历隐藏在
GHC.List.all
中,但它可能会在呼叫站点扩展为良好的代码 .判决:另一个强有力的竞争者 .
因此,在所有这些之间,我希望解决方案0和4是唯一值得使用的,并且它们几乎相同 . 在某些情况下我可能会考虑选项3 .
编辑:在这两种情况下,空列表中的错误可以简单地修复,如@ augustss的答案 .
下一步是使用criterion进行一些时间分析 .
使用连续对的解决方案:
Q1 - 是的,我认为你的简单解决方案很好,没有内存泄漏 . Q4 - 解决方案3不是log(n),通过非常简单的参数,您需要查看所有列表元素以确定它们是否相同,并且查看1个元素需要1个时间步长 . Q5 - 是的 . Q6,见下文 .
解决这个问题的方法是输入并运行它
然后运行
ghc -O3 -optc-O3 --make Main.hs && time ./Main
. 我最喜欢上一个解决方案(你也可以使用模式匹配来清理它),打开ghci并在这些东西上运行“:step fcn” . 它将教你很多关于懒惰评估正在扩展的内容 . 通常,当您匹配构造函数时,例如“x:xs”,这是恒定的时间 . 当你调用“length”时,Haskell需要计算列表中的所有元素(尽管它们的值仍然是“待计算”),因此解决方案1和2都很糟糕 .
编辑1
对不起,如果我之前的回答有点浅薄 . 似乎手动扩展的东西确实有点帮助(虽然与其他选项相比,这是一个微不足道的改进),
似乎ghc已经专门化了这个函数,但你也可以查看specialize pragma,以防它对你的代码不起作用[link] .
这是另一个版本(如果某些内容不匹配,则不需要遍历整个列表):
这可能在语法上不正确,但我希望你有这个想法 .
这是另一种有趣的方式:
通过跟踪前一个元素而不是第一个元素,可以轻松地更改此实现以实现
increasing
或decreasing
. 要针对第一个检查所有这些,您可以将prev
重命名为first
,并将Just x
替换为Just first
.这将如何优化?我没有详细检查,但我要去了根据我对GHC优化的一些了解,讲述一个好故事 .
首先假设列表融合没有发生 . 那么
foldr
将被内联,给出类似的东西Eta扩张然后收益
内联
go
,现在GHC可以识别递归调用
Maybe
值总是Just
,并使用worker-wrapper转换来利用这个:现在请记住
并且
allSame'
不再是递归的,因此可以降低beta:因此,高阶代码已经转变为有效的递归代码而没有额外的分配 .
使用
-O2 -ddump-simpl -dsuppress-all -dno-suppress-type-signatures
编译定义allSame
的模块会产生以下结果(我已将其清理了一下):如您所见,这与我描述的结果基本相同 .
equal = == $dEq_a
位是从Eq
字典中提取相等方法并保存在变量中的位置,因此只需要提取一次 .如果列表融合发生怎么办?以下是对定义的提醒:
如果我们调用
allSame (build g)
,foldr
将根据规则foldr c n (build g) = g c n
与build
融合,产生除非
g
已知,否则这并不能让我们感到有趣 . 所以让我们选择一些简单的东西:所以,如果
h = allSame (replicate k0 a)
,h
成为Eta扩张,
内联
go
,再次,GHC可以看到递归调用总是
Just
,所以由于
rep
不再递归,GHC可以减少它:正如您所看到的,这可以在没有任何分配的情况下运行!显然,这是一个愚蠢的例子,但在许多更有趣的案例中会发生类似的事情 . 例如,如果您编写导入
allSame
函数并定义的AllSameTest
模块并按上述方法编译它,您将得到以下内容(未清理) .
这可能看起来很恶心,但是你会注意到在任何地方都没有
:
构造函数,并且Int
都是未装箱的,因此该函数可以在零分配的情况下运行 .我想我可能只是在实现
find
并重做this . 不过,我认为看到它的内部结构是有益的 . (注意解决方案如何依赖于等式是可传递的,尽管还要注意问题如何要求相等的传递是连贯的 . )我喜欢
sameElement
如何查看列表的第一个O(1)元素,然后返回结果或递归列表的某些后缀,特别是尾部 . 我对这个结构没有任何明智的说法,我只是喜欢它:-)我想我和this做了同样的比较 . 如果我用
sameElement x:xs
递归,我会将输入列表的头部与解决方案0中的每个元素进行比较 .切线:如果需要,可以通过将
Nothing
替换为Left (x, y)
并将Just x
替换为Right x
并将isJust
替换为either (const False) (const True)
来报告两个不匹配的元素 .虽然效率不高(即使前两个元素不匹配,它也会遍历整个列表),这是一个厚颜无耻的解决方案:
纯娱乐 .
这种实现是优越的 .
给定(==)运算符的传递性,如果您希望确保表达式链的相等性(例如a = b = c = d),则假设Eq的实例已得到很好的实现,您只需要确保a = b,b = c,c = d,并且d = a,代替上面提供的技术,例如a = b,a = c,a = d,b = c,b = d,c = d .
我提出的解决方案与您希望测试的元素数量线性增长,后者是二次方,即使您引入常数因素以期提高其效率 .
它也优于使用组的解决方案,因为您最终不必使用长度 .
你也可以用逐点的方式写出来,但我不会厌烦你这么琐碎的细节 .