背景:决策平台中涉及到大量图存储的问题。 这里对图的存储做一个讨论。
代码是对 graphlib 图存储部分进行讨论。 库中还有基于存储之上的图论算法,这里暂时不进行梳理。只关注存储。
图数据结构 是 图位置重排 与 图算法的基础 也是核心。 图数据结构,应该也也包含一系列的操作用于对数据结构的管理。
图分类
-
Directed! //如果图是有向图的话,设置为true。
-
isMultigraph //多重图?,一对结点中存在多个边。a graph that can have more than one edge between the same pair of nodes.
-
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。
进行如下操作:
-
根据eid获得边的配置信息 edge = this._edgeObjs[e];
-
删除边的记录附带信息 delete this._edgeLabels[e]; //删除边描述
-
删除边的信息 delete this._edgeObjs[e]; //删除边
-
decrementOrRemoveEntry(this._preds[w], v); // v->w, w的前置边减少一个
-
decrementOrRemoveEntry(this._sucs[v], w); // v->w, v的后置边减少一个
-
delete this._in[w][e]; //删除掉 w 的进入边 e
-
delete this._out[v][e]; // 删除掉 v的出边 e
-
_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不传 则是节点的全部边(包含 出 和 入所有的边)