Haskell 功能阵列倍频堆栈的摊销

Haskell 功能阵列倍频堆栈的摊销,haskell,data-structures,functional-programming,amortized-analysis,Haskell,Data Structures,Functional Programming,Amortized Analysis,我一直在考虑紧凑型堆栈的概念,随着阵列大小的增加,其空间需求将接近阵列。候选结构: data Stack a = Empty | Zero (Stack a) | One !(SmallArray a) (Stack a) | Two !(SmallArray a) !(SmallArray a) (Stack a) -- Invariant: the array size at depth `n` is `2^n`. push :: a -> Stack a ->

我一直在考虑紧凑型堆栈的概念,随着阵列大小的增加,其空间需求将接近阵列。候选结构:

data Stack a
  = Empty
  | Zero (Stack a)
  | One !(SmallArray a) (Stack a)
  | Two !(SmallArray a) !(SmallArray a) (Stack a)
-- Invariant: the array size at depth `n` is `2^n`.

push :: a -> Stack a -> Stack a
push = pushA . pure

pushA :: SmallArray a -> Stack a -> Stack a
pushA sa Empty = One sa Empty
pushA sa (Zero more) = One sa more
pushA sa1 (One sa2 more) = Two sa1 sa2 more
pushA sa1 (Two sa2 sa3 more) = One sa1 (pushA (sa2 <> sa3) more)

pop :: Stack a -> Maybe (a, Stack a)
pop stk = do
  (sa, stk') <- popA stk
  hd <- indexSmallArrayM sa 0
  Just (hd, stk')

popA :: Stack a -> Maybe (SmallArray a, Stack a)
popA Empty = Nothing
popA (Zero more) = do
  (sa, more') <- popA more
  let !(sa1, sa2) = -- split sa in two
  Just (sa1, One sa2 more')
popA (One sa more) = Just (sa, Zero more)
popA (Two sa1 sa2 more) = Just (sa1, One sa2 more)
数据堆栈a
=空
|零(堆栈a)
|一个!(小型阵列a)(堆栈a)
|两个!(a)!(小型阵列a)(堆栈a)
--不变量:深度'n'处的数组大小为'2^n'。
推送::a->堆栈a->堆栈a
push=pushA。纯净的
pushA::SmallArray a->Stack a->Stack a
pushA sa Empty=一个sa Empty
pushA sa(零个以上)=一个以上sa
pushA sa1(一个sa2以上)=两个sa1 sa2以上
pushA sa1(两个sa2 sa3以上)=一个sa1(pushA(sa2 sa3)以上)
堆栈a->Maybe(a,堆栈a)
pop stk=do

(sa,stk')我想我已经想出了一个办法。我在问题中提出的数字系统不是正确的;它不支持
O(logn)
pop
(或者至少不支持简单的操作)。我们可以通过从0/1/2冗余二进制切换到1/2/3冗余二进制来修补这个问题

——注意两个构造函数中的lazy字段。
数据栈a
=空
|一个!(a)!(a组)
|两个!(a)!(小型阵列a)(堆栈a)
|三个!(a)!(a)!(a)!(a组)
推送::a->堆栈a->堆栈a
push=pushA。纯净的
pushA::SmallArray a->Stack a->Stack a
pushA sa Empty=一个sa Empty
pushA sa1(一个sa2以上)=两个sa1 sa2以上
pushA sa1(两个sa2 sa3以上)=三个sa1 sa2 sa3以上
pushA sa1(三个sa2 sa3 sa4以上)=两个sa1 sa2(pushA(sa3 sa4)以上)
堆栈a->Maybe(a,堆栈a)
pop stk=do
ConsA sa stk“两个sa2 sa3更多”
哪里
len'=sizeofSmallArray sa1`quot`2
sa2=克隆甲基阵列sa1 0 len'
sa3=Clonesmall阵列sa1 len'len'
证明其具有所需摊销界限的第一个重要步骤是选择借方不变量[*]。这让我陷入了相当长的一段时间,但我想我已经明白了

借方不变量:我们允许一个
两个
节点中的惰性
堆栈
的借方数量与该节点和所有早期
两个
节点中存储的元素数量相同

定理
push
pop
O(log n)
摊销时间内运行

校样草图 推

我们依次考虑每个病例。

  • 空的
    总是微不足道的

  • One
    :我们在下面增加了借方备抵

  • Two
    :我们将以下节点的借方容差减少1个单位。我们支付
    O(logn)
    来清偿多余的借项

  • Three
    :这是推送
    的棘手案例。我们有一定数量的
    三个
    节点,后跟其他节点。对于每个
    三个
    节点,我们暂停
    s
    数组加倍工作。我们使用从新的
    Two
    节点中的元素中获得的额外借记额度来支付。当我们到达
    Three
    链的末尾时,我们需要做一些有趣的事情。我们可能需要下面的全部借方余量,因此我们使用借方传递将最终数组追加的借方分摊到所有早期节点上

    最后,我们要么是
    空的
    一个
    ,要么是
    两个
    。如果我们有
    空的
    一个
    ,我们就完成了。如果我们有两个
    ,那么将其更改为三个
    将减少下面的借方备抵。但是我们也从下面的
    3个
    s变为
    2个
    s中获得借方备抵!我们的净损失借记准备金只有1,所以我们是黄金

流行音乐 我们再次按案例进行

  • Empty
    是微不足道的
  • Three
    :我们在下面增加了借方备抵
  • Two
    :我们将某些节点上的借方备抵减少1个单位;支付O(对数n)以清偿多余的借项
  • One
    :这是一个很难解决的问题。我们有一定数量的
    一个
    节点,后跟其他节点。对于每个
    一个
    ,我们执行拆分。我们用借项支付这些费用,从根本上解决这些问题。最后,我们遇到了一个类似于推送的情况:棘手的情况是以两个
    Two
    结束,我们使用了所有新的
    Two
    为最终
    Two
    的损失买单的事实
紧凑性 有人可能会担心,结构中会积累足够的thunk,从而否定基于数组表示的紧凑性。幸运的是,情况并非如此。thunk只能出现在
Two
节点中的
堆栈上。但该节点上的任何操作都会将其转换为
一个
三个
,从而强制执行
堆栈
。所以thunk永远不会在链中累积,每个节点的thunk也不会超过一个


[*]冈崎,C.(1998年)。纯功能数据结构。剑桥:剑桥大学出版社,或者在网上阅读他的相关部分。

你所说的“空间需求接近阵列”是什么意思?我想你会有更大的机会在上得到答案。好吧,我想我当时就理解了,但是一个简单的列表(
[a]
)不也能实现这一点吗(还有
O(1)
push/pop)?我想我没有看到将列表项“压缩”到数组中的好处。@Bergi常量因子在这里很重要。列出每个元素额外花费一个
Cons
节点(因此dfeuer描述的限制不会是1)。使用vectors,您只需支付(装箱)元素的固有成本:指针+内容。@dfeuer是否存在一种病态情况,即推送和弹出交替进行,每次操作的成本为O(n)是时候了,因为大数组不断被连接/拆分了?我的直觉是,这些
push
pop
方法无法实现
O(logn)
复杂性。不可否认,我还没有算过。@Bergi,你说得对<代码>零是一个更大的问题