假设我想编写一个算法,该算法处理一个以叶子列表作为输入的不可变树数据结构。它需要返回一个新树,其中对从那些树叶向上的旧树进行了更改。
我的问题是,如果它们在列表中没有重建整个树检查,似乎没有办法完成这个纯粹的功能,因为你总是需要返回一个完整的新树作为操作的结果,你不能改变现有的树。
这是函数式编程中的一个基本问题,只能通过使用更合适的算法来避免,或者我错过了什么?
编辑:我不仅要避免重新创建整个树,而且功能算法应该具有与变异变体相同的时间复杂度。
答案 0 :(得分:2)
答案 1 :(得分:2)
到目前为止,我见过的最有希望的事实(确实不是很长......)是Zipper data structure:它基本上保持一个单独的结构,从节点到根的反向路径,并进行本地编辑在这个单独的结构上。
它可以进行多次本地编辑,其中大部分是常量时间,并将它们一次性写回树中(重建根路径,这是唯一需要更改的节点)。
Zipper是standard library in Clojure(参见标题拉链 - 功能树编辑)。
并且the original paper by Huet在OCaml中有一个实现。
免责声明:我已经编程了很长时间,但几周前才开始编程,直到上周才开始编辑树的功能编辑问题,所以很可能还有其他解决方案我不知道。
但是,看起来拉链完成了人们所希望的大部分工作。如果在O(log n)或以下还有其他选择,我想听听它们。
答案 2 :(得分:1)
这取决于您的函数式编程语言。例如,在Haskell中,它是一种Lazy函数式编程语言,结果是在最后一刻计算的;什么时候他们是需要的。
在您的示例中,假设是因为您的函数创建了一个新树,所以必须处理整个树,而实际上该函数只是传递给下一个函数,并且只在必要时执行。
延迟评估的一个很好的例子是Haskell中的sieve of erastothenes,它通过消除数字列表中当前数字的倍数来创建素数。请注意,数字列表是无限的。取自here
primes :: [Integer]
primes = sieve [2..]
where
sieve (p:xs) = p : sieve [x|x <- xs, x `mod` p > 0]
答案 3 :(得分:1)
我最近编写了一种算法,该算法可以完全满足您的描述-https://medium.com/hibob-engineering/from-list-to-immutable-hierarchy-tree-with-scala-c9e16a63cb89
它分两个阶段工作:
一些警告:
无节点突变,结果是一棵不可变树
复杂度为O(n)
忽略传入列表中的循环引用