微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

使用差异列表快速序列化 BST 背景我的问题

如何解决使用差异列表快速序列化 BST 背景我的问题

背景

我在业余时间处理 Ullmans Elements of ML programming。最终目标是自学Andrew Appels Modern Compiler Implementation in ML

在 Elements of ML 中,Ullman 描述了差异列表:

LISP 程序员有一个技巧叫做差异列表,其中一个 通过保留,作为您的额外参数来更有效地操作列表 功能一个列表,以某种方式表示您已经完成的工作。 这个想法出现在许多不同的应用程序中;

Ullman 使用 reverse 作为差异列表技术的示例。这是一个以 O(n^2) 运行的慢函数

fun reverse nil = nil
  | reverse (x::xs) = reverse(xs) @ [x]

使用差异列表的速度更快

fun rev1(nil,M) = M
  | rev1(x::xs,ys) = rev1(xs,x::ys)

fun reverse L = rev1(L,nil)

我的问题

我有这种二叉搜索树 (BST) 数据类型。

datatype 'a btree = Empty
      | Node of 'a * 'a btree * 'a btree

按顺序收集元素列表的一个简单的解决方案是

fun preOrder Empty = nil
  | preOrder (Node(x,left,right)) = [x] @ preOrder left @ preOrder right

但 Ullman 指出 @ 运算符很慢,并在练习 6.3.5 中建议我使用差异列表实现 preOrder

经过一番摸索,我想出了这个功能

fun preOrder tree = let
    fun pre (Empty,L)  = L
      | pre (Node(x,right),L) = let
          val L = pre(right,L)
          val L = pre(left,L)
        in
            x::L
        end
    in
       pre (tree,nil)
end

它按预先顺序输出元素。 但是它按后序评估树!并且代码比幼稚的 preOrder 更丑陋。

> val t = Node(5,Node(3,Node(1,Empty,Empty),Node(4,Empty)),Node(9,Empty))
> preOrder t
val it = [5,3,1,4,9] : int list

现有技术

我尝试在 ML 编程中搜索对差异列表的引用,发现 John Hughes original article 描述了如何使用差异列表进行反向。

我还在 Haskell 中找到了带有示例的 Matthew Brecknells difference list blog post。他区分了使用累加器(如 Ullmans 反向示例)和为差异列表创建新类型之间的区别。他还展示了一个树木平整器。但是我很难理解 Haskell 代码,并且希望在标准 ML 中进行类似的公开。 ABC


问题

  • 如何实现一个函数来实际预排序评估树并预排序收集元素?遍历后是否必须反转列表?还是有什么别的技巧?

  • 我如何将这种技术推广到适用于中序和后序遍历?

  • 在 BST 算法中使用差异列表的惯用方法是什么?

解决方法

您最终执行此操作的方法是合理获得的最佳方法。事实证明,这样做的好方法是

fun preOrderHelper (Empty,lst) = lst
  | preOrderHelper (Node(x,left,right),lst) = 
        x :: preOrderHelper(left,preOrderHelper(right,lst))

fun preOrder tree = preOrderHelper(tree,Nil)

请注意,preOrderHelper(tree,list) 的运行时间只是 tree 的函数。在树 r(t) 上调用 preOrderHelper 的运行时间 t。然后我们有 r(Empty) = O(1)r(Node(x,right)) = O(1) + r(left) + r(right),所以很明显 r(t)t 的大小上是线性的。

这种技术的推导是什么?有没有更原则的推导方式?通常,当您将数据结构转换为列表时,您希望将 foldr 转换为空列表。我不太了解 ML 来说明类型类的等价物是什么,但在 Haskell 中,我们会按如下方式处理这种情况:

data Tree a = Empty | Node a (Tree a) (Tree a)

instance Foldable Tree where
   foldr f acc t = foldrF t acc  where
      foldrF Empty acc = acc
      foldrF (Node x left right) acc = f x (foldrF left (foldrF right acc))

要将 Tree a 转换为 [a],我们将调用 Data.Foldable.toList,它在 Data.Foldable 中定义为

toList :: Foldable f => f a -> [a]
toList = foldr (:) []

展开这个定义给了我们上面的机器学习定义的等价物。

如您所见,您的技术实际上是将数据结构转换为列表的一种非常有原则的方法的特例。

事实上,在现代 Haskell 中,我们可以完全自动地做到这一点。

{-# LANGUAGE DeriveFoldable #-}

data Tree a = Empty | Node a (Tree a) (Tree a) deriving Foldable

将自动为我们提供上述 Foldable 实现的等效(*),然后我们可以立即使用 toList。我不知道 ML 中的等价物是什么,但我确定有类似的东西。

ML 和 Haskell 的区别在于 Haskell 是懒惰的。 Haskell 的懒惰意味着 preOrder 的评估实际上是按照 pre-Order 顺序遍历树的。这是我喜欢懒惰的原因之一。惰性允许对评估顺序进行非常细粒度的控制,而无需求助于非功能性技术。


(*)(根据参数顺序,在惰性 Haskell 中不计算在内。)

,

您显示的不是我所看到的通常称为差异列表的内容。

那将是,在伪代码中,

-- xs is a prefix of an eventual list xs @ ys,-- a difference between the eventual list and its suffix ys:
dl xs = (ys => xs @ ys)

然后

pre Empty = (ys => ys)  -- Empty contributes an empty prefix
pre (Node(x,right)) = (ys =>
    --  [x] @ pre left @ pre right @ ys  -- this pre returns lists
    (dl [x] . pre left . pre right)  ys) -- this pre returns diff-lists
                        -- Node contributes an [x],then goes 
                        -- prefix from `left`,then from `right`

这样

preOrder tree = pre tree []

其中 . 是函数组合运算符,

(f . g) = (x => f (g x))

当然,因为 dl [x] = (ys => [x] @ ys) = (ys => x::ys)等价于你所展示的,形式为

--pre Empty = (ys => ys)  -- Empty's resulting prefix is empty
pre'  Empty    ys =  ys  

--pre (Node(x,right)) = (ys =>
pre'  (Node(x,right))    ys = 
    --     [x] @ pre  left @ pre  right @ ys
    -- (dl [x] . pre  left . pre  right)  ys
            x::( pre' left ( pre' right   ys))

-- preOrder tree = pre' tree []

在操作上,这将在急切语言中从右到左遍历树,在惰性语言中从左到右遍历。

概念上,从左到右看,结果列表有[x],然后是遍历left的结果,然后是遍历right的结果,不管树是什么遍历顺序。

这些差异列表只是部分应用了 @ 运算符,附加只是功能组合:

   dl (xs @ ys)     ==  (dl xs . dl ys)
 -- or:
   dl (xs @ ys) zs  ==  (dl xs . dl ys)  zs
                    ==   dl xs ( dl ys   zs)
                    ==      xs @   (ys @ zs)

前缀 xs @ ys 是前缀 xs,然后是前缀 ys,然后是最终的后缀 zs

因此附加这些差异列表是一个 O(1) 操作,创建一个新的 lambda 函数,它是参数的组合:

append dl1 dl2 = (zs =>  dl1 ( dl2  zs))
               = (zs => (dl1 . dl2) zs )
               =        (dl1 . dl2)

现在我们可以很容易地看到如何编写中序或后序遍历的代码,如

in_ Empty = (ys => ys)
in_  (Node(x,right)) = (ys =>
    --  in_ left @    [x] @ in_ right @ ys
       (in_ left . dl [x] . in_ right)  ys)

post Empty = (ys => ys)
post  (Node(x,right)) = (ys =>
    --  post left @ post right @    [x] @ ys
       (post left . post right . dl [x])  ys)

只关注列表 [x] 及其附加的 @ 让我们可以统一对待这一点——无需担心 :: 及其具有不同类型的参数。

@ 的两个参数的类型是相同的,就像 + 的整数和 . 的函数一样。在附加操作是关联的 (a+b)+c == a+(b+c) 并且有一个“空”元素 e @ s == s @ e == s 的条件下,与此类操作配对的此类类型被称为 monoids。这只是意味着组合操作在某种程度上是“结构化的”。这适用于苹果和橙子,但适用于原子核 - 不是那么多。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。