【问题标题】:Efficient queue in HaskellHaskell 中的高效队列
【发布时间】:2010-12-16 21:59:10
【问题描述】:

如何有效地实现一个列表数据结构,在该结构中我可以有 2 个视图来查看列表的开头和结尾,这些视图始终指向列表的开头和结尾,而无需进行昂贵的反向调用。 即:

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

end 应该能够在不调用“反向”的情况下执行此操作,而只需从列表自动反向的角度查看给定列表。如果我从串联创建新列表开始,也应该如此。

【问题讨论】:

  • 在 Haskell 中你不能改变值。 start 永远是空列表,end 永远是那个(空列表)的reverse。如果你想保持状态,你应该看看 State monad。
  • 更正:更新是指重新绑定。
  • @Absolute:你所说的并没有改变在 Haskell 中你不能改变事物的终极真理(尽管IO monad)。你不能重新绑定东西。
  • @Martinho:我的意思是在旧的基础上创建一个新的并将其分配给一个新名称,不知道为什么不清楚。
  • 强烈推荐 Apocalisp 提到的纯功能数据结构。那本书中有不少“啊哈”时刻。

标签: algorithm data-structures haskell list queue


【解决方案1】:

你总是可以只使用Data.Sequence

另外,一个众所周知的纯函数队列实现是使用两个列表。一个用于入队,另一个用于出队。入队只会与入队列表相反。出队占据出队列表的头部。当出队列表比入队列表短时,通过反转入队列表来重新填充它。请参阅 Chris Okasaki 的 Purely Functional Datastructures.

即使这个实现使用reverse,它的摊销时间成本渐近是微不足道的。这样一来,对于每个入队,您都会因出队列表重新填充而产生 Θ(1) 的时间债务。因此,出队的预期时间最多是入队的两倍。这是一个常数因子,因此两种操作的最坏情况成本都是 O(1)。

【讨论】:

  • 如果临时使用它,它是微不足道的。如果您的应用程序依赖于持久性,那么每次访问队列时都完全有可能遇到这种 O(n) 情况。
  • @Juliet 使用惰性和记忆化,在保持持久性的同时,摊销界限仍然是 O(1)
  • @Yuhta 你能举一个使用惰性和记忆的例子吗?
  • @CMCDragonkai 不是一个真正的例子,但你可以考虑这样的事情:持久使用不可变队列会导致 O(n) 摊销绑定的原因是因为多个线程会反转相同的后退列表多次排队。但是如果我们有一些机制来记忆反转的结果,那么在第一个线程反转返回列表后,其余线程只需要检索第一个的结果,而无需支付反转的代价。
  • 我确实认为需要一个例子,在所有情况下都有缓存工作似乎很不简单。例如,如果您有 Deque a 即将在下一次弹出时需要反转列表,那么插入一个新元素不会改变这一事实,但在任何简单的实现中它都会破坏缓存(因为您实际上是在操作在缓存的反向列表的尾部,即 O(n))。所以你可以插入 n 个元素来创建 n 个不同的Deques,然后从它们中全部弹出。有了足够聪明的实现,它可能会奏效,但可能并不容易。
【解决方案2】:

当我 google Haskell queue 时,此问题显示为第一页上的第三个结果,但之前提供的信息具有误导性。所以,我觉得有必要澄清一些事情。 (第一个搜索结果是一篇博客文章,其中包含一个粗心的实现......)

以下内容基本上来自冈崎的论文,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,它必须在到达reverse rs 之前采取|fs| 步骤。如果我们在达到暂停reverse rs 之前重复tail,那么结果将被记住,所以第二次只需要O(1)。另一方面,如果我们在放置fs ++ reverse rs 之前使用该版本,那么它必须再次通过fs 步骤才能到达reverse rs。冈崎的书中使用(修改的)银行家方法的正式证明。

  3. @Apocalisp 的回答

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

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

    不幸的是,本章介绍的简单摊销观点在持久性存在的情况下打破了

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

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

再一次,一切都已经在他的论文中了。

【讨论】:

    【解决方案3】:

    Data.Dequeue 是您要找的吗?

    (它没有reverse,但您可以很容易地添加它并将补丁发送给作者。)

    【讨论】:

    • 我知道这个库,我只是想自己实现它作为一个有趣的思想实验。
    • 也可以使用标准库中已有的Data.Sequence模块
    • 或者......他们可以自己实现它作为一个有趣的思想实验,就像他们确定他们想做的那样。这并没有错。
    【解决方案4】:

    我并不是真正的 Haskell 用户,但我发现 a blog post 声称描述了一个 Haskell 队列,可以在摊销的常数时间内对其进行操作。它基于 Chris Okasaki 出色的纯函数式数据结构的设计。

    【讨论】:

      猜你喜欢
      • 2013-10-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-01-23
      • 2018-12-29
      相关资源
      最近更新 更多