最近几天我一直在尝试这个东西。
我首先尝试让每个节点保存对边的引用,每条边保存对节点的引用。我使用 (dosync... (ref-set...))
这种操作将它们相互设置为相等。但是我不喜欢这个方法,因为更改一个节点需要大量更新,并且打印图形有点棘手。我必须覆盖 print-method
多态方法,以防止repl溢出堆栈。而且,每当我想要向现有节点添加边时,我都必须先从图中提取实际节点,然后进行各种边缘更新和其他事情,以确保每个人都持有另一件事的最新版本。此外,由于事物在引用中,确定某些东西是否连接到其他东西是线性时间操作,这似乎很不优雅。在确定使用此方法执行任何有用的算法之前,我并没有取得很大进展。
然后我尝试了另一种方法,这是矩阵的变体。图是一个Clojure映射,其中键是节点(而不是节点的引用),值是另一个映射,其中键是相邻节点,每个键的单个值是指向该节点的边,表示为指示边强度的数字值或我在其他地方定义的边结构。
对于 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
要访问节点1的邻居,您可以使用
(keys (graph node-1))
或调用在其他地方定义的函数
(neighbors graph node-1)
,或者您可以使用
((graph node-1) node-2)
,以获取从
1->2
的边缘。
几个优点:
- 在图中查找节点和相邻节点的时间复杂度是常数级别的,如果不存在则返回nil。
- 简单灵活的边缘定义。当您将相邻节点添加到映射中的节点条目时,有向边缘隐式存在,并且其值(或结构体以提供更多信息)显式提供或为nil。
- 您不必查找现有的节点才能对其进行任何操作。它是不可变的,因此您可以在将其添加到图形中之前仅定义一次,然后在更改事物时不必追赶最新版本。如果图表中的连接发生更改,则更改图表结构,而不是节点/边缘本身。
- 这将矩阵表示的最佳特性(图形拓扑结构在图形映射本身中而不是编码在节点和边缘中、常数时间查找和非突变节点和边缘)与邻接表相结合(每个节点“有”其相邻节点的列表、空间效率高,因为没有类似于规范稀疏矩阵的“空白”)。
- 您可以在节点之间有多个边缘,并且如果意外定义了已经存在的边缘,则映射结构会确保您不重复它们。
- 节点和边缘标识由Clojure保留。我不必想出任何索引方案或通用参考点。地图的键和值是它们所代表的东西,而不是在其他地方查找的内容或引用。您的节点结构可以全部为nil,只要它是唯一的,就可以在图形中表示它。
我看到的唯一一个大问题是对于任何给定操作(添加、删除、任何算法),您不能只传递起始节点。您必须传递整个图形映射和一个起始节点,这可能是为整个流程提供简单性所需付出的合理代价。另一个小缺点(或者说也许不是)是对于无向边,您必须在每个方向上定义边。实际上,这是可以接受的,因为有时边对于每个方向具有不同的值,而这种方案允许您这样做。
我在这里看到的唯一其他事情是,因为边缘在映射中的键值对存在中是隐含的,所以您不能定义超边缘(即连接多于2个节点的边)。我不认为这一定是大问题,因为我遇到的大多数(全部?)图算法只处理连接2个节点的边。
transient
构建循环数据结构的方法。你有任何示例代码来说明这个想法吗?即使是像向量自身作为元素这样简单的东西? - Laurence Gonsalves