如何有效地实现列表数据结构,其中我可以在列表的头部和末尾有2个视图,这总是指向列表的尾部而没有昂贵的反向调用。 即:
start x = []
end x = reverse start -- []
start1 = [1,2,3] ++ start
end start1 -- [3,2,1]
end应该能够在不调用'reverse'的情况下执行此操作,而只是从列表的角度自动反向查看给定列表。如果我从连接开始创建新列表,那么同样应该成立。
答案 0 :(得分:35)
您可以随时使用Data.Sequence
。
或者,纯函数队列的一个众所周知的实现是使用两个列表。一个用于入队,另一个用于出队。入队将简单地纳入入队名单。 Dequeue占据了出队名单的头部。当出列列表短于入队列表时,通过反转入队列表重新填充它。见Chris Okasaki的Purely Functional Datastructures.
即使此实现使用reverse
,此摊销的时间成本也是渐进式无关紧要的。它的工作原理是,对于每个入队,你都会为出列表重新填充产生Θ(1)的时间债务。因此,出队的预期时间最多是入队时间的两倍。这是一个常数因子,因此两种操作的最坏情况成本是O(1)。
答案 1 :(得分:5)
Data.Dequeue您要找的是什么?
(它没有reverse
,但您可以非常轻松地添加它并向作者发送补丁。)
答案 2 :(得分:5)
当我用Google Haskell queue
进行搜索时,此问题在首页上显示为第三个结果,但先前提供的信息具有误导性。因此,我觉得有必要澄清一些事情。 (第一个搜索结果是一篇博客文章,其中包含一个粗心的实现...)
下面的所有内容基本上都来自于Okasaki 1995年的论文Simple and efficient purely functional queues and deques或他的book。
好的,让我们开始吧。
具有摊销的 O(1)时间复杂度的持久队列实现是可能的。诀窍是只要前部分足够长以摊销reverse
操作的成本,就可以反转代表队列后部分的列表。因此,当前部比后部短时,我们将其反转,而不是在前部为空时翻转后部。以下代码摘自冈崎书记的附录
data BQueue a = BQ !Int [a] !Int [a]
check :: Int -> [a] -> Int -> [a] -> BQueue a
check lenf fs lenr rs =
if lenr <= lenf
then BQ lenf fs lenr rs
else BQ (lenr+lenf) (fs ++ reverse rs) 0 []
head :: BQueue a -> a
head (BQ _ [] _ _) = error "empty queue"
head (BQ _ (x:_) _ _) = x
(|>) :: BQueue a -> a -> BQueue a
(BQ lenf fs lenr rs) |> x = check lenf fs (lenr + 1) (x:rs)
tail :: BQueue a -> BQueue a
tail (BQ lenf (x:fs) lenr rs) = check (lenf-1) fs lenr rs
为什么为什么摊销的 O(1)甚至永久使用 ? Haskell很懒,因此reverse rs
直到需要时才真正发生。要强制reverse rs
,在到达fs
之前必须采取 | reverse rs
| 步骤。如果我们在达到暂停状态tail
前重复reverse rs
,那么结果将被记忆,因此第二次仅需 O(1)。另一方面,如果我们在放置暂停fs ++ reverse rs
之前使用该版本,那么它再次必须经过fs
步才能达到reverse rs
。冈崎的书中使用(修改的)Banker方法进行了正式证明。
@Apocalisp的答案
出队列表为空时,通过反转入队列表重新填充
是他书第5章中的实现,一开始就带有警告
不幸的是,本章介绍的简单的摊销视图因存在持久性而中断
冈崎在第6章中描述了他摊销的 O(1)持久队列。
到目前为止,我们仅讨论摊销时间的复杂性。实际上,可以完全消除摊销以实现持久队列的最坏情况 O(1)时间复杂度。诀窍是每次调用de / enqueue时都必须递增强制reverse
。但是,实际的实现在这里很难解释。
同样,一切都已经在他的论文中了。
答案 3 :(得分:1)
我不是真正的Haskell用户,但我发现a blog post声称描述了一个可以在摊还的常数时间内操作的Haskell队列。它基于Chris Okasaki优秀的纯功能数据结构的设计。