DAG图存储设计

723 阅读7分钟

背景:决策平台中涉及到大量图存储的问题。 这里对图的存储做一个讨论。

代码是对 graphlib 图存储部分进行讨论。 库中还有基于存储之上的图论算法,这里暂时不进行梳理。只关注存储。

图数据结构 是 图位置重排 与 图算法的基础 也是核心。 图数据结构,应该也也包含一系列的操作用于对数据结构的管理。

图分类

  1. Directed! //如果图是有向图的话,设置为true。

  2. isMultigraph //多重图?,一对结点中存在多个边。a graph that can have more than one edge between the same pair of nodes.

  3. isCompound //是否是复合图? 复合图是一个节点可以是其他多个父节点的图。 A compound graph is one where a node can be the parent of other nodes.

节点数据结构

_nodes = {
    id: {}
}

if (this._isCompound) { // 如果图是复合图
  // 复合图是一个节点可以是其他多个节点的父节点的图。
  this._parent![v] = GRAPH_NODE; // 当前父节点为
  this._children![v] = {};
  this._children![GRAPH_NODE][v] = true;
 }
 
// v -> edgeObj
private _in = {};
// u -> v -> Number
private _preds = {};
// v -> edgeObj
private _out = {};
// v -> w -> Number
private _sucs = {};
// e -> edgeObj
private _edgeObjs = {};
// e -> label
private _edgeLabels = {};
  
 //_parent![v] 的数据结构应该是这样的,归graph统一维护, nodeid 的父节点
 _parent = {
     nodeid_1:  GRAPH_NODE(空字符串),
     nodeid_2:  GRAPH_NODE(空字符串)
 }
 // 节点子节点列表
 _children = {
     nodeid_1: {}, // nodeid_1 的子节点信息
     nodeid_2: {}
 }
 // 子节点特殊的数组表示节点是否存在自己的信息中
 _children![GRAPH_NODE] = {
     nodeid_1 : true
 };

_node

存储图节点信息,Object

Key: string

Value: 当前节点的信息(label)里面可以存储用户的任何私有信息。

形式如:

{
    '92d1537c-470a-4f28-b8cc-e347278def1e': {name:xxx, color:#ccc, ...etc}
}

_parent

维护图中 某个节点(v)的父节点信息。 Object

key:string, 节点ID

Value: string, 节点值 这里是个string,并且只有复合图可以使用。 这么设计是一个节点只能存在一个父节点? 这个结构设计会不会存在问题。

从成环检测方面来看,确实这样的,一个节点只能有一个父节点。

{
    "92d1537c-470a-4f28-b8cc-e347278def1e": "parentid-92d1537c"
}

_children

维护图中 某个节点的子节点列表。Object

Key: string, 节点ID

value:object,

并且 _children中存在一个特殊的 **空字符值 **的键。

this._children![GRAPH_NODE][v] = true; // 这种骚操作少见。 当前节点没有父节点,托管给根节点管理?

{
    "parent_node_id_v": {    // 表示从parent_node_id_v => node_id_w 存在关系
        "node_id_w": true
    }
    "":{"parent_node_id_v": true} // 特殊存在字段 setNode方法中产生,表示节点存在? 
}

_in

维护图中 某个节点 入这个节点的边。 Object

Key: string, 节点ID。

Value: object 对象,对象的key为edgeid, 值为边对象 {v:入点id, w:出点id, name: 边名称}

关于边id (eid) 产生方法:** const e = edgeArgsToId(this._isDirected, v, w, name); // 获得边ID**

 {
     "nodeid":{
         "edgeid_1": {v: 入点id, w:出节点id, name:边名称},
         "edgeid_2": {v: 入点id, w:出节点id, name:边名称}
     }
 }

_preds

维护图中 某个节点 与 另外一个确定节点 之间连线个数。例如方向 v->w方向节点流向。节点w的前置节点v, 连接线个数。

表示w节点的前置节点v的连线个数。

{
    node_id_w: {
        node_id_v: number
    }
}

_out

维护图中 某个节点 出这个节点的边。 Object

Key: string, 节点ID。

Value: object 对象,对象的key为edgeid, 值为边对象 {v:入点id, w:出点id, name: 边名称}

 {
     "nodeid":{
         "edgeid_1": {v: 入点id, w:出节点id, name:边名称},
         "edgeid_2": {v: 入点id, w:出节点id, name:边名称}
     }
 }

_sucs

维护图中 某个节点 与 另外一个节点 后置关系。例如方向 v->w方向节点流向。节点w的前置节点v, 连接线个数。 表示从v->w 的连线个数

{
    node_id_v: {
        node_id_w: number
    }
}

_nodeCount

记录图中节点个数。

_edgeCount

记录边个数

_edgeObjs

边对象信息, Object

Key: string ,edgeid

Value: {v:string, w:string, name: 名称} V->W 表示从V顶到W顶点的关系

_edgeLabels

边对应的描述信息, Object

Key: string,

Value: any

{
   e_id: value_any
}

图数据存储查询(维护)方法

setNode 节点增加/更新

setNode(v:string, value:any):Graph //v: 节点id, value: 节点label

Complexity: O(1).

批量增加节点方法: setNodes(vs: string[], value?: string):** Graph Complexity: O(n).**

setParent 设父子节点关系

setParent(v: string, parent?: string): Graph

removeEdge 删除边

removeEdge(vOrEdge: string | Edge, w?: string, name?: string): Graph

无论传入的是什么,第一步都是获得边ID。

进行如下操作:

  1. 根据eid获得边的配置信息 edge = this._edgeObjs[e];

  2. 删除边的记录附带信息 delete this._edgeLabels[e]; //删除边描述

  3. 删除边的信息 delete this._edgeObjs[e]; //删除边

  4. decrementOrRemoveEntry(this._preds[w], v); // v->w, w的前置边减少一个

  5. decrementOrRemoveEntry(this._sucs[v], w); // v->w, v的后置边减少一个

  6. delete this._in[w][e]; //删除掉 w 的进入边 e

  7. delete this._out[v][e]; // 删除掉 v的出边 e

  8. _edgeCount--

removeNode 删除节点

removeNode(v: string): Graph 删除节点

setEdge 设置边

setEdge(

vOrEdge: string | Edge, // origin

wOrValue: string | any, // destination

_label?: any,

_name?: string // 多边图中使用

): Graph

根节点会根据 edgeArgsToId(isDirected, v_, w_, name) 后边的四个参数形成一个唯一id。

关于边设置,会触发 V->W 两个节点增加,注意边ID是不支持自定义的。

是由(v, v, name)三者唯一去定的。

查询图类型** **

** Complexity[ O(1) ]**

  • isDirected(): boolean

  • isMultigraph(): boolean

  • isCompound(): boolean

setGraph(label:any) 图标签

Complexity[O(1)]

给图加标签(数据结构,通过此参数可以透出给下层的 应用)

配套方法 graph(): 返回图的配置信息。

setDefault**NodeLabel **

setDefaultNodeLabel(newDefault:(v:string)=>any): graph

创建Node时,如果节点的value传入的是string,可以使用此函数计算得到node的value。

例如:A-${name} 形式 可以通过函数正则计算获得节点名称。

nodeCount(): 查询节点数目

  • 当前节点数目 Complexity[O(1)]

nodes() :查询节点

Array<string> 返回所有节点id 列表 Complexity[O(1)]

sources(): 返回起始节点

Array<string> 返回图中没有入边的节点(没有父节点) Complexity[O(n)]

sinks(): 返回结束节点

Array<string> 返回图中没有出边的节点(没有子节点) Complexity[O(n)]

**node(v: string): any ** 返回节点信息

返回用户在节点中保存的信息。

可以供上层对节点使用 Complexity[O(1)]

hasNode(v: string): boolean

是否包含某个节点 Complexity[O(1)]

parent(v: string): string | void

获得父节点 Complexity[O(1)]

children(v?:string):string[]|void

返回节点v的所有孩子节点。如果不在图中则返回undefined。如果不是复合图,始终返回[]

Complexity: O(1).

predecessors(v: string): string[] | void

返回指定节点的所有前导节点,如果图中不存在此节点,则返回undefined.

如果是无向图,则会返回undefined,应该使用neighbors。

successors(v: string): string[] | void

返回指定节点的所有后续节点。如果图中没有此节点,则返回undefined. 如果是无向图,则会返回undefined,应该使用neighbors。

neighbors(v: string): string[] | void

返回指定节点的前导节点或者后续节点。如果图中没有此节点,则返回undefined。

无向图中相邻即可。

Complexity: O(n).

isLeaf(v) 是否为叶子节点

有向图为后续节点。

无向图为孤立的节点。啥都没的那种。

filterNodes 去除掉某些节点(会形成新图)

filterNodes(filter: (v: string) => boolean): Graph

通过过滤指定节点,形成新图,原来的图结构并没有保存。

setDefaultEdgeLabel 边处理函数

setDefaultEdgeLabel(newDefault: (v: string) => any | any): Graph

可以对边进行计算,获得label信息。

edgeCount(): number

返回图中边个数信息。

edges(): Edge[]

返回图中所有边的对象。

setPath(vs:string[], value?:any):Graph

批量设置边,内部调用的是setEdge(v, w, value)

edge(vOrEdge: s| e, w?: s, name?: s): 获得边的配置信息

edge(vOrEdge: string | Edge, w?: string, name?: string): any

传入参数可以是

  • Eid

  • 获得 唯一的决定变量 (v, w, name)

  • 或者是边对象

hasEdge()判断边是否存在

hasEdge(vOrEdge: string | Edge, w: string, name?: string): boolean

inEdges(v:string, u?:string):Edge[] 找某个节点入边

查找某个节点的入边,如果不传第二个参数就是节点的全部返回

outEdges(v: string, w?: string): Edge[] | void 查找某个节点出边

nodeEdges(v: string, w?: string): Edge[]

返回v->w节点的所有边。

如果w不传 则是节点的全部边(包含 出 和 入所有的边)