使用文件夹将列表分成指定大小的子列表

问题描述

我正在上一门函数式编程课,而且很难摆脱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)

从列表的其余部分的递归处理中获得的yszs表示列表的其余部分将进入的第一个组和其余组。通过所述递归处理被分解。因此,如果当前元素仍比所需的子集短,那么我们可以在当前元素之前添加当前元素,或者在恰好 时在当前第一个子集之前添加当前元素,并开始一个新的空子集。