数据结构–图(Graph)详解(二)

文章目录

  • 数据结构--图(Graph)详解(二)
    • 一、图的存储结构
      • 1.图的顺序存储法
      • 2.图的邻接表存储法
      • 3.图的十字链表存储法
      • 4.图的邻接多重表存储法
    • 二、图的遍历
      • 1.深度优先搜索(DFS)
      • 2.广度优先搜索(BFS)

一、图的存储结构

1.图的顺序存储法

使用图结构表示的数据元素之间虽然具有“多对多”的关系,但是同样可以采用顺序存储,也就是使用数组有效地存储图

  • 使用数组存储图时,需要使用两个数组,一个数组存放图中顶点本身的数据(一维数组),另外一个数组用于存储各顶点之间的关系(二维数组)
  • 存储图中各顶点本身数据,使用一维数组就足够了;存储顶点之间的关系时,要记录每个顶点和其它所有顶点之间的关系,所以需要使用二维数组。
  • 不同类型的图,存储的方式略有不同,根据图有无权,可以将图划分为两大类:图和网 。
  • 图,包括无向图和有向图;网,是指带权的图,包括无向网和有向网。
  • 存储方式的不同,指的是:在使用二维数组存储图中顶点之间的关系时,如果顶点之间存在边或弧,在相应位置用 1 表示,反之用 0 表示
  • 如果使用二维数组存储网中顶点之间的关系,顶点之间如果有边或者弧的存在,在数组的相应位置存储其权值;反之用 0 表示
#define MAX_VERtEX_NUM 20                   //顶点的最大个数
#define VRType int                          //表示顶点之间的关系的变量类型
#define InfoType char                       //存储弧或者边额外信息的指针变量类型
#define VertexType int                      //图中顶点的数据类型typedef enum{DG,DN,UDG,UDN}GraphKind;       //枚举图的 4 种类型typedef struct {VRType adj;                             //对于无权图,用 1 或 0 表示是否相邻;对于带权图,直接为权值。InfoType * info;                        //弧或边额外含有的信息指针
}ArcCell,AdjMatrix[MAX_VERtEX_NUM][MAX_VERtEX_NUM];typedef struct {VertexType vexs[MAX_VERtEX_NUM];        //存储图中顶点数据AdjMatrix arcs;                         //二维数组,记录顶点之间的关系int vexnum,arcnum;                      //记录图的顶点数和弧(边)数GraphKind kind;                         //记录图的种类
}MGraph;


例如,存储图 1 中的无向图(B)时,除了存储图中各顶点本身具有的数据外,还需要使用二维数组存储任意两个顶点之间的关系。

由于 (B) 为无向图,各顶点没有权值,所以如果两顶点之间有关联,相应位置记为 1 ;反之记为 0 。构建的二维数组如图 2 所示。


在此二维数组中,每一行代表一个顶点,依次从 V1 到 V5 ,每一列也是如此。比如 arcs[0][1] = 1 ,表示 V1 和 V2 之间有边存在;而 arcs[0][2] = 0,说明 V1 和 V3 之间没有边。

对于无向图来说,二维数组构建的二阶矩阵,实际上是对称矩阵,在存储时就可以采用压缩存储的方式存储下三角或者上三角。

通过二阶矩阵,可以直观地判断出各个顶点的度,为该行(或该列)非 0 值的和。例如,第一行有两个 1,说明 V1 有两个边,所以度为 2。

存储图 1 中的有向图(A)时,对应的二维数组如图 3 所示:


例如,arcs[0][1] = 1 ,证明从 V1 到 V2 有弧存在。

且通过二阶矩阵,可以很轻松得知各顶点的出度和入度,出度为该行非 0 值的和,入度为该列非 0 值的和。

例如,V1 的出度为第一行两个 1 的和,为 2 ; V1 的入度为第一列中 1 的和,为 1 。所以 V1 的出度为 2 ,入度为 1 ,度为两者的和 3 。

  • 具体C实现代码
#include <stdio.h>
#define MAX_VERtEX_NUM 20                   //顶点的最大个数
#define VRType int                          //表示顶点之间的关系的变量类型
#define InfoType char                       //存储弧或者边额外信息的指针变量类型
#define VertexType int                      //图中顶点的数据类型typedef enum{DG,DN,UDG,UDN}GraphKind;       //枚举图的 4 种类型typedef struct {VRType adj;                             //对于无权图,用 1 或 0 表示是否相邻;对于带权图,直接为权值。InfoType * info;                        //弧或边额外含有的信息指针
}ArcCell,AdjMatrix[MAX_VERtEX_NUM][MAX_VERtEX_NUM];typedef struct {VertexType vexs[MAX_VERtEX_NUM];        //存储图中顶点数据AdjMatrix arcs;                         //二维数组,记录顶点之间的关系int vexnum,arcnum;                      //记录图的顶点数和弧(边)数GraphKind kind;                         //记录图的种类
}MGraph;//根据顶点本身数据,判断出顶点在二维数组中的位置
int LocateVex(MGraph * G,VertexType v){int i=0;//遍历一维数组,找到变量vfor (; i<G->vexnum; i++) {if (G->vexs[i]==v) {break;}}//如果找不到,输出提示语句,返回-1if (i>G->vexnum) {printf("no such vertex.\n");return -1;}return i;
}//构造有向图
void CreateDG(MGraph *G){//输入图含有的顶点数和弧的个数scanf("%d,%d",&(G->vexnum),&(G->arcnum));//依次输入顶点本身的数据for (int i=0; i<G->vexnum; i++) {scanf("%d",&(G->vexs[i]));}//初始化二维矩阵,全部归0,指针指向NULLfor (int i=0; i<G->vexnum; i++) {for (int j=0; j<G->vexnum; j++) {G->arcs[i][j].adj=0;G->arcs[i][j].info=NULL;}}//在二维数组中添加弧的数据for (int i=0; i<G->arcnum; i++) {int v1,v2;//输入弧头和弧尾scanf("%d,%d",&v1,&v2);//确定顶点位置int n=LocateVex(G, v1);int m=LocateVex(G, v2);//排除错误数据if (m==-1 ||n==-1) {printf("no this vertex\n");return;}//将正确的弧的数据加入二维数组G->arcs[n][m].adj=1;}
}//构造无向图
void CreateDN(MGraph *G){scanf("%d,%d",&(G->vexnum),&(G->arcnum));for (int i=0; i<G->vexnum; i++) {scanf("%d",&(G->vexs[i]));}for (int i=0; i<G->vexnum; i++) {for (int j=0; j<G->vexnum; j++) {G->arcs[i][j].adj=0;G->arcs[i][j].info=NULL;}}for (int i=0; i<G->arcnum; i++) {int v1,v2;scanf("%d,%d",&v1,&v2);int n=LocateVex(G, v1);int m=LocateVex(G, v2);if (m==-1 ||n==-1) {printf("no this vertex\n");return;}G->arcs[n][m].adj=1;G->arcs[m][n].adj=1;//无向图的二阶矩阵沿主对角线对称}
}//构造有向网,和有向图不同的是二阶矩阵中存储的是权值。
void CreateUDG(MGraph *G){scanf("%d,%d",&(G->vexnum),&(G->arcnum));for (int i=0; i<G->vexnum; i++) {scanf("%d",&(G->vexs[i]));}for (int i=0; i<G->vexnum; i++) {for (int j=0; j<G->vexnum; j++) {G->arcs[i][j].adj=0;G->arcs[i][j].info=NULL;}}for (int i=0; i<G->arcnum; i++) {int v1,v2,w;scanf("%d,%d,%d",&v1,&v2,&w);int n=LocateVex(G, v1);int m=LocateVex(G, v2);if (m==-1 ||n==-1) {printf("no this vertex\n");return;}G->arcs[n][m].adj=w;}
}//构造无向网。和无向图唯一的区别就是二阶矩阵中存储的是权值
void CreateUDN(MGraph* G){scanf("%d,%d",&(G->vexnum),&(G->arcnum));for (int i=0; i<G->vexnum; i++) {scanf("%d",&(G->vexs[i]));}for (int i=0; i<G->vexnum; i++) {for (int j=0; j<G->vexnum; j++) {G->arcs[i][j].adj=0;G->arcs[i][j].info=NULL;}}for (int i=0; i<G->arcnum; i++) {int v1,v2,w;scanf("%d,%d,%d",&v1,&v2,&w);int m=LocateVex(G, v1);int n=LocateVex(G, v2);if (m==-1 ||n==-1) {printf("no this vertex\n");return;}G->arcs[n][m].adj=w;G->arcs[m][n].adj=w;//矩阵对称}
}void CreateGraph(MGraph *G){//选择图的类型scanf("%d",&(G->kind));//根据所选类型,调用不同的函数实现构造图的功能switch (G->kind) {case DG:return CreateDG(G);break;case DN:return CreateDN(G);break;case UDG:return CreateUDG(G);break;case UDN:return CreateUDN(G);break;default:break;}
}//输出函数
void PrintGrapth(MGraph G)
{for (int i = 0; i < G.vexnum; i++){for (int j = 0; j < G.vexnum; j++){printf("%d ", G.arcs[i][j].adj);}printf("\n");}
}int main() {MGraph G;//建立一个图的变量CreateGraph(&G);//调用创建函数,传入地址参数PrintGrapth(G);//输出图的二阶矩阵return 0;
}

注意:在此程序中,构建无向网和有向网时,对于之间没有边或弧的顶点,相应的二阶矩阵中存放的是 0。

目的只是为了方便查看运行结果,而实际上如果顶点之间没有关联,它们之间的距离应该是无穷大(∞)。

例如,使用上述程序存储图 4(a)的有向网时,存储的两个数组如图 4(b)所示:

  • 简易的Cpp实现
#include <iostream>
#include <vector>
#include <queue>
#include <string>
#include <unordered_map>
using namespace std;namespace matrix
{template<class V,class W,bool D = false>class Graph  {public:Graph(const V* a,int n){_vertexs.reserve(n);_edge.resize(n);for(int i = 0;i < n;i++){_edge[i].resize(n,W());_vertexs.push_back(a[i]);_vertexsIndexMap[a[i]] = i;}}    void AddEdge(const V& src,const V& dest,const W& w){int srcIndex;int descIndex;try {srcIndex = GetVertexsIndex(src);descIndex = GetVertexsIndex(dest);}catch(string er){cout<<"非法顶点"<<endl;exit(1);}_edge[srcIndex][descIndex] = w;if(false == D){_edge[descIndex][srcIndex] = w;}}void DFS(const V& src){int srcIndex;try {srcIndex = GetVertexsIndex(src);                                                                                    }catch(string ex){cout<<ex<<endl;exit(1);}vector<bool> visted(_vertexs.size(),false);_DFS(srcIndex,visted);cout<<endl;}void BFS(const V& src){int srcIndex;try {srcIndex = GetVertexsIndex(src);}catch(string ex){cout<<ex<<endl;exit(1);}vector<bool> visted(_vertexs.size(),false);queue<int> q;q.push(srcIndex);visted[srcIndex] = true;                                                                                                while(!q.empty()){int front = q.front();q.pop();cout<<front<<" : "<<_vertexs[front]<<" ";for(int i = 0;i < _edge[front].size();i++){if(_edge[front][i] != W() && visted[i] == false){q.push(i);visted[i] = true;}}}cout<<endl;}private:void _DFS(int srcIndex,vector<bool> visted){visted[srcIndex] = true;cout<<srcIndex<<" : "<<_vertexs[srcIndex]<<" -> ";for(int i = 0;i < _edge[srcIndex].size();i++){if(_edge[srcIndex][i] != W() && visted[i] == false){_DFS(i,visted);}}}int GetVertexsIndex(const V& v){if(_vertexsIndexMap.find(v) != _vertexsIndexMap.end()){return _vertexsIndexMap[v];}                                                                                                                       else {throw string("非法顶点");//return -1;}}private://顶点的集合                                                                                                                vector<V> _vertexs;//边的集合:邻接矩阵vector<vector<W>> _edge;//通过顶点找下标;保存映射关系unordered_map<V,int> _vertexsIndexMap;        };//end of class Graph
}//end of namespace matrix

2.图的邻接表存储法

  • 邻接表既适用于存储无向图,也适用于存储有向图。
  • 在具体讲解邻接表存储图的实现方法之前,先普及一个"邻接点"的概念。在图中,如果两个点相互连通,即通过其中一个顶点,可直接找到另一个顶点,则称它们互为邻接点
  • 邻接指的是图中顶点之间有边或者弧的存在
  • 邻接表存储图的实现方式是,给图中的各个顶点独自建立一个链表,用节点存储该顶点,用链表中其他节点存储各自的临界点。
  • 与此同时,为了便于管理这些链表,通常会将所有链表的头节点存储到数组中(也可以用链表存储)。
  • 也正因为各个链表的头节点存储的是各个顶点,因此各链表在存储临界点数据时,仅需存储该邻接顶点位于数组中的位置下标即可。

例如,存储图 1a) 所示的有向图,其对应的邻接表如图 1b) 所示:

拿顶点 V1 来说,与其相关的邻接点分别为 V2 和 V3,因此存储 V1 的链表中存储的是 V2 和 V3 在数组中的位置下标 1 和 2。

从图 1 中可以看出,存储各顶点的节点结构分为两部分,数据域和指针域。数据域用于存储顶点数据信息,指针域用于链接下一个节点,如图 2 所示:

在实际应用中,除了图 2 这种节点结构外,对于用链接表存储网(边或弧存在权)结构,还需要节点存储权的值,因此需使用图 3 中的节点结构:

#define  MAX_VERTEX_NUM 20//最大顶点个数
#define  VertexType int//顶点数据的类型
#define  InfoType int//图中弧或者边包含的信息的类型typedef struct ArcNode{int adjvex;//邻接点在数组中的位置下标struct ArcNode * nextarc;//指向下一个邻接点的指针InfoType * info;//信息域
}ArcNode;typedef struct VNode{VertexType data;//顶点的数据域ArcNode * firstarc;//指向邻接点的指针
}VNode,AdjList[MAX_VERTEX_NUM];//存储各链表头结点的数组typedef struct {AdjList vertices;//图中顶点的数组int vexnum,arcnum;//记录图中顶点数和边或弧数int kind;//记录图的种类
}ALGraph;
  • 邻接表计算顶点的出度和入度
  • 使用邻接表计算无向图中顶点的入度和出度会非常简单,只需从数组中找到该顶点然后统计此链表中节点的数量即可
  • 而使用邻接表存储有向图时,通常各个顶点的链表中存储的都是以该顶点为弧尾的邻接点,因此通过统计各顶点链表中的节点数量,只能计算出该顶点的出度,而无法计算该顶点的入度。
  • 对于利用邻接表求某顶点的入度,有两种方式:
  • 遍历整个邻接表中的节点,统计数据域与该顶点所在数组位置下标相同的节点数量,即为该顶点的入度
  • 建立一个逆邻接表,该表中的各顶点链表专门用于存储以此顶点为弧头的所有顶点在数组中的位置下标

比如说,建立一张图 1a) 对应的逆邻接表,如图 4 所示:


对于具有 n 个顶点和 e 条边的无向图,邻接表中需要存储 n 个头结点和 2e 个表结点。在图中边或者弧稀疏的时候,使用邻接表要比前一节介绍的邻接矩阵更加节省空间。

  • 简易Cpp实现
#include <iostream>
#include <vector>
#include <queue>
#include <string>
#include <unordered_map>
using namespace std;namespace linktable
{template<class W>struct LinkEdge{LinkEdge(const int& src,const int& dest,const W& w):_src(src),_dest(dest),_w(w),_next(nullptr){}int _src;int _dest;W _w;LinkEdge<W>* _next;};template<class V,class W,bool D = false>class Graph {typedef LinkEdge<W> Edge;public:Graph(const V* a,int n){_vertexs.reserve(n);_linktable.resize(n,nullptr);for(int i = 0;i < n;i++){_vertexs.push_back(a[i]);_vertexsIndexMap[a[i]] = i;}}void AddEdge(const V& src,const V& dest,const W& w){int srcIndex;int descIndex;try {srcIndex = GetVertexsIndex(src);                                                                                        descIndex = GetVertexsIndex(dest);}catch(string ex){cout<<"非法边界"<<endl;exit(1);}_AddEdge(srcIndex,descIndex,w);if(false == D){_AddEdge(descIndex,srcIndex,w);}}private:void _AddEdge(const int srcIndex,const int destIndex,const W& w){                                                                                                                               Edge* edge = new Edge(srcIndex,destIndex,w);edge->_next = _linktable[srcIndex];_linktable[srcIndex] = edge;}int GetVertexsIndex(const V& v){if(_vertexsIndexMap.find(v) != _vertexsIndexMap.end()){return _vertexsIndexMap[v];}else {throw string("非法顶点");//return -1;}}private:vector<V> _vertexs;//边的集合:邻接表vector<Edge*> _linktable;unordered_map<V,int> _vertexsIndexMap;};//end if class Graph
}//end of namespace linktable 

3.图的十字链表存储法

  • 与邻接表不同,十字链表法仅适用于存储有向图和有向网。不仅如此,十字链表法还改善了邻接表计算图中顶点入度的问题

十字链表存储有向图(网)的方式与邻接表有一些相同,都以图(网)中各顶点为首元节点建立多条链表,同时为了便于管理,还将所有链表的首元节点存储到同一数组(或链表)中。

其中,建立个各个链表中用于存储顶点的首元节点结构如图 1 所示:


从图 1 可以看出,首元节点中有一个数据域和两个指针域(分别用 firstin 和 firstout 表示):

  • firstin 指针用于连接以当前顶点为弧头的其他顶点构成的链表;
  • firstout 指针用于连接以当前顶点为弧尾的其他顶点构成的链表;
  • data 用于存储该顶点中的数据;

由此可以看出,十字链表实质上就是为每个顶点建立两个链表,分别存储以该顶点为弧头的所有顶点和以该顶点为弧尾的所有顶点

注意,存储图的十字链表中,各链表中首元节点与其他节点的结构并不相同,图 1 所示仅是十字链表中首元节点的结构,链表中其他普通节点的结构如图 2 所示:

从图 2 中可以看出,十字链表中普通节点的存储分为 5 部分内容,它们各自的作用是:

  • tailvex 用于存储以首元节点为弧尾的顶点位于数组中的位置下标;
  • headvex 用于存储以首元节点为弧头的顶点位于数组中的位置下标;
  • hlink 指针:用于链接下一个存储以首元节点为弧头的顶点的节点;
  • tlink 指针:用于链接下一个存储以首元节点为弧尾的顶点的节点;
  • info 指针:用于存储与该顶点相关的信息,例如量顶点之间的权值;

比如说,用十字链表存储图 3a) 中的有向图,存储状态如图 3b) 所示:


拿图 3 中的顶点 V1 来说,通过构建好的十字链表得知,以该顶点为弧头的顶点只有存储在数组中第 3 位置的 V4(因此该顶点的入度为 1)

而以该顶点为弧尾的顶点有两个,分别为存储数组第 1 位置的 V2 和第 2 位置的 V3(因此该顶点的出度为 2)。

对于图 3 各个链表中节点来说,由于表示的都是该顶点的出度或者入度,因此没有先后次序之分。

  • 图 3 中十字链表的构建过程转化为 C 语言代码为:
#define  MAX_VERTEX_NUM 20
#define  InfoType int//图中弧包含信息的数据类型
#define  VertexType inttypedef struct ArcBox{int tailvex,headvex;//弧尾、弧头对应顶点在数组中的位置下标struct ArcBox *hlik,*tlink;//分别指向弧头相同和弧尾相同的下一个弧InfoType *info;//存储弧相关信息的指针
}ArcBox;typedef struct VexNode{VertexType data;//顶点的数据域ArcBox *firstin,*firstout;//指向以该顶点为弧头和弧尾的链表首个结点
}VexNode;typedef struct {VexNode xlist[MAX_VERTEX_NUM];//存储顶点的一维数组int vexnum,arcnum;//记录图的顶点数和弧数
}OLGraph;int LocateVex(OLGraph * G,VertexType v){int i=0;//遍历一维数组,找到变量vfor (; i<G->vexnum; i++) {if (G->xlist[i].data==v) {break;}}//如果找不到,输出提示语句,返回 -1if (i>G->vexnum) {printf("no such vertex.\n");return -1;}return i;
}//构建十字链表函数
void CreateDG(OLGraph *G){//输入有向图的顶点数和弧数scanf("%d,%d",&(G->vexnum),&(G->arcnum));//使用一维数组存储顶点数据,初始化指针域为NULLfor (int i=0; i<G->vexnum; i++) {scanf("%d",&(G->xlist[i].data));G->xlist[i].firstin=NULL;G->xlist[i].firstout=NULL;}//构建十字链表for (int k=0;k<G->arcnum; k++) {int v1,v2;scanf("%d,%d",&v1,&v2);//确定v1、v2在数组中的位置下标int i=LocateVex(G, v1);int j=LocateVex(G, v2);//建立弧的结点ArcBox * p=(ArcBox*)malloc(sizeof(ArcBox));p->tailvex=i;p->headvex=j;//采用头插法插入新的p结点p->hlik=G->xlist[j].firstin;p->tlink=G->xlist[i].firstout;G->xlist[j].firstin=G->xlist[i].firstout=p;}
}

4.图的邻接多重表存储法

  • 为了提高在无向图中操作顶点的效率,学习一种适用于存储无向图的方法——邻接多重表

注意,邻接多重表仅适用于存储无向图或无向网。

  • 邻接多重表存储无向图的方式,可看作是邻接表和十字链表的结合
  • 同邻接表和十字链表存储图的方法相同,都是独自为图中各顶点建立一张链表,存储各顶点的节点作为各链表的首元节点,同时为了便于管理将各个首元节点存储到一个数组中。

各首元节点结构如图 1 所示:


图 1 中各区域及其功能为:

  • data:存储此顶点的数据;
  • firstedge:指针域,用于指向同该顶点有直接关联的存储其他顶点的节点。

从图 1 可以看到,邻接多重表采用与邻接表相同的首元节点结构。但各链表中其他节点的结构与十字链表中相同,如图 2 所示:


图 2 节点中各区域及功能如下:

  • mark:标志域,用于标记此节点是否被操作过,例如在对图中顶点做遍历操作时,为了防止多次操作同一节点,mark 域为 0表示还未被遍历;mark 为 1 表示该节点已被遍历;
  • ivex 和 jvex:数据域,分别存储图中各边两端的顶点所在数组中的位置下标;
  • ilink:指针域,指向下一个存储与 ivex 有直接关联顶点的节点;
  • jlink:指针域,指向下一个存储与 jvex 有直接关联顶点的节点;
  • info:指针域,用于存储与该顶点有关的其他信息,比如无向网中各边的权;

综合以上信息,如果我们想使用邻接多重表存储图 3a) 中的无向图,则与之对应的邻接多重表如图 3b) 所示:


从图 3 中,可直接找到与各顶点有直接关联的其他顶点。比如说,与顶点 V1 有关联的顶点为存储在数组下标 1 处的 V2 和数组下标 3 处的 V4,而与顶点 V2 有关联的顶点有 3 个,分别是 V1、V3 和 V5。

图 3 中邻接多重表的整体结构转化为 C 语言代码如下所示:

#define MAX_VERTEX_NUM 20                   //图中顶点的最大个数
#define InfoType int                        //边含有的信息域的数据类型
#define VertexType int                      //图顶点的数据类型typedef enum {unvisited,visited}VisitIf;    //边标志域typedef struct EBox{VisitIf mark;                           //标志域int ivex,jvex;                          //边两边顶点在数组中的位置下标struct EBox * ilink,*jlink;             //分别指向与ivex、jvex相关的下一个边InfoType *info;                         //边包含的其它的信息域的指针
}EBox;typedef struct VexBox{VertexType data;                        //顶点数据域EBox * firstedge;                       //顶点相关的第一条边的指针域
}VexBox;typedef struct {VexBox adjmulist[MAX_VERTEX_NUM];//存储图中顶点的数组int vexnum,degenum;//记录途中顶点个数和边个数的变量
}AMLGraph;

二、图的遍历

1.深度优先搜索(DFS)

  • 深度优先搜索的过程类似于树的先序遍历,首先从例子中体会深度优先搜索。例如图 1 是一个无向图,采用深度优先算法遍历这个图的过程为
  • 首先任意找一个未被遍历过的顶点,例如从 V1 开始,由于 V1 率先访问过了,所以,需要标记 V1 的状态为访问过;
  • 然后遍历 V1 的邻接点,例如访问 V2 ,并做标记,然后访问 V2 的邻接点,例如 V4 (做标记),然后 V8 ,然后 V5 ;
  • 当继续遍历 V5 的邻接点时,根据之前做的标记显示,所有邻接点都被访问过了。
  • 此时,从 V5 回退到 V8 ,看 V8 是否有未被访问过的邻接点,如果没有,继续回退到 V4 , V2 , V1 ;
  • 通过查看 V1 ,找到一个未被访问过的顶点 V3 ,继续遍历,然后访问 V3 邻接点 V6 ,然后 V7 ;
  • 由于 V7 没有未被访问的邻接点,所有回退到 V6 ,继续回退至 V3 ,最后到达 V1 ,发现没有未被访问的;
  • 最后一步需要判断是否所有顶点都被访问,如果还有没被访问的,以未被访问的顶点为第一个顶点,继续依照上边的方式进行遍历。

根据上边的过程,可以得到图 1 通过深度优先搜索获得的顶点的遍历次序为:
V1 -> V2 -> V4 -> V8 -> V5 -> V3 -> V6 -> V7

  • 所谓深度优先搜索,是从图中的一个顶点出发,每次遍历当前访问顶点的临界点,一直到访问的顶点没有未被访问过的临界点为止

  • 然后采用依次回退的方式,查看来的路上每一个顶点是否有其它未被访问的临界点。

  • 访问完成后,判断图中的顶点是否已经全部遍历完成,如果没有,以未访问的顶点为起始点,重复上述过程。

  • 深度优先搜索是一个不断回溯的过程

  • 采用深度优先搜索算法遍历图的实现代码为:

#include <stdio.h>
#define MAX_VERtEX_NUM 20                   //顶点的最大个数
#define VRType int                          //表示顶点之间的关系的变量类型
#define InfoType char                       //存储弧或者边额外信息的指针变量类型
#define VertexType int                      //图中顶点的数据类型typedef enum{false,true}bool;               //定义bool型常量
bool visited[MAX_VERtEX_NUM];               //设置全局数组,记录标记顶点是否被访问过
typedef struct {VRType adj;                             //对于无权图,用 1 或 0 表示是否相邻;对于带权图,直接为权值。InfoType * info;                        //弧或边额外含有的信息指针
}ArcCell,AdjMatrix[MAX_VERtEX_NUM][MAX_VERtEX_NUM];typedef struct {VertexType vexs[MAX_VERtEX_NUM];        //存储图中顶点数据AdjMatrix arcs;                         //二维数组,记录顶点之间的关系int vexnum,arcnum;                      //记录图的顶点数和弧(边)数
}MGraph;//根据顶点本身数据,判断出顶点在二维数组中的位置
int LocateVex(MGraph * G,VertexType v){int i=0;//遍历一维数组,找到变量vfor (; i<G->vexnum; i++) {if (G->vexs[i]==v) {break;}}//如果找不到,输出提示语句,返回-1if (i>G->vexnum) {printf("no such vertex.\n");return -1;}return i;
}//构造无向图
void CreateDN(MGraph *G){scanf("%d,%d",&(G->vexnum),&(G->arcnum));for (int i=0; i<G->vexnum; i++) {scanf("%d",&(G->vexs[i]));}for (int i=0; i<G->vexnum; i++) {for (int j=0; j<G->vexnum; j++) {G->arcs[i][j].adj=0;G->arcs[i][j].info=NULL;}}for (int i=0; i<G->arcnum; i++) {int v1,v2;scanf("%d,%d",&v1,&v2);int n=LocateVex(G, v1);int m=LocateVex(G, v2);if (m==-1 ||n==-1) {printf("no this vertex\n");return;}G->arcs[n][m].adj=1;G->arcs[m][n].adj=1;//无向图的二阶矩阵沿主对角线对称}
}int FirstAdjVex(MGraph G,int v)
{//查找与数组下标为v的顶点之间有边的顶点,返回它在数组中的下标for(int i = 0; i<G.vexnum; i++){if( G.arcs[v][i].adj ){return i;}}return -1;
}int NextAdjVex(MGraph G,int v,int w)
{//从前一个访问位置w的下一个位置开始,查找之间有边的顶点for(int i = w+1; i<G.vexnum; i++){if(G.arcs[v][i].adj){return i;}}return -1;
}void visitVex(MGraph G, int v){printf("%d ",G.vexs[v]);
}void DFS(MGraph G,int v){visited[v] = true;//标记为truevisitVex( G,  v); //访问第v 个顶点//从该顶点的第一个边开始,一直到最后一个边,对处于边另一端的顶点调用DFS函数for(int w = FirstAdjVex(G,v); w>=0; w = NextAdjVex(G,v,w)){//如果该顶点的标记位false,证明未被访问,调用深度优先搜索函数if(!visited[w]){DFS(G,w);}}
}//深度优先搜索
void DFSTraverse(MGraph G){//int v;//将用做标记的visit数组初始化为falsefor( v = 0; v < G.vexnum; ++v){visited[v] = false;}//对于每个标记为false的顶点调用深度优先搜索函数for( v = 0; v < G.vexnum; v++){//如果该顶点的标记位为false,则调用深度优先搜索函数if(!visited[v]){DFS( G, v);}}
}int main() {MGraph G;//建立一个图的变量CreateDN(&G);//初始化图DFSTraverse(G);//深度优先搜索图return 0;
}

2.广度优先搜索(BFS)

  • 广度优先搜索类似于树的层次遍历
  • 从图中的某一顶点出发,遍历每一个顶点时,依次遍历其所有的邻接点,然后再从这些邻接点出发,同样依次访问它们的邻接点
  • 按照此过程,直到图中所有被访问过的顶点的邻接点都被访问到。
  • 最后还需要做的操作就是查看图中是否存在尚未被访问的顶点,若有,则以该顶点为起始点,重复上述遍历的过程。
  • 还拿图 1 中的无向图为例,假设 V1 作为起始点,遍历其所有的邻接点 V2 和 V3
  • 以 V2 为起始点,访问邻接点 V4 和 V5
  • 以 V3 为起始点,访问邻接点 V6 、 V7
  • 以 V4 为起始点访问 V8
  • 以 V5 为起始点,由于 V5 所有的起始点已经全部被访问,所有直接略过
  • V6 和 V7 也是如此。
  • 以 V1 为起始点的遍历过程结束后,判断图中是否还有未被访问的点,由于图 1 中没有了,所以整个图遍历结束。

遍历顶点的顺序为:
V1 -> V2 -> v3 -> V4 -> V5 -> V6 -> V7 -> V8

  • 广度优先搜索的实现需要借助队列这一特殊数据结构,实现代码为:
#include <stdio.h>
#include <stdlib.h>
#define MAX_VERtEX_NUM 20                   //顶点的最大个数
#define VRType int                          //表示顶点之间的关系的变量类型
#define InfoType char                       //存储弧或者边额外信息的指针变量类型
#define VertexType int                      //图中顶点的数据类型typedef enum{false,true}bool;               //定义bool型常量bool visited[MAX_VERtEX_NUM];               //设置全局数组,记录标记顶点是否被访问过typedef struct Queue{VertexType data;struct Queue * next;
}Queue;typedef struct {VRType adj;                             //对于无权图,用 1 或 0 表示是否相邻;对于带权图,直接为权值。InfoType * info;                        //弧或边额外含有的信息指针
}ArcCell,AdjMatrix[MAX_VERtEX_NUM][MAX_VERtEX_NUM];typedef struct {VertexType vexs[MAX_VERtEX_NUM];        //存储图中顶点数据AdjMatrix arcs;                         //二维数组,记录顶点之间的关系int vexnum,arcnum;                      //记录图的顶点数和弧(边)数
}MGraph;//根据顶点本身数据,判断出顶点在二维数组中的位置
int LocateVex(MGraph * G,VertexType v){int i=0;//遍历一维数组,找到变量vfor (; i<G->vexnum; i++) {if (G->vexs[i]==v) {break;}}//如果找不到,输出提示语句,返回-1if (i>G->vexnum) {printf("no such vertex.\n");return -1;}return i;
}//构造无向图
void CreateDN(MGraph *G){scanf("%d,%d",&(G->vexnum),&(G->arcnum));for (int i=0; i<G->vexnum; i++) {scanf("%d",&(G->vexs[i]));}for (int i=0; i<G->vexnum; i++) {for (int j=0; j<G->vexnum; j++) {G->arcs[i][j].adj=0;G->arcs[i][j].info=NULL;}}for (int i=0; i<G->arcnum; i++) {int v1,v2;scanf("%d,%d",&v1,&v2);int n=LocateVex(G, v1);int m=LocateVex(G, v2);if (m==-1 ||n==-1) {printf("no this vertex\n");return;}G->arcs[n][m].adj=1;G->arcs[m][n].adj=1;//无向图的二阶矩阵沿主对角线对称}
}int FirstAdjVex(MGraph G,int v)
{//查找与数组下标为v的顶点之间有边的顶点,返回它在数组中的下标for(int i = 0; i<G.vexnum; i++){if( G.arcs[v][i].adj ){return i;}}return -1;
}int NextAdjVex(MGraph G,int v,int w)
{//从前一个访问位置w的下一个位置开始,查找之间有边的顶点for(int i = w+1; i<G.vexnum; i++){if(G.arcs[v][i].adj){return i;}}return -1;
}//操作顶点的函数
void visitVex(MGraph G, int v){printf("%d ",G.vexs[v]);
}//初始化队列
void InitQueue(Queue ** Q){(*Q)=(Queue*)malloc(sizeof(Queue));(*Q)->next=NULL;
}//顶点元素v进队列
void EnQueue(Queue **Q,VertexType v){Queue * element=(Queue*)malloc(sizeof(Queue));element->data=v;Queue * temp=(*Q);while (temp->next!=NULL) {temp=temp->next;}temp->next=element;
}//队头元素出队列
void DeQueue(Queue **Q,int *u){(*u)=(*Q)->next->data;(*Q)->next=(*Q)->next->next;
}//判断队列是否为空
bool QueueEmpty(Queue *Q){if (Q->next==NULL) {return true;}return false;
}//广度优先搜索
void BFSTraverse(MGraph G){//int v;//将用做标记的visit数组初始化为falsefor( v = 0; v < G.vexnum; ++v){visited[v] = false;}//对于每个标记为false的顶点调用深度优先搜索函数Queue * Q;InitQueue(&Q);for( v = 0; v < G.vexnum; v++){if(!visited[v]){visited[v]=true;visitVex(G, v);EnQueue(&Q, G.vexs[v]);while (!QueueEmpty(Q)) {int u;DeQueue(&Q, &u);u=LocateVex(&G, u);for (int w=FirstAdjVex(G, u); w>=0; w=NextAdjVex(G, u, w)) {if (!visited[w]) {visited[w]=true;visitVex(G, w);EnQueue(&Q, G.vexs[w]);}}}}}
}int main() {MGraph G;//建立一个图的变量CreateDN(&G);//初始化图BFSTraverse(G);//广度优先搜索图return 0;
}

数据结构--图(Graph)详解(二)相关推荐

  1. 数据结构--图(Graph)详解(四)

    数据结构–图(Graph)详解(四) 文章目录 数据结构--图(Graph)详解(四) 一.图中几个NB的算法 1.普里姆算法(Prim算法)求最小生成树 2.克鲁斯卡尔算法(Kruskal算法)求最 ...

  2. 数据结构--图(Graph)详解(三)

    数据结构–图(Graph)详解(三) 文章目录 数据结构--图(Graph)详解(三) 一.深度优先生成树和广度优先生成树 1.铺垫 2.非连通图的生成森林 3.深度优先生成森林 4.广度优先生成森林 ...

  3. 数据结构--图(Graph)详解(一)

    数据结构–图(Graph)详解(一) 文章目录 数据结构--图(Graph)详解(一) 一.图的基本概念 1.图的分类 2.弧头和弧尾 3.入度和出度 4.(V1,V2) 和 < V1,V2 & ...

  4. echart关系树状图_echart——关系图graph详解

    VueEchart组件见上一篇 export default { data () { const title = { // show: true, //是否显示 text: "画布关系图&q ...

  5. 红黑树详解(二)红黑树的插入(附动图和案例)

    红黑树详解(二)红黑树的插入(附动图和案例) 摘要: 在很多源码涉及到大量数据处理的时候,通常都是用红黑树这一数据结构.红黑树是一种自平衡的二叉查找树,它能在进行插入和删除操作时通过特定操作保持二叉查 ...

  6. OS--进程间通信详解(二)

    OS–进程间通信详解(二) 文章目录 OS--进程间通信详解(二) 一.进程间通信 1.互斥量 Futexes Pthreads中的互斥量 2.管程 3.消息传递 消息传递系统的设计要点 用消息传递解 ...

  7. Pytorch|YOWO原理及代码详解(二)

    Pytorch|YOWO原理及代码详解(二) 本博客上接,Pytorch|YOWO原理及代码详解(一),阅前可看. 1.正式训练 if opt.evaluate:logging('evaluating ...

  8. PackageManagerService启动详解(二)之怎么通过packages.xml对已安装应用信息进行持久化管理?

    PKMS启动详解(二)之怎么通过packages.xml对已安装应用信息进行持久化管理? Android PackageManagerService系列博客目录: PKMS启动详解系列博客概要 PKM ...

  9. 数据结构之链表详解(2)——双向链表

    目录 前言 一.双向链表 A.双向链表的含义 B.双向链表的实现 1.双向链表的结构 2.链表的初始化 初始化图解: 函数代码: 3.动态申请节点函数 函数代码: 4.打印双向链表函数 函数代码: 5 ...

最新文章

  1. linux获取目标主机shell,expect案例-批量获取主机并分发密钥
  2. PL/SQL轻量版(四)——存储函数/存储过程与触发器
  3. js中substring和substr的用法
  4. Zabbix介绍及安装部署
  5. 游戏界著名设计师 Cory Schmtiz:“灵感乍现”是设计生涯里的浪漫
  6. linux下的struct sigaction
  7. java制造null异常_Java中NullPointerException的完美解决方案
  8. Linux中级之ansible概念及hoc命令行调用模式
  9. iBATIS的自定义类型处理器TypeHandlerCallback解决乱码
  10. php 利用cookie实现访问次数统计
  11. pymysql.err.OperationalError: 1136, Column count doesn t match value count at row 1
  12. 软件开发常用设计模式—单例模式总结(c++版)
  13. 二分排序法(折半排序)
  14. mysql gui tools ojdbc14.jar_转 OJDBC驱动版本区别 [ojdbc14.jar,ojdbc5.jar跟ojdbc6.jar的区别]...
  15. html如何让窗口不在任务栏显示,小编教你任务栏不显示打开的窗口怎么解决
  16. Chibi Dinos上线薄饼IFO打新,是否值得参与?
  17. 301代码php代码在哪里加,php 301转向实现代码
  18. 利用计算机打开电视盒子,原来还可以把旧笔记本电脑当电视盒子用!
  19. React之Ref如何去使用?
  20. 编程之路第11天:解决此前截屏权限需要手动点击确认问题(打开双线程)

热门文章

  1. 201105阶段二qt创建简单工程
  2. MQTT.fx连接aliyun阿里云的方法
  3. 485 通信注意事项
  4. WIN10安装ubuntu全过程
  5. 7分钟理解JS的节流、防抖及使用场景
  6. mac 键盘映射优化配置
  7. 洗礼灵魂,修炼python(54)--爬虫篇—urllib2模块
  8. 韩国研制出世界最薄光伏电池:厚度仅为人类头发直径百分之一
  9. 在Scrollview中使用AutoLayout
  10. 科技发烧友之单反佳能700d中高端