我目前正在尝试在clojure中实现一个不可变的BST。 这是我的make-tree函数:
(defn make-tree [v] {:v v :l nil :r nil})
并插入:
(defn insert [tree v]
(if (nil? tree)
(make-tree v)
(case (compare v (tree :v))
-1 (assoc tree :l (insert (:l tree) v))
0 tree
1 (assoc tree :r (insert (:r tree) v)))))
问题是,这个插入函数会溢出类似
的东西 (reduce insert (make-tree 1) (range 10000))
我知道我可以让树平衡,这样我几乎不需要超过1000深度。我仍然很好奇是否有定义函数所以它不会溢出。
由于可变版本只是修改节点因此不需要存储父节点,这看起来很方便。
你会在现实生活中选择什么?可变的还是不可变的?
答案 0 :(得分:4)
添加到A. Webb的答案:
虽然需要在CPS中编写纯函数实现,但您可以将树实现为功能包装器+可变节点,其中修改将按如下方式进行:
Wrapper创建一个新的空根并告诉当前根执行修改并将结果放入新根。
如果新root确定要修改自己的值,则将其子项复制到新根,将新值放入新根并返回。
否则,它会将其值和不需要修改的分支复制到新根,并在新根中安装一个新的(空白)节点来代替(或者更确切地说,可能)需要修改的分支。
当前的根告诉需要修改的分支修改自己,同时将其交给新的空白节点。
分支在重复步骤2-5中充当根。
包装器使用新的根创建一个新的包装器。作为顶级操作的结果,返回新的包装器。
上面的第2-5点描述了一个递归过程,但它实际上是尾递归的,因此可以重写为循环。
完成所有操作后,旧包装器当然完全正常并且仍然保持相同的树(因为所有突变仅涉及新节点)。
事实上,许多Clojure数据结构一直使用包含的可变性(主要涉及数组)。 (但不是树形图;使用可变性的实际模式也是不同的,但使用包含可变性来加速内部事物同时在外部保持功能界面的基本前提是类似的。)
我还要添加两个相切的评论:
首先,您的实现假设compare
将返回-1,0,1中的一个,其中实际上它可以自由地返回“小于”的任何负数和“大于”的任何正数({ {1}}在我的REPL中评估为(compare "foo" "bar")
- 可能是任何JVM REPL,尽管确切的值再次未指定。)
其次,如果您对在Clojure中实现的自平衡树的现实生活示例感兴趣,您可能需要查看sorted.clj,这是Clojure的4
和{的实现。 {1}}在Clojure中。它基于ClojureScript实现,而ClojureScript实现又是Clojure Java实现的一个端口。在这一点上,我会说它纯粹是实验性的 1 ,但它似乎确实有用(通过CLJS测试套件,做我在REPL上所期望的)并且可能有用作为一个例子在Clojure中,简单的PDS实现可能是什么样的。
1 基本上在编写ClojureScript实现之后,我想看看生成相同数据结构的Clojure实现需要多少额外工作。很明显会有一些额外的工作,因为要实现Java接口,一些数组处理代码需要调整等等,但我希望它不会太多。我很高兴地报告说,基本的期望是由经验证实的。在性能方面,它不太符合Java实现的标准(我似乎记得在大多数基准测试中放慢了1.5倍,但我必须重新检查);我希望最终能够改进这一点,尽管此时core.rrb-vector对于perf调整具有更高的优先级。
答案 1 :(得分:3)
您的实现不是自我平衡的,您的示例是最坏的情况,其中元素按顺序插入。堆栈溢出是由于深度递归造成的。为避免这种情况,您需要以连续传递样式重写算法,以便可以使用尾递归或创建显式展开堆栈,而不是隐式使用调用堆栈。
在现实生活中,Clojure已经有了一个不可变的自平衡树sorted-set
和sorted-map
,我会在大多数情况下使用它。 Java具有可变TreeMap
,如果需要,您可以通过Clojure的Java互操作轻松地使用它。
(into (sorted-set) (range 10000))