haskell foldl或foldr插入列表元素更有效吗?

drkbr07n  于 2023-11-18  发布在  其他
关注(0)|答案(1)|浏览(100)
| l > 1     = foldr (\a b -> a ++ "-----\n" ++ b) "" xs

字符串
xs是一个字符串列表,目的是用五个破折号和一个换行符连接列表中的每个元素。
在这种情况下,使用foldlfoldr会更有效吗?为什么?我知道intercalate很适合这个目的,但在我的情况下,我不能使用导入。

yqlxgs2m

yqlxgs2m1#

foldr在这里更好,因为++(以及一般的列表)是“自然”右关联的。
++的定义是:

(++) :: [a] -> [a] -> [a]
(++) []     ys = ys
(++) (x:xs) ys = x : xs ++ ys

字符串
注意,第二个参数ys在两个等式中都是未经修改地使用的,而第一个列表是分开的,它的元素被前置到正在构造的新列表的前面。所以出现在++的 * 左边 * 的东西必须被扫描和重建,而右边出现的东西是免费的。即使我们完全展开++的递归定义,比如:

let xs = [x1, x2, x3, x4]
 in xs ++ ys


然后我们得到x1 : x2 : x3 : x4 : ys; ys只是原样存储在数据构造函数(嵌套最深的:)的字段中,因此不会被++检查。但是整个xs列表必须被遍历并重建到ys的前面。
因此,当我们折叠以构建列表时,我们希望确保累积参数(随着我们处理列表的更多内容,它会变得越来越大)始终用作++right 参数。这样就不需要在折叠的每一层都重新遍历它。
所以foldr (\a b -> a ++ "-----\n" ++ b) "" xs是好的; b参数接收折叠列表其余部分的结果,它被用作++的右参数,在那里它根本不需要被检查。这意味着折叠的每一层的成本是遍历a的成本和遍历"-----\n"的成本。总的来说,成本是整个fold的值是所有a值的成本之和(因此是xs中列表的长度之和),加上一些小的固定字符串,这些字符串等于xs中的元素数量。
如果我们使用像foldl (\b a -> b ++ "-----\n" ++ a) "" xs这样的左折叠,情况会更糟。(a)在++的右边,并在左边使用前面的结果b。所以我们保持a不变,并且必须遍历b以将其所有元素前置到a的前面。这在折叠的底部很好,其中b""的起始值。但是在b之外的水平上,* 结果 * 在第一个a字符串中折叠(加上"-----\n"分隔符字符串),所以我们无论如何都要遍历a字符串。然后再一次向外一级b将是++将另一个字符串连接到前面的结果,所以我们必须遍历新的字符串 * 加上 * 再次遍历第一个字符串 *。以这种方式折叠整个N个元素的列表,我们最终遍历第一个字符串N次,第二个N-1次,这比使用正确的折叠要昂贵得多。
你可能会想到,你可以使用左折叠,但仍然把累加参数放在++的右边(只要你不介意最后一个字符串的顺序颠倒)。所以:

foldl (\b a -> a ++ "-----\n" ++ b) "" xs


在性能方面,这并不像非反向左折叠那样糟糕。在每一层,我们只需要遍历来自列表的传入元素,而不检查折叠列表其余部分的结果,就像右折叠一样。(这是所有左折叠所固有的,无论你使用什么函数),扩展foldl递归的一个级别(假设xs不为空)会给我们:

foldl (\b a -> a ++ "-----\n" ++ b) "" (x : xs)
==> foldl (\b a -> a ++ "-----\n" ++ b) (x ++ "-----\n" ++ "") xs


它扩展到另一个对foldl的调用,在我们知道什么元素在结果列表的头部之前,我们必须评估它。(如果xs不为空)将返回 another 调用foldl。它将一直这样做,直到我们达到基本情况,并最终返回++应用程序的大链。所以我们不能得到直到我们遍历了整个字符串的原始列表。
如果我们看一下正确的fold版本,我们会看到一些不同的东西。展开foldr递归的一级(假设xs不为空),我们会得到:

foldr (\a b -> a ++ "-----\n" ++ b) "" (x : xs)
==> x ++ "-----\n" ++ foldr (\a b -> a ++ "-----\n" ++ b) "" xs)


结果表达式不是对foldr的调用,而是对++的调用(左边的是最外面的东西在这里被称为;剩下的都在++的右参数中)。我们只需要扩展++一步,就可以确定最终折叠列表的第一个元素是x的第一个字符(这是原始字符串列表中的第一个字符串)。我们可以做到这一点,而不必遍历xs字符串列表的任何其余部分,甚至处理"-----\n"分隔符 * 一次 *。

foldr的这种“流”行为(我们可以在只检查输入列表的第一部分后获得其输出的第一部分)使foldr能够处理foldl根本无法处理的无限列表,如果我们最终使用像take这样的函数,它不检查所有的结果列表,我们就可以保存很多工作 。遍历整个输入列表。但即使在最终读取整个结果的情况下,(因此无论如何都将遍历整个输入),这种方式通常还是更快。它允许元素一次处理一个,一直通过类似的管道的多个阶段处理。如果输入列表的初始生产者和输出列表的最终消费者也以这种“流”方式工作,这节省了必须一次将整个列表存储在内存中,这可以节省保存大量的时间分配和垃圾回收的内存。也有优化(例如重写规则)应用于许多标准列表函数,这些标准列表函数通常可以完全优化中间列表的:单元格的创建,这节省了更多的时间。
最终,列表是“自然”右关联的,因为它们在:单元格中的定义是在右边递归的。如果可以的话,以右关联的方式构建和处理它们总是更好。
主要的例外是当你把一个列表处理成一个非常小的(就内存大小而言)没有任何可以进行模式匹配的子结构的值,比如IntDouble。在这种情况下,不可能以流的方式进行该操作;如果你得到一个列表的长度作为一个Int有没有“第一部分”的Int
可能 * 后,遍历列表的第一部分可用。Int是全或-在这种情况下,foldl'(注意'后缀是strictleft fold)通常更快,因为right fold会建立一个不能被懒惰地消耗的大操作链;严格的left fold可以执行它们。

相关问题