【问题标题】:Tail recursive way to sort and merge two lists尾递归方式对两个列表进行排序和合并
【发布时间】:2019-11-05 00:06:11
【问题描述】:

我正在尝试编写一种尾递归方式来将两个排序列表合并为一个排序列表。

这就是我所拥有的。首先我有非尾递归方式

merge2 :: Ord a => [a] -> [a] -> [a]
merge2 l1 [] = l1
merge2 [] l2 = l2
merge2 (x:xs) (y:ys) | x > y = y : merge2 (x:xs) ys
                     | x < y = x : merge2 xs (y:ys)
                     | otherwise = x : merge2 (x:xs) ys

mergeTail :: Ord a => [a] -> [a] -> [a]
mergeTail accum [] = accum
mergeTail accum (x:xs) = mergeTail (x:accum) xs

当我输入类似 merge2Tail [1,2] [2,3,4] 的内容时,我希望得到 [1,2,2,3,4] 作为输出,但我以随机顺序得到它。我不确定在哪里或如何实现一个检查顺序的案例,同时保持它的尾递归。

【问题讨论】:

  • 你知道如何以非尾递归的方式来做吗?
  • 是的,我应该编辑并包含它吗?
  • 这听起来相当低效。为什么你希望它是尾递归的?
  • @Larry.Fish 那么你需要三个参数,l1l2 以及累加器。
  • 是的,基于累加器的解决方案使用带有附加参数的辅助函数。最初,它可以是一个空列表。递归时,将您希望“输出”到累加器的元素前置。最后,累加器将以相反的顺序包含想要的列表。

标签: list haskell


【解决方案1】:

正如其他人所指出的,这是一个高度人为的练习:将两个排序列表合并为一个排序列表应该在 Haskell 中以递归方式完成。但是,如果您在调用堆栈大小非常有限且不支持尾递归模 cons 的语言环境中构建 strict-spined 列表,那么这可能是一个合理的做法。在这样的环境下,通常最好把这样的问题分成两部分:

  1. 遍历列表,反向构建一个列表作为累加器。

  2. 使用累加器构建最终结果。

让我们开始吧。

merge :: Ord a => [a] -> [a] -> [a]
merge = \xs ys -> go [] xs ys
  where
    go acc [] ys = reverse acc ++ ys
    go acc xs [] = reverse acc ++ xs
    go acc xss@(x : xs) yss@(y : ys)
      | x <= y = go (x : acc) xs yss
      | otherwise = go (y : acc) xss ys

存在效率问题(即使在上述情况下):reverse 完全重建其参数,++ 完全重建其 first 参数。所以reverse acc ++ ys 重建acc 两次(如果++ 也被尾递归编写,它将被重建三次)。让我们解决这个问题。

-- reverseOnto xs ys = reverse xs ++ ys
reverseOnto :: [a] -> [a] -> [a]
reverseOnto [] ys = ys
reverseOnto (x : xs) ys = reverseOnto xs (x : ys)

最后,

merge :: Ord a => [a] -> [a] -> [a]
merge = \xs ys -> go [] xs ys
  where
    go acc [] ys = acc `reverseOnto` ys
    go acc xs [] = acc `reverseOnto` xs
    go acc xss@(x : xs) yss@(y : ys)
      | x <= y = go (x : acc) xs yss
      | otherwise = go (y : acc) xss ys

我相信这是在你的限制下你能做的最好的事情。

【讨论】:

  • 顺便说一句,reverseOnto 在 Lisps 中非常常见,我一直认为甚至有一个内置的,revappend 或其他东西;或者应该是,如果没有的话。 (或者我们当然可以在 Haskell 中使用 flip $ foldl' (flip (:)))。 :)
【解决方案2】:

首先,让我们看看非尾递归的样子:

merge :: Ord a => [a] -> [a] -> [a]
merge xs []                                 = xs
merge [] ys                                 = ys
merge fullXs@(x:xs) fullYs@(y:ys)  | x <= y = x : merge xs fullYs
                                   | x > y  = y : merge fullXs ys

要实现尾递归,您必须在某处有一个accum,并调用一个子函数助手。 由于是尾递归,所以必须在末尾添加元素才能保持顺序:

看看foldl会发生什么:

foldl (flip (:)) [] [1,2,3,4,5]
=> [5,4,3,2,1]

所以函数将是:

mergeTail :: Ord a => [a] -> [a] -> [a]
mergeTail xs [] = xs
mergeTail [] ys = ys
mergeTail xs ys = mergeAccum [] xs ys


mergeAccum :: Ord a => [a] -> [a] -> [a] -> [a]
mergeAccum acc [] []                       = acc
mergeAccum acc [] (y:ys)                   = mergeAccum (acc ++ [y]) [] ys
mergeAccum acc (x:xs) []                   = mergeAccum (acc ++ [x]) xs []
mergeAccum acc (x:xs) (y:ys)  | x <= y     = mergeAccum (acc ++ [x]) xs (y:ys)
                              | x > y      = mergeAccum (acc ++ [y]) (x:xs) ys

ejemlo:

$> mergeTail [1,2,6] [1,2,3,5]
=> [1,1,2,2,3,5,6]

注意:

在这种情况下,这个函数效率很低,因为它必须在每次递归调用时都到accum 列表的末尾。

【讨论】:

  • 这种情况的标准补救措施是反向构建累加器,使用 高效 (:),并在返回之前反转基本情况中的累加结果。跨度>
  • @WillNess 我不太明白你的概念,它应该是怎样的?我不知道
  • 您将递归子句中的mergeAccum (acc ++ [y]) ... 更改为mergeAccum (y : acc) ...,并将基本情况子句... = acc 更改为... = reverse acc。 :)
猜你喜欢
  • 2022-10-04
  • 2015-06-05
  • 2018-01-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-17
相关资源
最近更新 更多