2

背景

我在业余时间研究 ML 编程的 Ullmans Elements 。最终目标是自学ML 中的 Andrew Appels 现代编译器实现

在 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 的原始文章,该文章描述了如何使用差异列表进行反向操作。

我还在 Haskell 中找到了Matthew Brecknells 差异列表博客文章以及示例。他区分了使用累加器(如 Ullmans 反向示例)和为差异列表创建新类型。他还展示了一种树木压平机。但是我很难理解 Haskell 代码,并且希望在标准 ML 中进行类似的公开。美国广播公司


问题

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

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

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

4

2 回答 2

2

你这样做的最终方法是它合理得到的最好的方法。这样做的好方法是

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。调用on treer(t)的运行时间。然后我们有和,很明显在 的大小上是线性的。preOrderHelpertr(Empty) = O(1)r(Node(x, left, 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))

要将 a 转换Tree a为 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 中不计算在内。)

于 2021-07-08T18:15:54.340 回答
1

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

那将是,在伪代码中,

-- 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)

prefixxs @ ys是 prefix xs,然后是 prefix 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。这只是意味着组合操作在某种程度上是“结构性的”。这适用于苹果和橙子,但原子核 - 不是那么多。

于 2021-07-10T09:32:07.280 回答