图存储与遍历的一些技巧

1. 广度优先遍历

图的广度优先遍历伪代码如下,其中Q为队列,visited为大小为n的bool数组。

memset(visited, false, n);//n是图G结点个数
for u∈G
if !visited[u]
Q.push(u);
while !Q.empty()
v = Q.pop();
for each v’s neighbors w
if !visited[w]
visited[w] = ture;
Q.push(w);
endif
endfor
endwhile
endif
endfor

1.1 优化队列Q

如代码所示将访问到的结点暂存入队列Q中。队列可以循环队列方式,也可以采用可变大小队列方式。前者可以节省空间,在多次存取队列时还可以提高效率。假设循环队列长度为n,加入每一个时刻在队列中的元素个数均不会超过n,则无需扩展队列长度。而使用后者可变大小队列(非循环),因每次存储元素队尾均需后移,当存储次数超过n(注意队列中的元素不超过n)时,因”溢出”需要扩展队列空间,从而还会导致数据拷贝,因此前者效率高。

而图的广度优先遍历又具有特殊性,因为每个结点进队列仅一次,因此队列的大小设置为图结点个数n,就不会溢出。

更近一步不难发现队列只要有一个队首和一个队尾即可,而若将队列封装成一个类则在图的遍历过程中会浪费大量的队列调用开销,从而降低图遍历的效率。因此在图遍历时,只需申请一块连续的大小为n(图结点个数)的空间,以及一个队尾和一个队首即可。

1.2 优化visited数组

在上述遍历过程中除了队列外还用到了另外一个用于标记结点是否被访问的变量——visited数组。遍历正式开始之前需要visited数组进行初始化,且是不可或缺的。因此,当遍历次数较少或者只有一次时,初始化操作不会影响图遍历效率。但当需要对图连续遍历k遍时,若采用上述方式需要对visited数组初始化k次,显然影响图遍历的效率。

当需要遍历图多次,会浪费大量的时间在对visited进行初始化,因此可以用int类型visited数组替换bool类型的visited数组,且初始化为-1,另外加一个用于标记遍历当前次数的变量visitedIterm,初始化为0。每次开始遍历时,visitedIterm自动加1。

经1.1、1.2优化后,图的广度优先遍历伪代码如下:

memset(visited,-1, n);//n是图G结点个数

visitedIterm =0;

//以下为图的遍历

visitedIterm ++;

for u∈G

head = tail = 0;

if visitedIterm != visited[u]

q[tail++] = u;

while head < tail

v = q[head++];

for each v’s neighbors w

if visitedIterm!= visited[w]

visited[w] = visitedIterm;

q[tail++] = w;

endif

endfor

endwhile

endif

endfor

2. 深度优先遍历

深度优先遍历伪代码如下,其中S为栈,visited为大小为n的bool数组。

memset(visited,false, n);//n是图G结点个数

for u∈G

if !visited[u]

S.push(u);

while !S.empty()

v=S’s first elem;

if v has not-visited neighbor

u=v’s first not-visited neighbor

visited[u] = ture;

S.push(u);

else

S.pop();

易知深度优先遍历与广度优先遍历相似,因此类似1.1也可以申请大小为n的数组来代替封装的栈,从而减少了函数调用的开销。与1.2相同当需要多次深度优先遍历图时,用int类型的visited数组代替bool类型的visited数组。

与广度优先遍历相比,栈中的元素不是访问时就取出,而是在其所有的邻居全部访问完时才出栈,因此S中存储结点和当前处理的孩子两个变量可以加快查找结点未访问孩子的效率。

3. 图的存储

当图规模比较小的时候采用邻接矩阵可以加快图的大部分操作,但当图的规模增长时邻接矩阵O(n2)的存储代价是计算机无法容忍的,因此图数据库中通常采用邻接表的存储图数据。邻接表通常为(结点,边集合)的二元组集合,而边集合也有不同的形式。

设n、m、d分别为图G的结点、边个数以及图中结点最大度。

(1) 链表形式存储。用这种存储方式,动态添加边的代价O(1),查询边的代价O(d),删除边代价O(d)(查找代价O(d)+删除代价O(1)),遍历图代价O(n+m)。

(2) 哈希结构存储。动态添加边的代价O(1),查询边的代价O(1),删除边的代价O(1),遍历图代价O(n+m)。

(3) 静态有序数组存储。这种方法动态添加边代价O(d+log2d),其中O(log2d)为查找代价,O(d)为插入元素其它元素后移的代价。查询边代价O(log2d),删除代价O(d+log2d),遍历代价O(m+n)。另外需要注意当增加边后个数超过数组大小需要对数组进行扩充。

(4) 静态无序数组存储。这种方法动态添加边代价O(1),查找代价O(n),删除代价O(d),遍历代价O(m+n)。类似(3)当增加边后个数超过数组大小需要对数组进行扩充。

不难发现当只需对图进行查找、添加边、删除边时使用(2)哈希结构效率最好。但图的遍历操作是图的最基本操作,例如可达性中,利用DFS求解时需要遍历图,而其他利用建立标签的方式求解可达性的方法在建立索引标签的过程中需要多次的遍历部分或整个图。例外图的其它操作如图匹配也需要对图进行大量的遍历操作。

但从时间复杂度上来说,4种存储方式的时间复杂度均为O(m+n),即只与图规模相关。(1)、(2)均属于链式存储,当访问边由第一到第二条边时可能需要跳跃多个内存地址,此时cache完全失效。而(3)、(4)每个结点的边都存储于连续的内存空间,即对内存友好,cache命中率极大地得到了提高。因此,采用(3)、(4)这两种存储结构遍历时的效率远远高于(1)、(2)。

考虑到内存友好性,有了第五种存储方式:

(5) 边集中存储方式。用一个大小为m的空间存储n个结点的边,同时用大小为n的空间记录该节点的边在边空间中的起始位置,用一个大小为n的空间记录对应结点的出度大小。著名的可达性研究” TF-Label: a Topological-Folding Labeling Scheme for ReachabilityQuerying in a Large Graph-sigmod’13”中的TF算法作者给出的源代码就采用此种形式。

时间: 2024-10-08 09:19:56

图存储与遍历的一些技巧的相关文章

以邻接表作为存储结构的图的深度优先遍历和广度优先遍历(c++版)

一.图的存储 用邻接表法存储图,存储结构分为两部分,一部分为存储图的所有顶点的数组,另一部分为挂载在数组的每个元素后面的用来表示顶点的邻接点的链表. 1.存储顶点的结构单元为: class vnode { public: string nodename; bool visted;//进行图的遍历时用于标记图是否被访问过 node *next; vnode() { visted = false; next = NULL; } }; 链表的结构单元为: class node { public: st

数据结构之图(存储结构、遍历)

新学期开始了,开始专心于技术上了,上学期的寒假总是那么短暂,飘飘乎就这样逝去,今天补补上学期还没学完的数据结构---图,希望能和大家一起探讨,共同进步~ 定义: 图是由顶点集合及顶点间的关系集合组成的一种数据结构. 图的存储结构: 1.1 邻接矩阵 图的邻接矩阵存储方式是用两个数组来表示图.一个一维数组存储图中顶点信息,一个二维数组(邻接矩阵)存储图中的边或弧的信息. 设图G有n个顶点,则邻接矩阵是一个n*n的方阵,定义为: 看一个实例,下图左就是一个无向图. 从上面可以看出,无向图的边数组是一

邻接表存储图,DFS遍历图的java代码实现

import java.util.*; public class Main{ static int MAX_VERTEXNUM = 100; static int [] visited = new int[MAX_VERTEXNUM]; public static void main(String [] args){ Graph G = new Graph(); creatGraph(G); output(G); for(int i=0;i<G.vertex_num;i++) visited[i

数据结构算法之图的存储与遍历(Java)

一:图的分类 1:无向图 即两个顶点之间没有明确的指向关系,只有一条边相连,例如,A顶点和B顶点之间可以表示为 <A, B> 也可以表示为<B, A>,如下所示 2:有向图 顶点之间是有方向性的,例如A和B顶点之间,A指向了B,B也指向了A,两者是不同的,如果给边赋予权重,那么这种异同便更加显著了 =============================================================================================

基于邻接矩阵存储的图的深度优先遍历和广度优先遍历

图的存储结构相比较线性表与树来说就复杂很多,对于线性表来说,是一对一的关系,所以用数组或者链表均可简单存放.树结构是一对多的关系,所以我们要将数组和链表的特性结合在一起才能更好的存放. 那么我们的图,是多对多的情况,另外图上的任何一个顶点都可以被看作是第一个顶点,任一顶点的邻接点之间也不存在次序关系. 仔细观察以下几张图,然后深刻领悟一下: 因为任意两个顶点之间都可能存在联系,因此无法以数据元素在内存中的物理位置来表示元素之间的关系(内存物理位置是线性的,图的元素关系是平面的). 如果用多重链表

PTA 邻接矩阵存储图的深度优先遍历

6-1 邻接矩阵存储图的深度优先遍历(20 分) 试实现邻接矩阵存储图的深度优先遍历. 函数接口定义: void DFS( MGraph Graph, Vertex V, void (*Visit)(Vertex) ); 其中MGraph是邻接矩阵存储的图,定义如下: typedef struct GNode *PtrToGNode; struct GNode{ int Nv; /* 顶点数 */ int Ne; /* 边数 */ WeightType G[MaxVertexNum][MaxVe

PTA 邻接表存储图的广度优先遍历(20 分)

6-2 邻接表存储图的广度优先遍历(20 分) 试实现邻接表存储图的广度优先遍历. 函数接口定义: void BFS ( LGraph Graph, Vertex S, void (*Visit)(Vertex) ); 其中LGraph是邻接表存储的图,定义如下: /* 邻接点的定义 */ typedef struct AdjVNode *PtrToAdjVNode; struct AdjVNode{ Vertex AdjV; /* 邻接点下标 */ PtrToAdjVNode Next; /*

6-1 邻接表存储图的广度优先遍历 (20 分)

6-1 邻接表存储图的广度优先遍历 (20 分) 试实现邻接表存储图的广度优先遍历. 函数接口定义: void BFS ( LGraph Graph, Vertex S, void (*Visit)(Vertex) ); 其中LGraph是邻接表存储的图,定义如下: /* 邻接点的定义 */ typedef struct AdjVNode *PtrToAdjVNode; struct AdjVNode{ Vertex AdjV; /* 邻接点下标 */ PtrToAdjVNode Next; /

图的概念、存储及遍历

图的概念.存储及遍历 图是一种特殊的数据结构,由点和边构成,它可以用来描述元素之间的网状关系,这个网状没有顺序,也没有层次,就是简单的把各个元素连接起来.图在我们的生活中也十分常见,地图就是最简单的例子. 图的基本概念: 顶点集合为V,边集合为E的图记作G=(V,E).另外,G=(V,E)的顶点数和边数分别为|V|和|E|.对于两个图G和G',如果G'的顶点集合与边集合均为G的顶点集合与边集合的子集,那么称G'是G的子图.子图实际上就是一张图里面小一点的图,也可以是点,不难理解. 有向图:图的边