【问题标题】:How do I take the last n elements of a list如何获取列表的最后 n 个元素
【发布时间】:2013-06-19 14:59:08
【问题描述】:

要获取列表xs 的最后一个n 元素,我可以使用reverse (take n (reverse xs)),但这不是很好的代码(它在返回任何内容之前将完整列表保留在内存中,并且结果不共享与原始列表)。

如何在 Haskell 中实现 lastR 函数?

【问题讨论】:

  • 从未尝试过 Haskell 但知道 OCaml 并且有一句话让我印象深刻:如何与原始列表共享结果? Haskell 中的对象不是不可变的吗? (与大多数函数式语言一样)
  • @Boris 是的。但是当您执行drop 1 xs 之类的操作时,会从列表中删除第一个元素。 Haskell 通常不会创建一个少 1 个元素的新列表,而是通过将指针更改为指向原始列表的第二个元素来优化这一点。
  • 感谢您的澄清。事实上,我一定是累了 ;-) 确实,它 - 因为 - 我猜你可以做这个优化是不可变的:共享地址仍然是安全的,并且保留了按值复制的语义,因为你知道对象无论如何都不会改变。如果我错了,请纠正我,多年来没有玩过这些东西。
  • tailn n xs = drop (length xs - n) xs ---- 把你不想要的从列表的前面拿走,留下你想要的,列表的其余部分.
  • 好奇:\n xs -> [x | [x] <- transpose [drop n xs, xs]]。分享:(\n xs -> foldr (\_ r (_:z) -> r z) id (drop n xs) xs)(与 Davorak 的foldl' 解决方案相同)

标签: list haskell


【解决方案1】:

这应该具有仅迭代列表长度一次的属性。 N 代表drop n,n - 1 代表 zipLeftover。

zipLeftover :: [a] -> [a] -> [a]
zipLeftover []     []     = []
zipLeftover xs     []     = xs
zipLeftover []     ys     = ys
zipLeftover (x:xs) (y:ys) = zipLeftover xs ys

lastN :: Int -> [a] -> [a]
lastN n xs = zipLeftover (drop n xs) xs

这是一个更短的替代方案,也许更好,因为正如 Satvik 指出的那样,使用递归运算符通常比使用显式递归更好。

import Data.Foldable

takeLeftover :: [a] -> t -> [a]
takeLeftover [] _ = []
takeLeftover (x:xss) _ = xss

lastN' :: Int -> [a] -> [a]
lastN' n xs = foldl' takeLeftover xs (drop n xs)

还要注意 Will Ness 在下面的评论 takeLeftover 只是:

takeLeftover == const . drop 1

这让事情变得相当整洁:

lastN' :: Int -> [a] -> [a]
lastN' n xs = foldl' (const . drop 1) xs (drop n xs)
-- or
-- lastN' n xs = foldl' (const . drop 1) <*> drop n

【讨论】:

  • 这就是我一直在寻找的那个,请参阅joachim-breitner.de/blog/archives/… 以了解对该问题的长期处理。注意zipLeftover可以减少病例数,但这只是化妆品。
  • 感谢您的链接,我会检查一下。如果我对选择犹豫不决并且不想花太多时间考虑它,我默认是明确的,因此是额外的情况。
  • 我不确定这个变体是否真的很好;通常 foldl 有堆栈溢出问题。也许您打算使用 foldr?
  • 是的,第二个变种不好。比较lastN 1 [1..](无限循环,但不分配显着内存)和lastN' 1 [1..](很快填满堆)。
  • 是的,但那是因为我忘了严格要求。我应该使用foldl' 而不是懒惰的foldl。使用 foldl' 时,lastN'lastN 上的内存配置文件至少相同,最多 100M 个元素。
【解决方案2】:

据我所知,你可以使用类似的东西

lastN :: Int -> [a] -> [a]
lastN n xs = drop (length xs - n) xs

但是对于内置列表中的任何实现,您的性能都不会比O(length of list - n) 好。

您似乎正在尝试将 list 用于某些原本不能有效执行的操作。 使用Data.Sequence 或其他一些允许在列表末尾高效执行操作的列表实现。


编辑:

Davorak 的实现看起来是您可以从内置列表中获得的最有效的实现。但请记住,除了单个函数的运行时间之外,还有其他复杂的问题,比如它是否与其他函数融合得很好。

Daniel 的解决方案使用内置函数,具有与 Davorak 相同的复杂性,我认为与其他函数融合的机会更大。

【讨论】:

  • "如是否与其他功能融合良好等"你能评论一下丹尼尔的功能使它更容易融合吗?或者更确切地说,是什么暗示可能会如此。
  • @Davorak 我不是融合方面的专家,但是使用内置函数编写的函数更有可能融合。我认为 Daniel 可以对此发表评论并提供更多信息。
  • 谢谢,这也是我的印象,特别是使用递归运算符而不是显式递归,因为融合重写规则是基于运算符的。
  • 我不明白为什么这不是答案,这么容易理解和实施,也应该是前奏的一部分。
  • Fusion 在这里对您没有帮助,因为该列表被使用了两次。这种实现严格来说效率较低。有多少数据保持活动状态对于垃圾收集性能非常重要。当Int 参数较小时,这会在计算过程中保持更多的列表处于活动状态。
【解决方案3】:

不确定它是否非常快,但很容易:

lastR n xs = snd $ dropWhile (not . null . fst) $ zip (tails $ drop n xs) (tails xs)

【讨论】:

    【解决方案4】:

    请注意,无论您做什么,都需要遍历整个列表。也就是说,您可以比reverse (take n (reverse xs)) 做得更好,方法是先计算列表的长度,然后删除适当数量的元素:

    lastN :: Int -> [a] -> [a]
    lastN n xs = let m = length xs in drop (m-n) xs
    

    【讨论】:

    • 这不是很好,因为它保留内存的时间超过了必要的时间。特别是,即使在计数超过 n 个元素后,它仍保留在列表的开头。两指针解决方案避免了这种情况。
    【解决方案5】:

    这是对 Davorak 的第一个解决方案的简化:

    -- dropLength bs = drop (length bs)
    dropLength :: [b] -> [a] -> [a]
    dropLength [] as = as
    dropLength _ [] = []
    dropLength (_ : bs) (_ : as) = dropLength bs as
    
    lastR :: Int -> [a] -> [a]
    lastR n as = dropLength (drop n as) as
    

    n &lt;= length aslength (drop n as) = length as - n,所以dropLength (drop n as) as = drop (length (drop n as)) as = drop (length as - n) as,这是as的最后一个n元素。当n &gt; length asdropLength (drop n as) as = dropLength [] as = as,这是唯一明智的答案。

    如果你想使用折叠,你可以写

    dropLength :: [b] -> [a] -> [a]
    dropLength = foldr go id
      where
         go _b _r [] = []
         go _b r (_a : as) = r as
    

    这对lastR 没有任何影响,但在其他应用程序中它可以为您赢得一些列表融合。

    【讨论】:

      【解决方案6】:

      简单的解决方案还不错。无论如何,算法都是 O(n)。

      takeLastN n = reverse . take n . reverse
      

      时间对比:

      > length $ lastN 3000000 (replicate 10000000 "H") -- Davorak's solution #1
      3000000
      (0.88 secs, 560,065,232 bytes)
      > length $ lastN' 3000000 (replicate 10000000 "H") -- Davorak's solution #2
      3000000
      (1.82 secs, 840,065,096 bytes)
      > length $ lastN'' 3000000 (replicate 10000000 "H") -- Chris Taylor's solution
      3000000
      (0.50 secs, 560,067,680 bytes)
      > length $ takeLastN 3000000 (replicate 10000000 "H") -- Simple solution
      3000000
      (0.81 secs, 1,040,064,928 bytes)
      

      正如Joachim Breitner 在问题和评论中指出的那样,仍然存在内存问题。不比其他解决方案慢多少,这样的解决方案需要几乎两倍的内存。您可以在基准测试中看到这一点。

      【讨论】:

      • 请注意,您失去了共享:您知道内存中有两份后缀(spine of),一份作为原始列表的一部分,一份在返回的列表中。
      • @JoachimBreitner 编辑提及这一点。
      • 这也会分配整个反向列表,当它可能只使用一部分时。
      • 究竟什么是O(n)?正确的take 2 $ lastN 10000000 [1..10000000]O(1),你的确实是O(n)。差别很大。
      • @WillNess 在O(1) 中无法到达单链表的末尾。你写的代码也是O(n)。我提出的解决方案可能存在性能问题,但从算法复杂度的角度来看,它并不比其他解决方案差。
      【解决方案7】:
      takeLast :: Int -> [a] -> [a]
      takeLast n xs 
       | n < 1 = []
       | otherwise = let s = splitAt n xs in bla (fst s) (snd s)
       where 
        bla xs [] = xs
        bla (x:xs) (y:ys) = bla (xs ++ [y]) ys
      

      【讨论】:

      • 任何在循环中带有xs ++ [y] 的代码肯定不是O(n)
      • 但我没有遍历列表一次吗?
      • 但每一步的大小并不固定。
      猜你喜欢
      • 2010-10-30
      • 2022-11-15
      • 1970-01-01
      • 1970-01-01
      • 2018-08-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多