【问题标题】:Haskell - given list, break it into lists of identical items next to each other: [1,1,2,4,5,5] ⟼ [[1,1], [2], [4], [5,5]]Haskell - 给定列表,将其分解为彼此相邻的相同项目的列表:[1,1,2,4,5,5] ⟼ [[1,1], [2], [4], [5,5 ]]
【发布时间】:2021-11-29 14:48:44
【问题描述】:

给定列表,将其分解为彼此相邻的相同项目的列表:
[1,1,2,4,5,5][[1,1], [2], [4], [5,5]]

我需要使用递归而不是高阶函数来做到这一点。

这是我到目前为止所得到的

breakList :: [Int] -> [[Int]]
breakList [] = [[]]
breakList [x] = [[x]]
breakList (x:y:rest)
    | x == y = [x] : breakList (y:rest)
    | otherwise = [x] : breakList rest

但它不起作用。

【问题讨论】:

    标签: list haskell recursion


    【解决方案1】:

    您可以检查递归调用结果的第一项是否具有相同的值。如果是这种情况,我们会在该列表前面加上x,否则我们会以x 作为列表的唯一成员开始一个“新组”,所以[x]

    breakList :: Eq a => [a] -> [[a]]
    breakList [] = []
    breakList [x] = [[x]]  -- (1)
    breakList (x:xs)  -- (2)
      | x == y = (x:ys) : yss  -- (3)
      | otherwise = [x] : ys : yss  -- (4)
      where ~(ys@(y:_):yss) = breakList xs  -- (5)

    这里我们首先计算尾递归的结果,我们知道这将是非空的,因为 (2) 中的 (x:xs) 模式只有在列表包含至少两个项目时才会触发(如果它只包含一项,则 (1) 子句将触发。

    然后我们可以将结果与模式~(ys@(y:_):yss) 进行模式匹配,其中ys 是结果的第一个子列表,y 是该子列表的第一项,yss 是一个可能为空的列表已构建的其他组。

    因此,我们可以检查我们必须放入组中的项目 x 是否与第一个子组的第一项 y 具有相同的值。如果是这种情况,我们使用(x:ys) : yss 构造一个新的 lsit,我们在第一个子列表前面加上x (2);如果不是这种情况,我们会在子列表前面加上 [x] 列表来创建一个新组。

    我们可以让它更懒惰:

    breakList :: Eq a => [a] -> [[a]]
    breakList [] = []
    breakList [x] = [[x]]  -- (1)
    breakList (x:xs@(y:_))  -- (2)
      | x == y = (x:ys) : yss  -- (3)
      | otherwise = [x] : ys : yss  -- (4)
      where ~(ys:yss) = breakList xs  -- (5)

    这也可以在每次相同对象的无限列表上工作:breakList (1 : 1 : 2 : 4 : 5: 5 : repeat 1)) 将产生 [[1,1],[2],[4],[5,5],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, …

    【讨论】:

    • 不幸的是,新版本在take 1 . head . breakList $ [1]++undefined 上失败了。
    【解决方案2】:

    这是一个替代解决方案,它在列表的遍历过程中使用列表作为辅助数据结构。

    这个辅助数据结构存储了到目前为止的相等元素。对于每个新元素 (1) 我们检查它是否等于辅助列表中的元素之一,如果是我们将其添加到该列表中,否则 (2) 我们输出辅助列表作为结果并开始一个新的辅助列出这个新元素。最后(3)我们简单的输出我们到现在得​​到的辅助列表作为最终结果。

    breakList :: [Int] -> [[Int]]
    breakList [] = []
    breakList (x:xs) = go [x] xs where
      go ys [] = [ys]                -- (3)
      go ~ys@(y:_) (x:xs)
        | x == y    = go (x:ys) xs   -- (1)
        | otherwise = ys : go [x] xs -- (2)
    

    如果您希望它稳定,即所有元素必须保持相同的顺序(在整数的情况下这无关紧要,但对于其他数据类型则可以),那么您必须反转输出列表:

    breakList :: [Int] -> [[Int]]
    breakList [] = []
    breakList (x:xs) = go [x] xs where
      go ys [] = [reverse ys]
      go ~ys@(y:_) (x:xs)
        | x == y    = go (x:ys) xs
        | otherwise = reverse ys : go [x] xs
    

    该算法还假设== 是可传递的,这在技术上不是必需的,但对于整数来说肯定是这样。虽然,如果== 不具有传递性,您给出的规范是不明确的,我们应该将每个新元素与哪个元素进行比较?组的第一个元素,还是前一个元素?无论如何,这可能不是您需要担心的。

    【讨论】:

    • 啊,是的,用累加器建立一个组。唯一可能的缺点可能是它不是“稳定的”。但我同意这可能是吹毛求疵。 +1。
    • @WillemVanOnsem 我已经添加了关于如何在正确的位置使用reverse 函数使其稳定的部分。
    • 传统上,Haskell 中一个好的解决方案应该尽可能地懒惰。特别是,take 1 . head . breakList $ repeat 1 不应挂起。
    • @WillNess 这是一个很好的观点,但似乎 Willem Van Onsem 的解决方案也适用于该测试用例。可以写一个更懒的版本吗?请添加它作为另一个解决方案。我也会试试看能不能想出点什么来。
    【解决方案3】:

    group 已经这样做了。 :) 显然,它被定义为等同于group (x:xs) = (x:ys) : group zs where (ys,zs) = span (x ==) xs

    当然禁止使用 HOF,因此需要内联 span 的定义。它必须足够懒惰,即尽可能早地产生,尽可能少地强制输入。

    类似

    breakList :: [Int] -> [[Int]]
    breakList [] = [[]]
    breakList (x:ys) = (x:xs):r
      where
      (xs:r) = go x ys
      go x (y:ys) 
         | x==y      =      ((y:a):b)
         | otherwise = [] : ((y:a):b)
                        where (a:b) = go y ys
      go _ []        = [[]]
    

    所以我们有

    > breakList $ [1,1,1,2,3,3,4]
    [[1,1,1],[2],[3,3],[4]]
    
    > take 1 . head . breakList $ repeat 1
    [1]
    
    > take 3 . head . breakList $ [1,1,1]++undefined
    [1,1,1]
    

    似乎确实很懒惰。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-04-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多