问题描述
背景
我在业余时间处理 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)
我的问题
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
问题
解决方法
您最终执行此操作的方法是合理获得的最佳方法。事实证明,这样做的好方法是
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。这只是意味着组合操作在某种程度上是“结构化的”。这适用于苹果和橙子,但适用于原子核 - 不是那么多。