【问题标题】:subsequences of length n from list performance列表性能中长度为 n 的子序列
【发布时间】:2020-01-27 21:57:14
【问题描述】:

我实现了这个答案的一个版本https://stackoverflow.com/a/9920425/1261166(我不知道回答的人的意图)

sublistofsize 0 _        = [[]]
sublistofsize _ []       = []
sublistofsize n (x : xs) = sublistsThatStartWithX ++ sublistsThatDontStartWithX
  where sublistsThatStartWithX = map (x:) $ sublistofsize (n-1) xs
        sublistsThatDontStartWithX = sublistofsize n xs

我不确定的是sublistsThatStartWithX = map (x:) $ sublistofsize (n-1) xs

我假设 map (x:) 在性能方面给出了问题,但不确定如何解决它。我已经对print $ length $ sublistofsize 5 $ primesToTakeFrom 50进行了分析

COST CENTRE                                  MODULE                                        no.     entries  %time %alloc   %time %alloc
sublistofsize                             Main                                          112     4739871   46.9   39.9    96.9  100.0
 sublistofsize.sublistsThatDontStartWithX Main                                          124     2369935    2.2    0.0     2.2    0.0
 sublistofsize.sublistsThatStartWithX     Main                                          116     2369935   47.8   60.1    47.8   60.1

我是否以一种好的方式实施它? 有没有更快的方法?

【问题讨论】:

  • 您测量过性能问题吗?这个问题基本上是输出大小的线性问题,map 不会改变这一点。
  • 我的想法是 map (x:) 使 x 挂起并等待递归调用的返回值,或者我错了......?
  • 没有关系,因为 Haskell 很懒惰,但即使有,又有什么关系呢?这项工作必须在某个时候完成。
  • 由于我不太擅长使用haskell 并寻找性能问题,我的猜测是那将是问题所在,也许是尾递归,我不知道。我制作了另一个更快的函数,它使用列表理解,但我的猜测是这会更快,因为我做了很多其他的事情,比如守卫,而且我在另一个版本中对素数没有限制(它检查所有( !)组合)
  • 我认为您需要更清楚您的问题实际上是什么 - 例如是关于为什么与您的其他代码存在性能差异(如果有,请提供其他代码和测量的详细信息),是否有更快的方法来编写上述代码,还是什么?

标签: performance haskell


【解决方案1】:

我假设 map (x:) 在性能方面给出了问题

没有。 map 编码效率高,运行在线性时间,这里没有问题。

但是,您的递归可能是个问题。你们都在调用 sublistofsize (n-1) xs 和 sublistofsize n xs,它们 - 给定一个开始列表 sublistofsize m (_:_:ys) - 确实评估术语 sublistofsize (m-1) ys 两次,因为它们之间在不同的递归步骤中没有共享。

所以我会应用动态规划来得到

subsequencesOfSize :: Int -> [a] -> [[a]]
subsequencesOfSize n xs = let l = length xs
                          in if n>l then [] else subsequencesBySize xs !! (l-n)
 where
   subsequencesBySize [] = [[[]]]
   subsequencesBySize (x:xs) = let next = subsequencesBySize xs
                             in zipWith (++) ([]:next) (map (map (x:)) next ++ [[]])

并不是说添加空列表是最漂亮的解决方案,但是您可以看到我如何将 zipWith 与移位列表一起使用,以便 next 的结果被使用两次 - 一次直接在长度为 n 并且在长度为 n+1 的子序列列表中出现一次。

在 GHCI 中使用 :set +s 对其进行测试,您会发现这比简单的解决方案要快得多:

*Main> length $ subsequencesOfSize 7 [1..25]
480700
(0.25 secs, 74132648 bytes)
(0.28 secs, 73524928 bytes)
(0.30 secs, 73529004 bytes)
*Main> length $ sublistofsize 7 [1..25] -- @Vixen (question)
480700
(3.03 secs, 470779436 bytes)
(3.35 secs, 470602932 bytes)
(3.14 secs, 470747656 bytes)
*Main> length $ sublistofsize' 7 [1..25] -- @Ganesh
480700
(2.00 secs, 193610388 bytes)
(2.00 secs, 193681472 bytes)
*Main> length $ subseq 7 [1..25] -- @user5402
480700
(3.07 secs, 485941092 bytes)
(3.07 secs, 486279608 bytes)

【讨论】:

    【解决方案2】:

    对于该问题,您的实现是自然的“Haskell-ish”。

    如果您最终使用了整个结果,那么在给定输出数据结构 ([[a]]) 的情况下,对于这个问题不会有任何渐近更快的速度,因为它运行的时间与输出的长度呈线性关系。

    map (x:) 的使用是在每个列表的开头添加元素的一种非常自然的方式,鉴于我们正在使用列表,因此不太可能有任何更快的选项。

    原则上,重复使用(++) 是低效的,因为它会导致每次调用左侧参数时都要遍历它,但在这种情况下,总成本应该只是一个额外的常数因素。

    您也许可以使用累加参数otherResults 来收集结果来改进它,但要进行此更改,您还需要以相反的顺序向下传递prefix 并在最后重新反转它,这很可能会耗尽积蓄:

    sublistofsize' 0 _        prefix otherResults = reverse prefix : otherResults
    sublistofsize' _ []       prefix otherResults = otherResults
    sublistofsize' n (x : xs) prefix otherResults =
       sublistofsize' (n-1) xs (x:prefix) (sublistofsize' n xs prefix otherResults)
    
    sublistofsize n xs = sublistofsize' n xs [] []
    

    【讨论】:

    • 谢谢,带累加器的速度大约快 2 倍(我将代码更改为不反转前缀,因为我不需要按顺序使用它
    • 实际上,我刚刚想到,首先反转输入列表而不是反转每个输出列表会更有效,至少如果您不关心结果的特定顺序。
    【解决方案3】:

    一个应该有帮助的优化是跟踪列表中是否有足够的元素来形成其余的子序列。这可以通过跟踪n-1-elements 在xs 之前的指针并在递归时推进它们来非常有效地完成。

    一个实现:

      nthtail 0 xs = xs
      nthtail _ [] = []
      nthtail n (x:xs) = nthtail (n-1) xs
    
      subseq 0 _ = [[]]
      subseq n xs =
        if null t
          then []
          else go n xs t
        where
          t = nthtail (n-1) xs  -- n should always be >= 1 here
          go 0 _ _  =  [[]]
          go _ _ [] = []
          go n xs@(x:xt) t = withx ++ withoutx
            where withx = map (x:) $ go (n-1) xt t
                  withoutx = go n xt (tail t)
    

    【讨论】:

    • nthtail = drop? :-)
    • @Bergi:唯一的区别是 nthtail 采用任何 n 即 (Num a),(如果提供浮点数是不正确的。
    • 支持Integer(和genericDrop一样)并且还匹配(x:xs)中的x,可以是(_:xs)
    【解决方案4】:

    这是一个 6 年前的话题,但我相信我有一个值得在这里分享的代码。

    @Bergi 接受的答案非常棒,但我仍然认为从两个方面来看这项工作可以做得更好;

    1. 虽然没有在任何规范中提及,但它会以相反的字典顺序返回组合。人们可能希望按字典顺序排列它们,因为大多数情况都是如此。
    2. 使用 C(n,n/2) 进行测试时,它们的性能相似,但使用 C(100,5) 进行测试时,以下代码速度更快,内存效率更高。

    .

    combinationsOf :: Int -> [a] -> [[a]]
    combinationsOf 1 as        = map pure as
    combinationsOf k as@(x:xs) = run (l-1) (k-1) as $ combinationsOf (k-1) xs
                                 where
                                 l = length as
    
                                 run :: Int -> Int -> [a] -> [[a]] -> [[a]]
                                 run n k ys cs | n == k    = map (ys ++) cs
                                               | otherwise = map (q:) cs ++ run (n-1) k qs (drop dc cs)
                                               where
                                               (q:qs) = take (n-k+1) ys
                                               dc     = product [(n-k+1)..(n-1)] `div` product [1..(k-1)]
    

    让我们将它们与接受答案下的测试用例进行比较。

    *Main> length $ subsequencesOfSize 7 [1..25]
    480700
    (0.27 secs, 145,572,672 bytes)
    
    *Main> length $ combinationsOf 7 [1..25]
    480700
    (0.14 secs, 95,055,360 bytes)
    

    让我们用 C(100,5) 等更难的东西来测试它们

    *Main> length $ subsequencesOfSize 5 [1..100]
    75287520
    (52.01 secs, 77,942,823,360 bytes)
    
    *Main> length $ combinationsOf 5 [1..100]
    75287520
    (17.61 secs, 11,406,834,912 bytes)
    

    【讨论】:

      猜你喜欢
      • 2021-03-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-11-15
      • 1970-01-01
      • 1970-01-01
      • 2023-03-31
      • 1970-01-01
      相关资源
      最近更新 更多