我需要从列表中获取最后n
个元素,使用O(n)
内存,所以我写了这段代码
take' :: Int -> [Int] -> [Int]
take' n xs = (helper $! (length $! xs) - n + 1) xs
where helper skip [] = []
helper skip (x : xs) = if skip == 0 then xs else (helper $! skip - 1) xs
main = print (take' 10 [1 .. 100000])
此代码需要O(|L|)
内存,其中|L|
- 是给定列表的长度。
但是当我写这段代码时
take' :: Int -> [Int] -> [Int]
take' n xs = helper (100000 - n + 1) xs
where helper skip [] = []
helper skip (x : xs) = if skip == 0 then xs else (helper $! skip - 1) xs
main = print (take' 10 [1 .. 100000])
此代码现在只占用O(n)
个内存(唯一的chage是(helper $! (length $! xs) - n + 1)
- > helper (100000 - n + 1)
)
所以,据我所知,Haskell由于某种原因在第一次调用length xs
之前没有评估helper
所以它在skip
中留下了一个thunk并且haskell必须保留这个值在每个堆栈帧中而不是进行尾递归。但在第二段代码中,它会评估(100000 - n + 1)
并将纯值提供给helper
。
所以问题是如何在第一次调用helper之前评估列表的长度,并且只使用O(n)
内存。
答案 0 :(得分:8)
other answer提到成为一个好消费者意味着什么。您已经发布了两个版本的函数,一个适用于任意长度的列表,但不是一个好的消费者,一个是一个很好的消费者,但假定一个特定的列表长度。为了完整性,这里有一个功能很好的消费者,适用于任意列表长度:
takeLast n xs = go (drop n xs) xs where
go (_:xs) (_:ys) = go xs ys
go _ ys = ys
答案 1 :(得分:5)
第二个版本实际上并不只占用 O ( n )内存。无论take'
做什么:你从一个长度为 L 的列表开始,并且必须存储在某个地方。
有效 O ( n )内存的原因是该列表仅供一位“好消费者”使用,即helper
。这样的消费者从头到尾解构清单;因为在其他任何地方都不需要对头部的引用,垃圾收集器可以立即开始清理那些第一个元素 - 在列表理解之前甚至已经建立了列表的其余部分!
但是,如果在使用helper
之前,您会计算该列表的length
。这已经强制整个列表为NF'd †,正如我所说,这不可避免地需要 O ( L )内存。因为你仍然持有一个与helper
一起使用的引用,在这种情况下,垃圾收集器可以不在整个列表在内存之前采取任何行动。
所以,它与严格的评估无关。事实上,实现目标的唯一方法是使 less 严格(只需要在任何给定时间评估长度 n 的子列表)。
† 更准确地说:它强制列表的 spine 为普通形式。不评估元素,但它仍然 O ( L )。