问题描述
我正在上一门函数式编程课,而且很难摆脱OOP的思维定式,无法找到许多问题的答案。
我必须创建一个使用有序列表的函数,并使用fold
的变体将其转换为指定大小的子列表。
这是不对的,但这就是我所拥有的:
splitList :: (Ord a) => Int -> [a] -> [[a]]
splitList size xs
| [condition] = foldr (\item subList -> item:subList) [] xs
| otherwise =
我一直在搜索,发现foldr
是最适合我想要的版本,我想我已经了解了折叠的工作方式,只是不知道如何设置防护装置,以便在length sublist == size
haskell重设累加器并转到下一个列表时。
如果我没有正确解释自己,这就是我想要的结果:
> splitList 3 [1..10]
> [[1,2,3],[4,5,6],[7,8,9],[10]]
谢谢!
解决方法
虽然Fabián和chi的答案完全正确,但实际上可以使用foldr
解决此难题。考虑以下代码:
splitList :: Int -> [a] -> [[a]]
splitList n =
foldr (\el acc -> case acc of
[] -> [[el]]
(h : t) | length h < n -> (el : h) : t
_ -> [el] : acc
) []
这里的策略是只要其长度小于期望的长度,就可以通过扩展其头部来构建列表。但是,此解决方案有两个缺点:
-
它的功能与您的示例稍有不同;
splitList 3 [1..10]
产生[[1],[2,3,4],[5,6,7],[8,9,10]]
-
它的复杂度为O(
n * length l
,因为我们测量每个元素上最多n个列表的长度,这会产生线性操作的线性数量。
让我们首先照顾第一个问题。为了从头开始计数,我们需要从左到右遍历列表,而foldr
从右到左遍历。有一个常见的技巧称为“连续通过”,它将使我们能够反转步行的方向:
splitList :: Int -> [a] -> [[a]]
splitList n l = map reverse . reverse $
foldr (\el cont acc ->
case acc of
[] -> cont [[el]]
(h : t) | length h < n -> cont ((el : h) : t)
_ -> cont ([el] : acc)
) id l []
在这里,我们建立了一个函数,该函数将沿正确的方向转换列表,而不是在累加器中构建列表。有关详情,请参见this question。副作用是反转了列表,因此我们需要通过reverse
应用程序对整个列表及其所有元素进行应对。这是线性的和尾递归的。
现在让我们研究性能问题。问题是length
在临时列表上是线性的。有两种解决方案:
- 使用另一种缓存长度以进行恒定时间访问的结构
- 自行缓存值
因为我想这是一个列表练习,所以让我们选择后一个选项:
splitList :: Int -> [a] -> [[a]]
splitList n l = map reverse . reverse . snd $
foldr (\el cont (countAcc,listAcc) ->
case listAcc of
[] -> cont (countAcc,[[el]])
(h : t) | countAcc < n -> cont (countAcc + 1,(el : h) : t)
(h : t) -> cont (1,[el] : (h : t))
) id l (1,[])
在这里,我们使用一个计数器扩展我们的计算状态,该计数器在每个点处存储列表的当前长度。这使我们能够对每个元素进行不断检查,并最终导致线性时间复杂度。
,简化此问题的一种方法是将其拆分为多个功能。您需要做两件事:
- 从列表中获取n个元素,并且
- 保持尽可能多地离开列表。
让我们先尝试:
taking :: Int -> [a] -> [a]
taking n [] = undefined
taking n (x:xs) = undefined
如果没有元素,那么我们就不能再包含任何元素,因此我们只能返回一个空列表,另一方面,如果我们有元素,那么我们可以将taking n (x:xs)
视为x : taking (n-1) xs
,我们只需要检查n> 0。
taking n (x:xs)
| n > 0 = x :taking (n-1) xs
| otherwise = []
现在,我们需要对余数执行多次操作,因此我们可能还应该返回从列表中获取n个元素得到的余数,在这种情况下,当n = 0时余数将变为余数,因此我们可以尝试对其进行适应
| otherwise = ([],x:xs)
,然后您需要修改类型签名以返回([a],[a])
和其他2个定义,以确保您返回taking n
之后的所有内容。
采用这种方法,您的splitList
看起来像:
splitList n [] = []
splitList n l = chunk : splitList n remainder
where (chunk,remainder) = taking n l
但是请注意,折叠将是不合适的,因为它将“变平”您正在处理的任何内容,例如,给定[Int]
时,您可以折叠以产生等于Int
的总和。 (foldr :: (a -> b -> b) -> b -> [a] -> b
或“ foldr function zero list
产生函数返回类型的元素”)
您要
splitList 3 [1..10]
> [[1,2,3],[4,5,6],[7,8,9],[10]]
由于尾部有“ [10]
”,因此建议您改用foldl
。例如
splitList :: (Ord a) => Int -> [a] -> [[a]]
splitList size xs
| size > 0 = foldl go [] xs
| otherwise = error "need a positive size"
where go acc x = ....
go
应该做什么?本质上,在您的示例中,我们必须具有:
splitList 3 [1..10]
= go (splitList 3 [1..9]) 10
= go [[1,9]] 10
= [[1,[10]]
splitList 3 [1..9]
= go (splitList 3 [1..8]) 9
= go [[1,8]] 9
= [[1,9]]
splitList 3 [1..8]
= go (splitList 3 [1..7]) 8
= go [[1,[7]] 8
= [[1,8]]
和
splitList 3 [1]
= go [] 1
= [[1]]
因此,go acc x
应该
- 检查
acc
是否为空,如果是,则生成一个单例列表[[x]]
。 - 否则,请检查
acc
中的最后一个列表:- 如果其长度小于
size
,请附加x
- 否则,将 new 列表
[x]
附加到acc
- 如果其长度小于
尝试在您的示例上手动进行操作,以了解所有情况。
这不会很有效,但是会起作用。
您实际上并不需要Ord a
约束。
检查累加器的第一个子列表的长度将导致信息从右侧流动,而第一个块可能终止于较短的那个(而不是最后一个)。此类功能也无法在无限列表上使用(更不用说基于foldl
的变体了。)
使用foldr
从左侧开始安排信息流的标准方法是使用附加参数。总体方案是
subLists n xs = foldr g z xs n
where
g x r i = cons x i (r (i-1))
....
i
的{{1}}参数将指导其决定将当前元素添加到何处。 cons
会在从左向右的方向递减计数器,而不是从右向后的递减计数器。 i-1
必须具有与z
相同的类型,并且必须与r
本身具有相同的类型,因此,
foldr
这意味着必须有一个后处理步骤,并且还必须处理一些边缘情况,
z _ = [[]]
subLists n xs = post . foldr g z xs $ n
where
z _ = [[]]
g x r i | i == 1 = cons x i (r n)
g x r i = cons x i (r (i-1))
....
必须足够懒惰,以免过早强制执行递归调用的结果。
我把它留作练习来结束。
有关带有预处理步骤的更简单版本,请参阅我的this recent answer。
,只需给出另一个答案:这与尝试将groupBy
折叠在一起非常相似,实际上有几个陷阱。高效和正确的实现必须牢记的懒惰。以下是我发现的最快的版本,可保留所有相关的惰性属性:
splitList :: Int -> [a] -> [[a]]
splitList m xs = snd (foldr f (const ([],[])) xs 1)
where
f x a i
| i <= 1 = let (ys,zs) = a m in ([],(x : ys) : zs)
| otherwise = let (ys,zs) = a (i-1) in (x : ys,zs)
从列表的其余部分的递归处理中获得的ys
和zs
表示列表的其余部分将进入的第一个组和其余组。通过所述递归处理被分解。因此,如果当前元素仍比所需的子集短,那么我们可以在当前元素之前添加当前元素,或者在恰好 时在当前第一个子集之前添加当前元素,并开始一个新的空子集。