Haskell中的高效队列

时间:2009-11-16 02:04:59

标签: algorithm data-structures haskell list queue

如何有效地实现列表数据结构,其中我可以在列表的头部和末尾有2个视图,这总是指向列表的尾部而没有昂贵的反向调用。 即:

start x = []
end x = reverse start -- []
start1 = [1,2,3] ++ start
end start1 -- [3,2,1]

end应该能够在不调用'reverse'的情况下执行此操作,而只是从列表的角度自动反向查看给定列表。如果我从连接开始创建新列表,那么同样应该成立。

4 个答案:

答案 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

好的,让我们开始吧。

  1. 具有摊销的 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
    
  2. 为什么为什么摊销的 O(1)甚至永久使用 ? Haskell很懒,因此reverse rs直到需要时才真正发生。要强制reverse rs,在到达fs之前必须采取 | reverse rs | 步骤。如果我们在达到暂停状态tail前重复reverse rs,那么结果将被记忆,因此第二次仅需 O(1)。另一方面,如果我们在放置暂停fs ++ reverse rs之前使用该版本,那么它再次必须经过fs步才能达到reverse rs。冈崎的书中使用(修改的)Banker方法进行了正式证明。

  3. @Apocalisp的答案

      

    出队列表为空时,通过反转入队列表重新填充

    是他书第5章中的实现,一开始就带有警告

      

    不幸的是,本章介绍的简单的摊销视图因存在持久性而中断

    冈崎在第6章中描述了他摊销的 O(1)持久队列。

  4. 到目前为止,我们仅讨论摊销时间的复杂性。实际上,可以完全消除摊销以实现持久队列的最坏情况 O(1)时间复杂度。诀窍是每次调用de / enqueue时都必须递增强制reverse。但是,实际的实现在这里很难解释。

同样,一切都已经在他的论文中了。

答案 3 :(得分:1)

我不是真正的Haskell用户,但我发现a blog post声称描述了一个可以在摊还的常数时间内操作的Haskell队列。它基于Chris Okasaki优秀的纯功能数据结构的设计。