图的广度优先遍历与树的宽度优先遍历类似,实现方法也类似。但是相对树,图存在一种特殊情况——环路,环路使得一个已经遍历过的结点,会在其后代结点的子结点中再次被遍历,从而产生多余的读取操作。为了解决这个问题,我们需要为结点设置一个状态以标记其是否已被遍历过。
下面是图和结点及相关定义。
typedef enum VertexColor { Vertex_WHITE = 0, // 未被搜索到 Vertex_BLACK = 1, // 子结点都被搜索完毕 Vertex_GRAY = 2 // 子结点正在被搜索 } VertexColor; typedef struct GNode { int number; // 顶点编号 struct GNode *next; } GNode; typedef struct Vertex { int number; VertexColor color; // 搜索过程标记搜索状态 struct Vertex *p; } Vertex; typedef struct Graph { GNode *LinkTable; Vertex *vertex; int VertexNum; } Graph;上述定义的图用邻接表来表示点和边的关系,每个顶点有编号和颜色两个属性。顶点的p成员变量记录了该顶点的前驱顶点。
/** * 广度优先搜索,要求输入图g的结点编号从1开始 */ void searchByWidthFirst(Graph *g, int start) { int VertexNum = g->VertexNum; Queue *q = initQueue(VertexNum); Vertex *vs = g->vertex; GNode *linkTable = g->LinkTable; Vertex *s = vs + start - 1; for (int i = 0; i < VertexNum; i++) { Vertex *v = vs + i; v->color = Vertex_WHITE; } s->color = Vertex_GRAY; s->p = NULL; enqueue(&q, s->number); while (!isEmpty(q)) { int number = dequeue(&q); Vertex *u = vs + number - 1; GNode *links = linkTable + number - 1; links = links->next; while (links != NULL) { Vertex *v = vs + links->number - 1; if (v->color == Vertex_WHITE) { v->color = Vertex_GRAY; v->p = u; enqueue(&q, links->number); } links = links->next; } u->color = Vertex_BLACK; } }上述广度优先遍历方法先对图的所有顶点的颜色初始化为WHITE,然后将起点顶点入队,开始循环。循环里面,每一次从队列里面取出一个顶点A并展开,然后对展开的顶点依次判断:
如果是白色的顶点,说明该顶点没有被访问过,入队并修改其颜色为GRAY,说明该顶点已经被访问过但是还没有被展开;
否则不做任何处理。
当处理完展开的这些顶点后,就将顶点A的颜色修改为BLACK,说明该顶点已经被展开过了,以后碰到它也不用处理了。以上就是图的广度优先遍历方法对解决“环路引起的重复访问顶点”的问题的处理方法。
下面给出使用searchByWidthFirst方法的例子。
void testGraph() { Graph graph; graph.VertexNum = 5; Vertex v[5]; Vertex v1; v1.number = 1; v1.p = NULL; v[0] = v1; Vertex v2; v2.number = 2; v2.p = NULL; v[1] = v2; Vertex v3; v3.number = 3; v3.p = NULL; v[2] = v3; Vertex v4; v4.number = 4; v4.p = NULL; v[3] = v4; Vertex v5; v5.number = 5; v5.p = NULL; v[4] = v5; graph.vertex = v; GNode nodes[5]; GNode n1; n1.number = 1; GNode n2; n2.number = 2; GNode n3; n3.number = 3; GNode n4; n4.number = 4; GNode n5; n5.number = 5; GNode a; a.number = 3; GNode b; b.number = 4; GNode y; y.number = 5; n1.next = &a; a.next = &b; b.next = &y; y.next = NULL; GNode c; c.number = 3; GNode x; x.number = 4; n2.next = &c; c.next = &x; x.next = NULL; GNode d; d.number = 1; GNode e; e.number = 2; n3.next = &d; d.next = &e; e.next = NULL; GNode f; f.number = 5; GNode g; g.number = 2; GNode j; j.number = 2; n4.next = &f; f.next = &g; g.next = &j; j.next = NULL; GNode h; h.number = 4; GNode i; i.number = 1; n5.next = &h; h.next = &i; i.next = NULL; nodes[0] = n1; nodes[1] = n2; nodes[2] = n3; nodes[3] = n4; nodes[4] = n5; graph.LinkTable = nodes; searchByWidthFirst(&graph, 5); printPath(&graph, 2); }上述示例代码先构造了一个图,然后调用searchByWidthFirst方法从5号结点开始进行广度优先遍历。printPath方法可以输出指定顶点到起点的广度优先遍历得到的路径。有趣的是,广度优先遍历得到的是从一个顶点到其它所有顶点的“无权最短路径”,即经过边数最少的路径。
下面是printPath方法的定义。
// 要求输入图的结点编号从1开始 void printPath(Graph *g, int vertex) { Vertex *vs = g->vertex; Vertex *v = vs + vertex - 1; while (v != NULL) { printf("%d ", v->number); v = v->p; } }下面是上述示例的运行结果。
这就是从2号顶点到5号顶点的“无权最短路径”。
完整的代码可以看到我的github项目 数据结构与算法
这个项目包含了本博客已经介绍了的和没有介绍的以及将要介绍的数据结构与算法的C语言实现,有兴趣的可以star和fork一下哦~