【问题标题】:Reverse a list in haskell反转haskell中的列表
【发布时间】:2015-01-06 22:58:07
【问题描述】:

我正在尝试反转列表。

以下是我的代码:

reverseList :: [Int] -> [Int]
reverseList [] = []
reverseList (x:xs) =  x:reverseList xs

最终发生的事情是我最终以相同的顺序返回列表。我什至有一个如何反转列表的解决方案,但我想了解我在这里做错了什么?我对haskell很陌生,所以我认为我应该更多地专注于理解,然后我才能轻松解决更多问题。我知道这个问题有很多解决方案,但我需要更多帮助来理解尤其是我在这段代码中做错了什么。

【问题讨论】:

  • 从这里学到的一个教训是,你可以做1:[2,3,4],但你不能[2,3,4]:1。此数组构造指令中最左边的项要求该项是元素而不是数组。这就是 Haskell 在这种情况下的方式,也是像我这样的新手认为内化和习惯非常重要的核心概念。

标签: haskell functional-programming


【解决方案1】:

您将列表分成头部和尾部,然后以相同的顺序重新组合列表。以列表[1, 2, 3] 为例:

在第一次调用中,x 将是 1xs 将是 [2, 3]。然后创建一个新列表,由前面的x(so 1)和reverseList [2, 3]组成。

【讨论】:

  • 我的错,对不起。已更正。
  • 我不是在做同样的事情,因为当我取出头部时,我首先插入头部,例如 x:reverseList xs
【解决方案2】:

在 Haskell 中有几种方法可以解决这个问题。天真的方法是使用连接函数++

reverseList [] = []
reverseList (x:xs) = reverseList xs ++ [x]

但是,对于大型列表,这将非常慢,因为 Haskell 列表实际上是单链表,因此为了附加元素,您必须遍历整个列表。另一种方法是跟上您在辅助函数中构建的列表:

reverseList = go []
    where
        go acc [] = acc
        go acc (x:xs) = go (x:acc) xs

但是,这实际上只是 fold 模式:

reverseList = foldl (\acc x -> x : acc) []

但是\acc x -> x : acc只是flip (:),所以可以写成

reverseList = foldl (flip (:)) []

但是,最简单的方法可能是只使用 Prelude 中的 reverse 函数。

我想指出,您的reverseList :: [Int] -> [Int] 类型可以泛化为:: [a] -> [a],您无需对列表元素做任何特殊处理,您只是用它们构建一个新列表。

【讨论】:

  • 为了完整性,懒惰的形式foldl'更好。
  • @PierreR 你不是说严格的形式吗?而且我知道,我只是避免提及额外的导入和两者之间的差异,这是我在 SO 上解释过太多次的事情;)
  • 是的……当然严格。我毫不怀疑你知道这件事。您的 SO 答案是如此宝贵的帮助。感谢您抽出宝贵时间。
  • 您的解决方案比使用串联的解决方案更好 (++)。该解决方案使用 $O(n^2)$ 时间进行连接,并使用 $O(n)$ 堆栈空间,因为每个连接都需要左侧部分的结果来评估为弱头范式。您的解决方案仍然在 $O(n)$ 堆栈空间中运行,因为 foldl 必然会建立一个大小为 $O(n)$ 的 thunk 以评估为弱头正常形式。更好的解决方案确实是使用严格的foldl',因为在这种情况下它可以在恒定的堆栈空间中运行。
  • @Sapphire_Brick 是乳胶表示法,表示数学公式,类似于表示代码的反引号。
【解决方案3】:

在 Haskell 中有几种方法可以解决这个问题。这里有一个带有 cons 和 last/init 的解决方案:

reverseList  [] = []
reverseList  xs = last xs : reverseList (init xs)

或者用 foldl:

reverseList xs = foldl (\x y -> y:x) [] xs 

【讨论】:

  • 您将这些作为两种选择,但只有一种是有效的解决方案!您可能应该解释为什么会这样。
  • 我对此表示赞同,因为作为 Haskell 的新手,我使用 lastinit 得出了相同的解决方案。它可能不是最有效的,但它加强了我对 Haskell 的理解。
  • 第二种选择实际上并不比第一种更有效。函数 foldl 在 list 参数中是严格的,并在它之上递归。这意味着为了将其结果评估为弱头范式,必须首先遍历整个列表。这很好,但是这种遍历会导致构建一个大小与列表大小成正比的 thunk,对其求值可能会导致堆栈溢出。这是因为foldl 对累加值不严格。 thunk 可以通过使用 foldl' 强制为 WHNF,这是累积值中的结构。
  • @justinpc,你在两个方面错了。你的主要错误是认为第二个和第一个一样糟糕。事实上,第二个需要 linear 时间,而第一个需要 quadratic 时间。如果您不相信我,请尝试使用一百万个元素进行一些测试,使用reverse 的每个实现打印last . rev $ [1..10^6]。如果你不明白为什么,我可以试着解释一下。第二件事是,如果启用了优化,那么 GHC 绝对不会为 foldl 实现构建任何多余的 thunk。
【解决方案4】:

基本上是使用附加的朴素算法

revList [] = []
revList (x:xs) = revList xs ++ [x]

效率低,因为追加是O(n) 操作,其中n++ 操作符的第一个(左)参数的长度。所以上面的revList 函数原来是 O(n(n-1)/2) ~ O(n^2)。

因此对于此类附加繁重的任务,有差异列表数据类型。

差异列表只是表示为函数的列表。我的意思是,像 [1,2,3] 这样的列表在以 DList 类型表示时将是 \xs -> [1,2,3] ++ xs 或简而言之 ([1,2,3] ++)

type DList a = [a] -> [a]

toDList :: [a] -> DList a
toDList xs  = (xs ++ )

fromDList :: DList a -> [a]
fromDList f = f []

这有点酷,因为由于 DList 是函数,我们可以通过组合 (.) 运算符附加它们并获得一个新的 DList。换句话说toDList (xs ++ ys) == (toDList xs) . (toDList ys)

那么这有什么用呢?通过使用嵌套函数组合,我们可以以与revList 函数类似的方式反转我们的列表,但它会花费更少。只有 O(n),因为每个函数组合都是 O(1)。

revList' :: [a] -> DList a
revList' []     = toDList []
revList' (x:xs) = revList' xs . toDList [x]

现在我们在 DList a 类型中已经有了反向的 [a],我们需要应用 fromDList

fastReverse :: [a] -> [a]
fastReverse = fromDList . revList'

差异列表数据类型并不像我上面显示的那么简单。它可以有 Monoid、Functor 和 MonadT 实例。有关此有用数据类型的更多信息,请查看Data.DList

【讨论】:

  • 不完全(即使不正确)。每个函数组合确实是 O(1),但是我们有什么保证组合函数完成它的工作需要多长时间?完整答案是here。 :)
【解决方案5】:

简单。使用内置的reverse函数:

print (reverse [1,2,3,4,5]) -- [5,4,3,2,1]

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-08-05
    相关资源
    最近更新 更多