递归的替代使用如何影响Haskell列表反转的效率?
休息了很长时间后,我正在复习哈斯克尔。我通过s工作。我为#5写了一个解决方案,然后看了一眼,扩展了我的知识。以下是#5解决方案页面的摘录: 撤销一份清单 前奏曲中的标准定义是简洁的,但不是简单的 非常可读。定义反向的另一种方法是:递归的替代使用如何影响Haskell列表反转的效率?,haskell,recursion,Haskell,Recursion,休息了很长时间后,我正在复习哈斯克尔。我通过s工作。我为#5写了一个解决方案,然后看了一眼,扩展了我的知识。以下是#5解决方案页面的摘录: 撤销一份清单 前奏曲中的标准定义是简洁的,但不是简单的 非常可读。定义反向的另一种方法是: reverse :: [a] -> [a] reverse [] = [] reverse (x:xs) = reverse xs ++ [x] 然而,这个定义比前奏曲中的定义更浪费 在累积结果时反复确认结果。以下 变异避免了这一点,因此在计算上更接近前奏 版
reverse :: [a] -> [a]
reverse [] = []
reverse (x:xs) = reverse xs ++ [x]
然而,这个定义比前奏曲中的定义更浪费
在累积结果时反复确认结果。以下
变异避免了这一点,因此在计算上更接近前奏
版本
reverse :: [a] -> [a]
reverse list = reverse' list []
where
reverse' [] reversed = reversed
reverse' (x:xs) reversed = reverse' xs (x:reversed)
有人能解释为什么第二种解决方案比第一种更有效吗?我可以看到这两种解决方案都使用递归,但我还不能理解差异是如何影响效率的,这与Haskell遍历列表有关吗?第一种解决方案使用递归,但不是尾部递归,递归调用是函数做的最后一件事。尾部递归比非尾部递归更有效,因为函数调用后不需要继续执行,因此其堆栈帧等可以重用 第一个解决方案首先进行递归调用,然后对结果和单个元素列表调用连接运算符。第二种解决方案首先将元素添加到列表的前面,然后再次调用自身,而无需做任何进一步的操作 此外,
++
在其第一个参数的长度上需要线性时间,因为它需要复制列表的每个节点,以便可以将最后一个节点更改为指向右侧列表的第一个节点。这是因为第一个变量从内到外执行其实际工作(首先它递归地调用自己,然后返回,它执行实际工作,这意味着内部调用将首先执行其工作),或者换句话说,它从右到左处理各个列表元素。因此,当它遇到一个新元素(在原始列表中位于更左侧)时,它需要将其添加到列表的右侧。在Haskell列表中,添加到右侧是昂贵的,添加到左侧是便宜的
第二种解决方案是从外到内,或从左到右,这是此问题的正确执行顺序。第一种解决方案使用递归,但它不是尾部递归,其中递归调用是函数执行的最后一件事。尾部递归比非尾部递归更有效,因为函数调用后不需要继续执行,因此其堆栈帧等可以重用 第一个解决方案首先进行递归调用,然后对结果和单个元素列表调用连接运算符。第二种解决方案首先将元素添加到列表的前面,然后再次调用自身,而无需做任何进一步的操作 此外,
++
在其第一个参数的长度上需要线性时间,因为它需要复制列表的每个节点,以便可以将最后一个节点更改为指向右侧列表的第一个节点。这是因为第一个变量从内到外执行其实际工作(首先它递归地调用自己,然后返回,它执行实际工作,这意味着内部调用将首先执行其工作),或者换句话说,它从右到左处理各个列表元素。因此,当它遇到一个新元素(在原始列表中位于更左侧)时,它需要将其添加到列表的右侧。在Haskell列表中,添加到右侧是昂贵的,添加到左侧是便宜的
第二种解决方案是从外到内,或从左到右,这是此问题的正确执行顺序。第一种解决方案使用递归,但它不是尾部递归,其中递归调用是函数执行的最后一件事。尾部递归比非尾部递归更有效,因为函数调用后不需要继续执行,因此其堆栈帧等可以重用 第一个解决方案首先进行递归调用,然后对结果和单个元素列表调用连接运算符。第二种解决方案首先将元素添加到列表的前面,然后再次调用自身,而无需做任何进一步的操作 此外,
++
在其第一个参数的长度上需要线性时间,因为它需要复制列表的每个节点,以便可以将最后一个节点更改为指向右侧列表的第一个节点。这是因为第一个变量从内到外执行其实际工作(首先它递归地调用自己,然后返回,它执行实际工作,这意味着内部调用将首先执行其工作),或者换句话说,它从右到左处理各个列表元素。因此,当它遇到一个新元素(在原始列表中位于更左侧)时,它需要将其添加到列表的右侧。在Haskell列表中,添加到右侧是昂贵的,添加到左侧是便宜的
第二种解决方案是从外到内,或从左到右,这是此问题的正确执行顺序。第一种解决方案使用递归,但它不是尾部递归,其中递归调用是函数执行的最后一件事。尾部递归比非尾部递归更有效,因为函数调用后不需要继续执行,因此其堆栈帧等可以重用 第一个解决方案首先进行递归调用,然后对结果和单个元素列表调用连接运算符。第二种解决方案首先将元素添加到列表的前面,然后再次调用自身,而无需做任何进一步的操作 除此之外还有
++<