首先介绍图:一、图的基本概念图是由顶点集合(vertex)及顶点间的关系集合组成的一种数据结构: Graph=( V, E ) V表示顶点的集合,E表示图的边的集合即顶点之间关系的集合。其中 V = { x | x 某个数据对象} 是顶点的有穷非空集合; E = {(x, y) | x, y V } 或 E = {<x, y> | x, y V && Path (x, y)} 是顶点之间关系的有穷集合,也叫做边(edge)集合。Path (x, y)表示从 x 到 y 的一条单向通路, 它是有方向的。 有向图与无向图 在有向图中,顶点对<x, y>是有序的。在无向图中,顶点对(x, y)是无序的。完全图 若有 n 个顶点的无向图有 n(n-1)/2 条边, 则此图为完全无向图。有 n 个顶点的有向图有n(n-1) 条边, 则此图为完全有向图。邻接顶点 如果 (u, v) 是 E(G) 中的一条边,则称 u 与 v 互为邻接顶点。某些图的边具有与它相关的数, 称之为权。这种带权图叫做网络。子图 设有两个图 G=(V, E) 和 G‘=(V’, E‘)。若 V’ V 且 E‘E, 则称 图G’ 是 图G 的子图顶点的度 一个顶点v的度是与它相关联的边的条数。记作TD(v)。在有向图中, 顶点的度等于该顶点的入度与出度之和。顶点 v 的入度是以 v 为终点的有向边的条数, 记作 ID(v); 顶点 v 的出度是以 v 为始点的有向边的条数, 记作 OD(v)。路径 在图 G=(V, E) 中, 若从顶点 vi 出发, 沿一些边经过一些顶点 vp1, vp2, …, vpm,到达顶点vj。则称顶点序列 ( vi vp1 vp2 ... vpm vj ) 为从顶点vi 到顶点 vj 的路径。它经过的边(vi, vp1)、(vp1, vp2)、...、(vpm, vj)应是属于E的边。路径长度非带权图的路径长度是指此路径上边的条数。带权图的路径长度是指路径上各边的权之和。回路 若路径上第一个顶点 v1 与最后一个顶点vm 重合, 则称这样的路径为回路或环。连通图与连通分量 在无向图中, 若从顶点v1到顶点v2有路径, 则称顶点v1与v2是连通的。如果图中任意一对顶点都是连通的, 则称此图是连通图。非连通图的极大连通子图叫做连通分量。强连通图与强连通分量 在有向图中, 若对于每一对顶点vi和vj, 都存在一条从vi到vj和从vj到vi的路径, 则称此图是强连通图。非强连通图的极大强连通子图叫做强连通分量。生成树 一个连通图的生成树是它的极小连通子图,在n个顶点的情形下,有n-1条边。但有向图则可能得到它的由若干有向树组成的生成森林。
上图可以看成是一个大图,里面有两个子图。
现在测试图的可达性:即一个图中每个顶点之间是否存在着关系,即一个顶点经过N条路径后可以到达另外一个顶点。
代码实现:spark-rdd
var rdd = sc.parallelize( Array((1,2),(1,3),(2,4),(3,4),(4,7), (2,5),(3,6),(7,5),(7,6),(6,8),(5,8),(9,10),(10,11))) val rdd_ = rdd.map(x =>( x._2,x._1)) var oldCount = rdd.count() var newCount = -1L while( oldCount != newCount ) { oldCount = newCount rdd = rdd.union( rdd.join( rdd_ ).map( x => ( x._2._2,x._2._1 ) ) ).distinct( ).cache( ) newCount = rdd.count( ) } newCount = 29
结果集:Array((7,8), (1,6), (7,6), (1,4), (6,8), (1,2), (4,7), (1,3), (2,6), (9,11), (3,7), (2,7), (4,5), (4,6), (3,6), (5,8), (3,4), (10,11), (9,10), (3,5), (2,5), (3,8), (2,8), (7,5), (1,5), (1,8), (1,7), (2,4), (4,8))
其实spark-graphx中提供了相关的API,即图的连通性,也就是求一个图的连通图:
val end: RDD[(VertexId, Int)] = sc.parallelize(Array( (1L, 1), (2L, 1), (3L,1), (4L, 1), (5L,1),(6L, 1), (7L,1),(8L,1),(9L,1),(10L,1),(11L,1))) val link: RDD[Edge[Double]] = sc.parallelize(Array( Edge(1L, 2L, 7.0), Edge(1L, 3L, 9.0), Edge(2L, 4L, 15.0),Edge(3L, 4L, 11.0), Edge(4L, 7L, 14.0),Edge(2L, 5L, 11.0),Edge(3L, 6L, 11.0), Edge(7L, 5L, 6.0), Edge(7L, 6L, 10.0), Edge(6L, 8L, 9.0), Edge(5L, 8L, 2.0),Edge(9L, 10L, 2.0),Edge(10L, 11L, 2.0))) val graph:Graph[Int,Double] = Graph( end, link ) // val graph = GraphLoader.edgeListFile(sc,"hdfs://master:9000/user/graph",false,2) val connect : Graph[VertexId, Double] = graph.connectedComponents()val count = connect.vertices.collect()结果集:Array((4,1), (6,1), (8,1), (10,9), (2,1), (11,9), (1,1), (3,1), (7,1), (9,9), (5,1))
其中点属性相同的点表示在同一张图中。下面分别取出每个子图:
val vert = connect.vertices.map( vertex => vertex._2).distinct().collect()//1,9 val graphs :ArrayBuffer[Graph[VertexId,Double]] = null for( i <- 0 to vert.length-1 ){ graphs += connect.subgraph( vpred = (id, value) => if( value == i) true else false) }下面简单介绍connectedComponents的实现原理:见源码部分:
def run[VD: ClassTag, ED: ClassTag](graph: Graph[VD, ED]): Graph[VertexId, ED] = { val ccGraph = graph.mapVertices { case (vid, _) => vid } def sendMessage(edge: EdgeTriplet[VertexId, ED]): Iterator[(VertexId, VertexId)] = { if (edge.srcAttr < edge.dstAttr) { Iterator((edge.dstId, edge.srcAttr)) } else if (edge.srcAttr > edge.dstAttr) { Iterator((edge.srcId, edge.dstAttr)) } else { Iterator.empty } } val initialMessage = Long.MaxValue Pregel(ccGraph, initialMessage, activeDirection = EdgeDirection.Either)( vprog = (id, attr, msg) => math.min(attr, msg), sendMsg = sendMessage, mergeMsg = (a, b) => math.min(a, b)) } // end of connectedComponents从spark1.4之后采用的是发送消息的模型。接触的spark-graphx,可以说是graphx部分的源码熟读了。如果有不正确的或者更好的方法希望大家能够指出,静候您的指教。后面会补充一些其他的关于图计算的算法,包括类似淘宝二条邻,最短路径(带出路径,打算自己实现,虽然graphx已经实现了)等