如何将列表中的连续数字组合到 Haskell 中的范围中?

How do I combine consectuive numbers in a list into a range in Haskell?

我正在努力解决 Haskell 问题,但我很难确定此特定任务的一般程序/算法。我想要做的基本上是给 Haskell 一个列表 [1,2,3,5,6,9,16,17,18,19] 并让它给我返回 [1-3, 5, 6, 9, 16 -19] 所以本质上是将三个或更多连续数字转换为最低数字 - 最高数字的范围。我认为我遇到的问题是在处理 Haskell 的功能范式时非常普遍的困难。所以我真的很感激一个通用算法或如何从"哈斯凯利"的angular来看待这个问题。

提前致谢。


如果我正确理解了这个问题,我们的想法是将输入列表分成块,其中块是单个输入元素或至少三个连续元素的范围。

所以,让我们从定义一个数据类型来表示这些块开始:

1
data Chunk a = Single a | Range a a

如您所见,类型在输入元素的类型中是参数化的。

接下来,我们定义一个函数 chunks 来从输入元素列表中实际构造一个块列表。为此,我们需要能够比较输入元素并获得给定输入元素(即其后继元素)的直接连续元素。因此,函数的类型为

1
chunks :: (Eq a, Enum a) => [a] -> [Chunk a]

实现比较简单:

1
2
3
4
5
chunks = foldr go []
 where
  go x (Single y : Single z : cs) | y == succ x && z == succ y = Range x z : cs
  go x (Range y z : cs) | y == succ x = Range x z : cs
  go x cs                             = Single x : cs

我们从右到左遍历列表,边走边生成块。如果输入元素在它的两个直接连续元素之前(辅助函数 go 的第一种情况)或者如果它在以它的直接连续元素开始的范围之前(第二种情况),我们将生成一个范围。否则,我们生成单个元素(最后一种情况)。

为了安排漂亮的输出,我们将类型构造函数 Chunk 的应用程序声明为类 Show 的实例(假设输入元素的类型在 Show 中):

1
2
3
instance Show a => Show (Chunk a) where
  show (Single x ) = show x
  show (Range x y) = show x ++"-" ++ show y

回到问题中的例子,我们有:

1
2
> chunks [1,2,3,5,6,9,16,17,18,19]
[1-3,5,6,9,16-19]

不幸的是,如果我们需要考虑有界元素类型,事情会稍微复杂一些;此类类型具有未定义 succ 的最大元素:

1
2
> chunks [maxBound, 1, 2, 3] :: [Chunk Int]
*** Exception: Prelude.Enum.succ{Int}: tried to take `succ' of maxBound

这表明我们应该从确定一个元素是否继承另一个元素的特定方法中抽象出来:

1
2
3
4
5
6
7
chunksBy :: (a -> a -> Bool) -> [a] -> [Chunk a]
chunksBy succeeds = foldr go []
 where
  go x (Single y : Single z : cs) | y `succeeds` x && z `succeeds` y =
    Range x z : cs
  go x (Range y z : cs) | y `succeeds` x = Range x z : cs
  go x cs = Single x : cs

现在,上面给出的 chunks 的版本,可以用 chunksBy 表示,只需编写

1
2
chunks :: (Eq a, Enum a) => [a] -> [Chunk a]
chunks = chunksBy (\\y x -> y == succ x)

此外,我们现在还可以为有界输入类型实现一个版本:

1
2
chunks' :: (Eq a, Enum a, Bounded a) => [a] -> [Chunk a]
chunks' = chunksBy (\\y x -> x /= maxBound && y == succ x)

这给我们带来了快乐:

1
2
> chunks' [maxBound, 1, 2, 3] :: [Chunk Int]
[9223372036854775807,1-3]


使用递归。递归是信念的飞跃。假设您已经编写了定义,因此可以("递归")在整个问题的子问题上调用它,并将(递归计算的)子结果与剩余部分结合起来以获得完整的解决方案——简单:

1
2
3
4
5
6
7
8
ranges xs  =  let  (leftovers, subproblem) = split xs
                   subresult = ranges subproblem
                   result = combine leftovers subresult
              in
                   result
   where
   split xs  =  ....
   combine as rs  =  ....

现在,我们知道 combiners 的类型(即 ranges 中的 subresult)——这就是 ranges 返回的内容:

1
ranges :: [a] -> rngs

那么,我们如何split 我们的输入列表xs?面向类型的设计理念说,遵循类型。

xsa 的列表 [a]。这种类型有两种情况:[]x:ysx :: ays :: [a]。因此,将列表拆分为较小的列表和一些剩余部分的最简单方法是

1
2
    split (x:xs)  =  (x, ys)
    split []      =  *error*"no way to do this"   -- intentionally invalid code

考虑到最后一种情况,我们必须调整整体设计以将其考虑在内。但首先,rngs 类型可能是什么?根据您的示例数据,它是 rng 的列表,自然是 rngs ~ [rng]

虽然是 rng 类型,但我们有相当大的自由度让它成为我们想要的任何东西。我们必须考虑的情况是对和单例:

1
2
data Rng a  =  Single a
            |  Pair a a

....现在我们需要将锯齿状的碎片拼凑成一张图片。

将数字与从连续数字开始的范围相结合是显而易见的。

将一个数字与一个数字组合将有两种明显的情况,无论这些数字是否连续。

我想/希望你能从这里开始。


首先,列表的所有元素必须属于同一类型。您的结果列表有两种不同的类型。 Ranges(无论这意味着什么)和 Ints。我们应该将一位数转换为最低和最高相同的范围。

已经说过,您应该定义 Range 数据类型并将您的 Int 列表折叠成 Range

列表

1
2
3
4
5
6
data Range = Range {from :: Int , to :: Int}

intsToRange :: [Int] -> [Range]
intsToRange [] = []
intsToRange [x] = [Range x x]
intsToRange (x:y:xs) = ...  -- hint: you can use and auxiliar acc which holds the lowest value and keep recursion till find a y - x differece greater than 1.

您也可以使用 fold 等... 获得一个非常简单的观点