【问题标题】:Fast serialization of BST using a difference list使用差异列表快速序列化 BST
【发布时间】:2021-09-18 09:20:46
【问题描述】:

背景

我在业余时间处理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, left, 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, Empty)), 
    Node(9, Empty, 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 算法中使用差异列表的惯用方式是什么?

【问题讨论】:

  • @ggorlen 是 stackoverflow 重写了亚马逊链接以使用他们的亚马逊帐户。
  • 两个函数产生相同的结果,你的例子是预购。 (您可以将pre 缩减为x::pre(left, pre(right, L)),这与第一个版本非常相似。)第二个版本的一个实际问题是它不是尾递归的。
  • 它确实与数学定义紧密映射:前序列表是节点值,然后是左孩子的前序遍历,然后是右孩子的前序遍历。此外,如果您想通过使用循环来避免(最有可能的)其他语言中的非尾递归,您需要维护自己的评估堆栈,这远非优雅。
  • 相关:preorder。相关:tailrecursion-modulo-consrelated(查找“地鼠”)。一个非常明确的 Prolog 答案是 this one。但它使用了突变——或者更确切地说,明确设置一个逻辑变量(我们只允许对一个未实例化的变量进行一次)。 这种技术在 Haskell 中通过 “打结” 进行了模拟。现在,一个人真的把一个人的心结成一个结!部分应用的附加运算符的功能组合是 easy (相比之下)。 :)
  • 顺便说一句,为清楚起见,变量确实应该编号,即使使用相同的名称,L,您的语言也允许:fun pre (Empty, L) = L| pre (Node(x, left, right), L1) = letval L2 = pre(right, L1)@ 987654344@in x::L3 end 让数据流不言而喻。现在它也不丑了。 :) 顺便说一句,在 Haskell 中,由于其惰性求值,这段确切的代码将从左到右遍历树!

标签: recursion functional-programming binary-search-tree sml difference-lists


【解决方案1】:

您这样做的最终方法是合理获得的最佳方法。这样做的好方法是

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, left, right)) = O(1) + r(left) + r(right),所以很明显r(t)t 的大小是线性的。

这种技术的起源是什么?有没有更原则的推导方法?通常,当您将数据结构转换为列表时,您希望将foldr 放入一个空列表中。我不知道足够的 ML 来说明 typeclass 的等价物是什么,但在 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 (:) []

展开此定义为我们提供了与上述 ML 定义等效的内容。

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

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

{-# LANGUAGE DeriveFoldable #-}

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

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

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


(*)(直到参数顺序,在懒惰的 Haskell 中不计算在内。)

【讨论】:

  • 所以树上的 foldr 给了我线性时间的预购表示?您可以使用 foldr 进行中序和后序遍历吗?
  • @DanielNäslund 将其与基于@ 的原始“简单”代码联系起来,foldr (:) [] t 等效于fold (map (\ x -> [x] ) t),其中fold 将所有组成列表附加在一起。它知道附加它们,因为当被视为幺半群时(这是 fold 所做的),列表由 ++ 运算符(在 ML 中是 @)组合。所以它真的相同的(嗯,语义上。操作上,是一个单独的考虑)。
  • @DanielNäslund “你可以使用foldr 进行中序和后序遍历吗?”这是一个非常有趣的问题。 this answer 说这是不可能的(至少不是很容易)。订单在foldr 中被烘焙,并通过自动派生由数据类型声明确定。对于中序遍历,我们需要data Tree a = Empty | Node (Tree a) a (Tree a),对于后序遍历,我们需要data Tree a = Empty | Node (Tree a) (Tree a) a
  • 该答案提示(和链接)无论如何都有可能这样做。另一种可能的方式是this answer。它定义了 foldMap,但任何 foldMap 都会产生 foldr,反之亦然。
  • 感谢@WillNess 和 Mark Saving。 Monoids 和 laziness 是我在编写 ML 代码时不必处理的两个概念。我会阅读你的解释和链接,希望在适当的时候我会有足够的上下文来完全理解你的论点。我会再等一天左右接受答案,希望有人会出现并仅使用标准 ML 给出解释。
【解决方案2】:

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

那将是,在伪代码中,

-- 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, left, 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, left, right)) = (ys =>
pre'  (Node(x, left, 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, left, right)) = (ys =>
    --  in_ left @    [x] @ in_ right @ ys
       (in_ left . dl [x] . in_ right)  ys)

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

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

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

【讨论】:

    猜你喜欢
    • 2014-08-29
    • 1970-01-01
    • 1970-01-01
    • 2021-07-14
    • 1970-01-01
    • 1970-01-01
    • 2012-05-24
    • 2010-10-25
    • 1970-01-01
    相关资源
    最近更新 更多