我发现的唯一一个用于处理图形的 PHP 库是 PEAR“Structures_Graph”包(http://pear.php.net/manual/en/package.structures.structures-graph.php)。它目前没有得到维护,重要的功能(例如删除节点)没有实现,并且存在严重的错误(例如无法在 Windows 7 下安装)。它看起来不像那个包,以其目前的形式,将是有用的。
操作图所需的基本操作可以分为更改图(变异)和查询图(非变异)的操作。
变异操作
- CreateNode($nodeName),返回一个 $nodeID – 请注意,当创建节点时,它们没有将它们连接到图中其他节点的边。
- DeleteNode($nodeID) - 为了强制引用完整性,只有在连接到节点的所有边都先前已被删除时才允许这样做,否则会抛出异常。
- UpdateNodeName($nodeID, $newNodeName) – 如果允许更改现有节点的名称。
- CreateHorizontalEdge($souceNodeID, $destinationNodeID) – 这是一条有向边。如果边缘已经存在,或者如果添加边缘缠绕创建一个循环,则抛出异常。
- DeleteHorizontalEdge($sourceNodeID, $destinationNodeID)
- CreateVerticalEdge($firstNodeID, $secondNodeID) - 这是一条双向边,第一个和第二个节点ID可以互换,对图的效果是一样的。如果边已经存在或者两个节点没有相同的水平父节点,则抛出异常。
- DeleteVerticalEdge($firstNodeID, $secondNodeID) - 由于边缘是无向的,即使参数的创建顺序相反,它也会删除边缘。
示例:要手动构建具有以“B”开头的节点名称的图形部分,代码将是:
$nodeID_B = CreateNode(“B”);
$nodeID_B1 = CreateNode(“B1”);
$nodeID_B2 = CreateNode(“B2”);
$nodeID_B3 = CreateNode(“B3”);
CreateHorizontalEdge($nodeID_B, $nodeID_B1);
CreateHorizontalEdge($nodeID_B, $nodeID_B2);
CreateHorizontalEdge($nodeID_B, $nodeID_B3);
CreateVerticalEdge($nodeID_B1, $nodeID_B2);
CreateVerticalEdge($nodeID_B2, $nodeID_B3);
手动删除名称为“B3”的节点的代码:
// Must remove all edges that connect to node first
DeleteVerticalEdge($nodeID_B2, $nodeID_B3);
DeleteHorizontalEdge($nodeID_B, $nodeID_B3);
// Now no edges connect to the node, so it can be safely deleted
DeleteNode($nodeID_B3);
非变异操作
- NodeExists($nodeID) – 返回真/假
- GetNodeIDByName($nodeName) – 返回 $nodeID
- GetNodeName($nodeID)
- HorizontalEdgeExists($sourceNodeID, $destinationNodeID) – 返回真/假
- VerticalEdgeExists($firstNodeID, $secondNodeID) – 返回真/假,无论参数顺序如何,结果都相同。
- HorizontalConnectionExists($startNodeID, $endNodeID) – 返回真/假 – 沿着水平箭头,是否有从开始节点到结束节点的路径?要测试创建从 $nodeID1 到 $nodeID2 的新水平边是否会创建一个循环,请调用 HorizontalConnectionExists($nodeID2, $nodeID1)。
- GetHorizontalAncestorNodes($nodeID) – 返回具有从它们到参数节点的水平路径的所有节点的数组。
- GetHorizontalDescendentNodes($nodeID) – 返回具有从参数节点到参数节点的水平路径的所有节点的数组。
- GetVerticalSiblingNodes($nodeID) – 返回与参数节点有垂直连接的所有节点的数组。由于(根据您对我的澄清问题的回答),垂直关系不是可传递的,因此 VerticalEdgeExists 函数(上图)是查询垂直关系所需的唯一函数。
示例:要获取要包含在问题中描述的子树中的节点列表,请结合 GetHorizontalAncestorNodes($nodeID) 和 GetVerticalSiblingNodes($nodeID) 的结果。
数据结构
您将始终需要一个“节点”表来保存 nodeID 和 nodeName。该表可以扩展以保存与节点相关的其他信息。
由于垂直边不具有传递性,因此可以简单地将有关它们的信息放在“VerticalEdges”表中,其中列有 vEdgeID、firstNodeID、secondNodeID。
如何存储水平边缘信息有多种选择。一方面,数据结构和变异操作可以很简单,但代价是使某些查询操作更慢、更复杂。另一方面,数据结构可能稍微复杂一些,但可能更大(在最坏的情况下随着节点数量呈指数增长),具有更复杂的变异操作,但查询更简单、更快。决定哪种实现最适合您将取决于图表的大小以及与查询它们的次数相比它们的更改频率。
我将描述三种可能的数据结构来表示图,从简单到复杂。我将仅针对最后一组数据结构详细介绍上面列出的操作的算法。我认为这组结构最适合查询与更改比例较高的较小图。
请注意,所有数据结构都有我上面讨论过的“Nodes”和“VerticalEdges”表。
最小数据结构
第一个数据结构有一个“HorizontalEdges”表,其中包含 hEdgeID、sourceNodeID 和 destinationNodeID 列。变异函数很简单,大部分代码都是抛出异常的错误检查代码。非变异函数 HorizontalConnectionExists、GetHorizontalAncestorNodes 和 GetHorizontalDescendentNodes 将很复杂并且可能很慢。每次调用它们都会递归遍历HorizontalEdges 表并收集nodeID。这些集合直接返回给后面的两个函数,而 HorizontalConnectionExists 可以终止并在搜索起始节点的后代时找到结束节点并返回 true。如果搜索完成但没有找到结束节点,它将返回 false。
传递闭包表
第二个数据结构也有一个与上述相同的 HorizontalEdges 表,但也有第二个表“HorizontalTransitiveClosures”,列 hTCID、startNodeID 和 endNodeID。对于每对开始节点和结束节点,该表中有一行,以便可以从开始节点到结束节点跟踪使用水平边的路径。
示例:对于问题中的图表,此表中包含节点 A 的行(为了简化符号,我将使用名称,而不是整数节点 ID 来标识节点,并省略 hTCID 列)是:
A, A2
A, A2B1
A, A2B1B2
A, X
A, Y
A, Z
包含节点 A2B1 的行(第一个也在上面的集合中)是:
A, A2B1
A2, A2B1
B, A2B1
B1, A2B1
A2B1, A2B1B2
A2B1, X
A2B1, Y
A2B1, Z
在更糟糕的情况下,此表按节点数的平方进行缩放。
使用此数据结构,HorizontalConnectionExists、GetHorizontalAncestorNodes 和 GetHorizontalDescendentNodes 可以实现为对 HorizontalTransitiveClosures 表的简单搜索。复杂性移至 CreateHorizontalEdge 和 DeleteHorizontalEdge 函数。DeleteHorizontalEdge 特别复杂,需要对算法的工作原理进行一些解释。
带路径的传递闭包
我将讨论的最终数据结构将水平边缘信息存储在两个表中。第一个,“HorizontalTransitiveClosurePaths”有列 hTCPathID、startNodeID、endNodeID、pathLength。第二个表“PathLinks”具有列 PathLinkID、hTCPathID、sourceNodeID、destinationNodeID。
HorizontalTransitiveClosurePaths 表与上述数据结构中的 HorizontalTransitiveClosures 表类似,但它对于可以实现传递闭包的每个可能路径都有一行,而不是每个传递闭包一行。例如,在问题所示的图表中,HorizontalTransitiveClosures 表将有一行 (B, A2B1B2) (如上的简写符号)用于从 B 开始并结束 A2B1B2 的闭包。HorizontalTransitiveClosurePaths 表将有两行:(10, B, A2B1B2, 3) 和 (11, B, A2B1B2, 2),因为从 B 到 A2B1B2 有两种方法。PathLinks 表描述了这些路径中的每一个,每个边都有一行组成路径。对于从 B 到 A2B1B2 的两条路径,行是:
101, 10, B, B1
102, 10, B1, A2B1
103, 10, A2B1, A2B1B2
104, 11, B, B2
105, 11, B2, A2B1B2
不需要 HorizonalEdges 表,因为可以通过选择 HorizontalTransitiveClosurePaths 表中长度为 1 的行来找到边。
查询函数的实现方式与上述传递闭包数据结构相同。由于闭包可能存在多个路径,因此需要使用 GROUP BY 运算符。例如,返回 ID 为 :nodeid 的节点的祖先的所有节点的 SQL 查询是: SELECT startNodeID from HorizontalTransitiveClosurePaths WHERE endNodeID = :nodeid GROUP BY startNodeID
要实现 DeleteHorizontalEdge,请在 PathLinks 中搜索包含边缘的所有路径的 hTCPathID。然后从 HorizontalTransitiveClosurePaths 表中删除这些路径以及与 PathLinks 表中的路径关联的所有边。
要实现 CreateHorizontalEdge($souceNodeID, $destinationNodeID),首先在 HorizontalTransitiveClosurePaths 表中搜索以 $souceNodeID 结尾的路径——这是“祖先路径集”。在 HorizontalTransitiveClosurePaths 中搜索从destinationNodeID 开始的路径——这是“下降路径集”。现在将来自以下 4 个组的新路径(其中一些可能为空)插入到 HorizontalTransitiveClosurePaths 表中,并将这些路径的链接插入到 PathLinks 表中。
- 从 $souceNodeID 到 $destinationNodeID 的长度为 1 的路径
- 对于祖先路径集中的每条路径,一条新路径将路径的末尾扩展一个从 $souceNodeID 到 $destinationNodeID 的附加链接
- 对于后代路径集中的每条路径,一个新路径将路径的起点扩展一个从 $souceNodeID 到 $destinationNodeID 的附加链接
- 对于祖先路径集合中的一个路径和后代路径集合中的一个路径的每个组合,通过对祖先路径进行切片创建的路径,到从$souceNodeID到$destinationNodeID的链接,到后代路径。
示例:一个图由 6 个节点组成:A1、A2、B、C、D1 和 D2。它有 4 个边,(A1, B), (A2, B), (C, D1), (C, D2)。HorizontalTransitiveClosurePaths 表(使用节点名称而不是数字)是:
1, A1, B, 1
2, A2, B, 1
3, C, D1, 1
4, C, D2, 1
PathLinks 表是:
1, 1, A1, B
2, 2, A2, B
3, 3, C, D1
4, 4, C, D2
现在我们添加从 B 到 C 的边。祖先路径集是 (1, 2),后代路径集是 (3, 4) 4 组中添加的路径分别是:
- 新边本身:HorizontalTransitiveClosurePaths: (5, B, C, 1); 路径链接(5、5、B、C)
- 在末尾将每个祖先路径扩展一个链接:
水平传递闭包路径:
6、A1、C、2
7、A2、C、2
路径链接:
6、6、A1、B
7、6、B、C
8、7、A2、B
9、7、B、C
- 在开始时将每个后代路径扩展一个链接:
水平传递闭包路径:
8、B、D1、2
9、B、D2、2
路径链接:
10、8、B、C
11、8、C、D1
12、9、B、C
13、9、C、D2
- 将包含一个祖先路径和一个后代路径的所有组合拼接在一起:
水平传递闭包路径:
10、A1、D1、3
11、A1、D2、3
12、A2、D1、3
13、A2、D2、3
路径链接:
14、10、A1、B
15、10、B、C
16、10、C、D1
17、11、A1、B
18、11、B、C
19、11、C、D2
20、12、A2、B
21、12、B、C
22、12、C、D1
23, 13, A2, B
24、13、B、C
25、13、C、D2
让我知道答案的任何部分是否需要进一步澄清。