如何在没有额外间接的情况下在Clojure中创建循环(和不可变)数据结构?

时间:2011-01-02 22:30:08

标签: clojure immutability directed-graph

我需要在Clojure中表示有向图。我想将图中的每个节点表示为一个对象(可能是一个记录),其中包含一个名为:edges的字段,该字段是可以从当前节点直接访问的节点的集合。希望不言而喻,但我希望这些图表是不可变的。

只要我进行拓扑排序并“从叶子上”构建每个图形,我就可以用这种方法构造有向非循环图形。

然而,这种方法不适用于循环图。我能想到的一个解决方法是为整个图形设置一个单独的集合(可能是地图或矢量)。然后,每个节点中的:edges字段将具有键(或索引)到图的边集合中。添加这种额外的间接级别是有效的,因为我可以在他们(将)引用的东西之前创建密钥(或索引),但它感觉就像一个kludge。每当我想要访问一个相邻节点时,我不仅需要进行额外的查找,而且还必须传递全局边缘集合,这感觉非常笨拙。

我听说有些Lisps有办法创建循环列表而不需要使用变异函数。有没有办法在Clojure中创建不可变的循环数据结构?

3 个答案:

答案 0 :(得分:6)

您可以将每个节点包装在一个引用中,为它指定一个稳定的句柄(并允许您修改可以以nil开头的引用)。然后可以以这种方式构建循环图。这确实有"额外"当然是间接的。

我认为这不是一个好主意。你的第二个想法是更常见的实现。我们构建了这样的东西来保存RDF图,并且可以在核心数据结构和层顶部索引的基础上构建它而不需要太多努力。

答案 1 :(得分:6)

过去几天我一直在玩这个。

我首先尝试让每个节点都有一组refs到边缘,每个边缘都有一组refs到节点。我在(dosync... (ref-set...))类型的操作中将它们设置为彼此相等。我不喜欢这样,因为更改一个节点需要大量更新,打印图表有点棘手。我不得不重写print-method多方法,因此repl不会堆栈溢出。此外,每当我想为现有节点添加边缘时,我必须首先从图中提取实际节点,然后进行各种边缘更新以及确保每个人都持有最新版本的事情另一件事。此外,因为事物在参考中,确定某些东西是否与其他东西相关联是线性时间操作,这似乎是不优雅的。在确定使用这种方法实际执行任何有用的算法之前,我并没有走得太远。

然后我尝试了另一种方法,它是其他地方提到的矩阵的变体。该图是一个clojure映射,其中键是节点(不是节点的refs),值是另一个映射,其中键是相邻节点,每个键的单个值是该节点的边缘,表示为表示边缘强度的数值,或我在别处定义的边缘结构。

对于1->2, 1->3, 2->5, 5->2

,它看起来像这样
(def graph {node-1 {node-2 edge12, node-3 edge13},
            node-2 {node-5 edge25},
            node-3 nil ;;no edge leaves from node 3
            node-5 {node-2 edge52}) ;; nodes 2 and 5 have an undirected edge

要访问node-1的邻居,请转到(keys (graph node-1))或调用其他位置(neighbors graph node-1)定义的函数,或者您可以说((graph node-1) node-2)1->2获取边缘。

几个优点:

  1. 在图表和相邻节点中对节点进行恒定时间查找,如果不存在,则返回nil。
  2. 简单灵活的边缘定义。将邻居添加到地图中的节点条目时会隐式存在有向边,并且显式提供其值(或更多信息的结构),或者为nil。
  3. 您不必查找现有节点即可对其执行任何操作。它是不可变的,因此您可以在将其添加到图形之前对其进行一次定义,然后在事情发生变化时不必追逐它来获取最新版本。如果图表中的连接发生更改,则更改图形结构,而不是节点/边缘本身。
  4. 这结合了矩阵表示的最佳特征(图形拓扑在图形图中本身未在节点和边缘中编码,恒定时间查找,非变异节点和边缘)和邻接列表(每个节点“拥有”其相邻节点的列表,节省空间,因为您没有像规范稀疏矩阵那样的“空白”。
  5. 您可以在节点之间设置多个边,如果您不小心定义了一个已经存在的边,则地图结构会确保您不会复制它。
  6. 节点和边缘标识由clojure保存。我不必提出任何索引方案或公​​共参考点。地图的键和值是它们所代表的东西,而不是其他地方的查找或参考。您的节点结构可以是所有nils,只要它是唯一的,它就可以在图中表示。
  7. 我看到的唯一不利的缺点是,对于任何给定的操作(添加,删除,任何算法),您不能只是将它传递给起始节点。你必须传递整个图形图和一个起始节点,这可能是一个公平的代价,以支付整个事物的简单性。另一个小缺点(或者可能不是)对于无向边缘,您必须在每个方向上定义边缘。这实际上没问题,因为有时边缘对每个方向都有不同的值,这个方案允许你这样做。

    我在这里看到的另一件事是,因为边缘隐含在地图中存在键值对,所以无法定义超边界(即连接超过2个节点的边界)。我不认为这是一个大问题,因为我遇到的大多数图算法(全部?)只处理连接2个节点的边缘。

答案 2 :(得分:3)

我之前遇到过这个挑战,并得出结论认为目前在Clojure中使用真正不可变的数据结构是不可能的。

但是,您可能会发现以下一个或多个选项可以接受:

  • 使用带有“:unsynchronized-mutable”的deftype在每个节点中创建一个mutable:edges字段,在构造期间只更改一次。从那时起,您可以将其视为只读,而不会产生额外的间接开销。这种方法可能具有最佳性能,但有点像黑客。
  • 使用原子实现:边缘。有一些额外的间接,但我个人发现阅读原子非常有效。