数据结构——图篇(邻接矩阵、邻接表、深度优先搜索、广度优先搜索)

数据结构——图篇

基本介绍

描述

图比树更为复杂,展现的是一种多对多的关系,图的结构是任意两个数据对象之间都可能存在某种特定的关系的数据结构

概念

顶点

  • 基本介绍

    • 顶点集合表示为V集合,要求图中顶点至少要有一个,即V集合不能为空集。通常使用|V|来表示顶点的个数,通常使用E(V)来表示图G的顶点集
  • 属性

    • 1.数据:指顶点中存储的数据信息
    • 2.度:指依附于该顶点的边的数目,有向图中顶点的度还可以细分为入度和出度(出度指以该顶点为起点的有向边的数目;入度指以该顶点为终点的有向边的数目),一个图中的入度和出度是相同的,因为一条有向边给一个顶点贡献出度的同时,一定给另一个顶点贡献了入度,即在一个图中,入度和出度是同时产生的,一条边为两个顶点都贡献了度,即一条边为整个图贡献了两个度,于是有等式:
      有向图的入度==有向图的出度==边的个数
  • 特殊名词

    • 邻接点:如果两个顶点相邻,在无向图中称两个点互为邻接点;在有向图中称起点“邻接到”终点,也称终点“邻接自”起点

  • 基本介绍
    • 边集合表示为E集合,边集合可以为空,每一条边可以用一个二元组(v,w)(其中v和w属于集合V),即两顶点确定一条边。通常使用|E|来表示边的个数,通常使用E(G)来表示图G的边集
  • 属性
    • 权(重):指边附带的一个数值信息,比如公路的造价等
  • 特殊名词
    • 无向边:使用()表示,(v,w)和(w,v)表示的是同一条边
    • 有向边:也称为“弧”,两个顶点分别称为起点/弧头、终点/弧尾,使用<>表示,<v,w>和<w,v>表示的是不同的边

  • 基本介绍

    • 图集合表示为G集合,一个图可以用一个二元组(V,E)唯一表示,即边和顶点的集合可以确定一个图
  • 属性

    • 1.连通:用来判断一个图是否为连通图/强连通图,在无向图中,如果两个顶点之间具有路径,则称两个顶点连通;在有向图中,如果两个顶点之间相互之间都具有到对方的路径(因为边为有向边),则称两个顶点连通
    • 2.极小和极大:用于判断一个图是否为极大强连通子图/极大连通子图/极小连通子图(无“极小强连通子图”这个概念!!)。
      极大表示极大边数和极大顶点数,极大顶点数为保持连通状态的最大顶点数,即再加入其他顶点就不能保持连通状态;极大边数为在顶点数确定的情况下,该子图中具有原图中所有和这些顶点有关的边
      极小极小表示能够保持连通状态的最小边数,即再删除一条边就不能保持连通状态,每一条边都是必要存在的,极小的概念只对边进行了约束
    • 3.稠密度:用来衡量一个图是否为稠密图,又称为“平均顶点数”,计算公式为两倍的边数除以顶点数
  • 特殊名词

    • 路径:顶点Vp到顶点Vq之间的路径是指顶点序列Vp,Vi1,Vi2…Vim,Vq,路径长度为该路径所包含的边数。
      简单路径指路径的顶点序列中,除了第一个顶点和最后一个顶点之外,其他顶点不重复出现的路径

    • 回路:又称为环,指代表路径的顶点序列中的第一个顶点和最后一个顶点相同的路径,有向图中的回路的路径长度可以为1(其实就是一条自回路边),无向图中的回路的路径长度必须要大于等于3(因为无向图中的边没有方向属性,路径长度为2的路径v,w,v不会是一个回路)
      如果一个图具有n个顶点,并且具有大于n-1条边,则该回路必定存在环
      简单回路是指代表路径的顶点序列中,除了第一个顶点和最后一个顶点之外,其他顶点不重复出现的回路,实际就是一个特殊的简单路径

    • 简单图:图中无重边(即边的集合E中具有重复的元素)且无自回路边(一条边的起点和终点为同一个顶点的边),数据结构中讨论的图均为简单图

    • 无向图:边是没有方向的属性的,即各个边都是无向边

    • 有向图:所有的边都是具有方向属性的,即各个边都是有向边

    • 网图:每一条边上都具有“权”的图

    • 稠密图:稠密度与顶点数成正比的图,直观地看,就是一个图的边数接近完全图的边数

    • 完全图:在无向图中,如果任意两个顶点之间都存在边,则称为无向完全图,含有n个顶点的无向完全图具有n*(n-1)/2条边;在有向图中,如果任意两个顶点之间都存在方向互反的两条弧,则称为有向完全图,含有n个顶点的有向完全图具有n*(n-1)条边

    • 子图:如果图A的顶点集合和边集合分别都是图B的顶点集合和边集合的子集,则称图A为图B的子图。注意,并非V和E的任何子集联合起来都可以构成G的子图,因为这样的构成的二元组可能甚至都无法构成一个图,即E的子集中的某一些边关联的顶点可能不在这个V的子集中,要构成图,边和顶点二者之间是具有一定约束作用的,不能完全独立地进行考虑
      生成子图是指如果子图A的顶点集合即为图B的顶点集合,则称图A为图B的生成子图,这个概念在后面的生成树中有用到

    • 连通图:任意两个顶点都是连通(指无向图的连通概念)的无向图。一个具有n个顶点的连通图,最大边数为n( n-1)/2(即一个无向完全图),最小边数为n-1

    • 强连通图:任意两个顶点都是连通(指有向图中的连通概念)的有向图。一个具有n个顶点的强连通图,最大边数为n( n-1)(即一个有向完全图),最小边数为n

    • 连通分量(极大连通子图)
      连通的无向图只有一个连通分量,就是其本身;非连通的无向图可以划分成若干个连通子图,具有多个连通分量
      判断条件(必须全部满足):
      1.为一个无向图的子图
      2.极大
      3.为一个连通图

    • 强连通分量(极大强连通子图)
      类似于无向图的连通分量,其实就是有向图版本的"连通分量",连通的有向图只有一个强连通分量,就是其本身;非连通的有向图具有多于一个的强连通分量
      判断条件(必须全部满足):
      1.为一个有向图的子图
      2.极大
      3.为一个强连通图

    • 极小连通子图
      判断条件(必须全部满足):
      1.为一个无向图的子图
      2.极小
      3.为一个连通图

    • 生成树
      无论对于有向图还是无向图,生成树中都没有环路

      无向图中,要明确,只有连通图才具有生成树,非连通图具有的是生成树森林。无向图中的生成树的定义为包含该图所有顶点的一个该图的极小连通子图,具有极小边数(即顶点数-1)
      判断条件(每一个都是充要条件):
      1.具有顶点树-1的边数,没有环
      2.具有顶点数-1的边数,而且是连通的
      3.每一对顶点有且仅有一条路径相连
      4.为连通图,但是删除任意一条边就会导致其不连通

      有向图中的生成树的定义为有一个顶点的入度为0,其他顶点的入度皆为1

1、邻接矩阵(顺序存储)

基本介绍

描述

以矩阵的形式来存储顶点信息和边信息,行编号与列编号均从0开始算起,邻接矩阵中列/行的编号代表顶点,而边体现为邻接矩阵中的每一个元素,邻接矩阵中G [x][y] 对应的元素描述的是 编号为x的顶点 到 编号为y的顶点 的边的情况(顶点编号一般从0开始计算)

小贴士

为什么邻接矩阵中的主对角线上总是没有元素?
根据邻接矩阵的存储方式,主对角线上的元素对应的边的特点是边的两端都是同一个顶点,即都是自回路边。由于我们研究的都是简单图,于是不可能出现自回路边,于是邻接矩阵的主对角线上的元素值必定表示该边不存在,但是我在下面的代码实现并没有考虑这一点,所以在看效果的时候,可能会看到邻接矩阵的主对角线上的元素值不是无穷大(表示该边存在)

不同类型的图中邻接矩阵的特点?
1.无向图:
由于无向图中的边为无向边,于是从顶点A到顶点B有一条边,必定此时从顶点B到顶点A也有一条边,这两个在无向图中就是同一条边,在二维数组的邻接矩阵中体现为两个关于主对角线对称的元素,所以无向图对应的二维数组的邻接矩阵必定为一个对称矩阵(即该矩阵的转置矩阵和该矩阵相等)
对于无向图,使用邻接矩阵表示法也可以使用一维数组进行存储,因为无向图对应的二维数组的邻接矩阵为一个对称矩阵,存在大量冗余信息,其实只需要使用一个一维数组来只存储整个二维数组的对称矩阵的上三角部分或者下三角部分即可,可以节省内存空间

2.有向图:
每一行中的值表示对应行编号的顶点的出度情况
每一列中的值表示对应列编号的顶点的入度情况

3.非网图:
矩阵中的每一个元素值为1(表示存在该边)或者0(表示不存在该边)

4.网图:
矩阵中的每一个元素值为权重(表示存在该边)或者0/无穷(表示不存在该边,无穷为相对的无穷,并不是数据真正达到了无穷大)

为什么网图中表示此边不存在会使用"无穷大"呢?
有的时候,读者可能会把0当作权重,认为存在该边,而且权重为0,于是为了避免曲解意思,有时也使用无穷表示该边不存在,此处的"无穷"表示"绝对不可能作为权重值的一个具体的数值",为一个不可能作为正常输入数据而存在的一个特殊数值,可以和堆中数组下标编号为0位置的那个最大/最小哨兵元素相类比进行理解

为什么在无向图的代码实现中,二维数组的邻接矩阵对于同一条边,要进行两次插入?
因为邻接矩阵作为一种图的存储方式,要讲究普适性,即不管是有向图还是无向图,都可以使用这种方式进行数据从存储,对于无向图而言,确实对于同一条边存储两次,会造成信息冗余,并不是数据存储的最优方案(使用一维数组进行存储,就不会导致冗余信息的出现),但对于有向图而言,边具有方向属性,邻接矩阵中关于主对角线对称的两个元素分别对应的边是方向相反、两端顶点相同的不同边,所以二维数组的邻接矩阵的每一个元素对于有向图都是必要的,不存在冗余情况。我们使用二维数组的邻接矩阵对无向图进行存储,实际就是为了追求普适性而要去迁就有向图,因为有向图的边多了一个方向属性

那么就有问题了,如果对于无向图来说,二维数组的邻接矩阵存在冗余信息,那是否可以在插入边的时候,只进行一次插入,来避免冗余信息?
不行
其实还是代码普适性的问题,由于有向图的边具有方向属性,于是决定了邻接矩阵的各个元素需要代表的信息是从行编号对应的顶点是否具有到达列编号对应的顶点的路径,因为这样就可以把边的方向属性添加到邻接矩阵中了。但是这样做的后果就是:对于无向图,想要使用二维数组的邻接矩阵进行数据存储,就必须进行两次插入,因为如果只插入一次,表示的是从顶点A到顶点B具有路径,而从顶点B到顶点A不具有路径,成为了一个有向图,这是有问题的
邻接矩阵仅仅就是一种存储方式,在具体对图的应用中,二维数组的邻接矩阵的每一个元素值告诉图的应用的代码编写者的是关于两个顶点是否具有可达路径的信息,所以这样存储对于无向图来说确实是造成了冗余,但是为图的应用相关代码的实现带来了便利,比如后面图的遍历操作,对于有向图的代码和对于无向图的深搜代码是完全相同的

代码实现

                                         无向网图

关键参数的宏定义及类型重命名

#define MaxVertexNum 100	//邻接矩阵使用的是静态数组的形式,所以需要先进行元素数目的定义
#define INFINITY 65535		//使用65535表示邻接矩阵中的元素对应的边不存在
typedef int Weight_Type;	//每一条边上的权(重)
typedef int Data_Type;		//顶点存储的数据信息以数字形式呈现
typedef int Vertex;			//使用数字编号表示顶点(从0开始计算)

结构定义

typedef struct MGraphNode		//图的定义
{
	int nv;		//图的顶点数
	int ne;		//图的边数
	Weight_Type G[MaxVertexNum][MaxVertexNum]; //邻接矩阵(保存各个边的权重信息)
	Data_Type* Data;		//指向了保存各个顶点的数据信息区域的指针

}*MGraph;
typedef struct EdgeNode		//在代码实现中起到一个搬运工的作用,把用户输入的边的信息,填入邻接矩阵中
{
	Vertex v1, v2;
	Weight_Type Weight;

}*Edge;

初始化图

MGraph Init_MGraph(int VertexNum)			//进行图的初始化,形成一个图的基本框架,初始化的结果为一个没有边,只有顶点的图,
{											//具体体现为邻接矩阵中的每一个方格的元素值都是无穷大,表示各个顶点之间的边都不存在
	int v1,v2;
	MGraph Graph = (MGraph)malloc(sizeof(struct MGraphNode));
	Graph->ne = 0;
	Graph->nv = VertexNum;
	for (v1 = 0; v1 < Graph->nv; v1++)
		for (v2 = 0; v2 < Graph->nv; v2++)
			Graph->G[v1][v2] = INFINITY;	//由于图只有顶点没有边,于是每一个元素(对应每一条边的状态)都初始化为无穷大
	return Graph;

}

插入边

bool Insert_Edge(MGraph Graph, Edge E)
{
	if (Graph->G[E->v1][E->v2] != INFINITY) return false;
	Graph->G[E->v1][E->v2] = E->Weight;		//表示v1顶点到v2顶点的边的权重填入图的邻接矩阵中

	//因为为无向图,于是在与邻接矩阵的主对角线对称的位置还要进行相应赋值
	Graph->G[E->v2][E->v1] = E->Weight;		//表示v2顶点到v1顶点的边的权重填入图的邻接矩阵中
	Graph->ne ++;	//由于为无向图,于是v2顶点到v1顶点的边和v1顶点到v2顶点的边算是一条边,
					//只是在邻接矩阵中,对v2顶点到v1顶点的边和v1顶点到v2顶点的边做了一个区分(为了兼顾有向图)
	return true;
}

删除边

bool Delete_Edge(MGraph Graph, Edge E)
{
	if (Graph->G[E->v1][E->v2] == INFINITY) return false;	//如果指定要删除的边在图中不存在,则返回false,表示删除失败
	
	Graph->G[E->v1][E->v2] = INFINITY;

	//由于为无向图,于是邻接矩阵中与主对称轴相对称的元素也要赋值为INFINITY
	Graph->G[E->v2][E->v1] = INFINITY;
	Graph->ne--;
	return true;
}

正式构造图

MGraph Build_MGraph()			//调用Init_MGraph建立基本框架以后,让用户输入信息,逐渐填充这个框架,正式建立图
{
	int nv, ne;
	int i;
	Edge E;

	printf("请输入顶点总数:");
	scanf("%d", &nv);	//用户输入顶点总数
	MGraph Graph = Init_MGraph(nv);		//初始化的结果为一个只有顶点,无边的图
	
	printf("请输入边总数:");
	scanf("%d", &ne);	//用户输入边总数
	if (ne != 0)		//根据图的定义,图的边集合可以为空集
	{
		E = (Edge)malloc(sizeof(struct EdgeNode));		//仅仅只需要开辟一个边空间即可,不需要开辟ne个边空间,
														//这个空间就像一辆卡车一样,负责在邻接矩阵和边空间来回运输用户输入的边的数据
		for (i = 0; i < ne; i++)		//循环ne次,不断更新动态开辟的这个边对象空间中的各个成员变量的数值(实现内存空间的复用),
		{				//从而把ne个边的信息全部填入邻接矩阵中
			printf("请依次输入边的信息(以起点 终点 权重的顺序输入):");
			scanf("%d%d%d", &E->v1, &E->v2, &E->Weight);
			Insert_Edge(Graph, E);	
		}
	}

	Graph->Data = (Data_Type*)malloc(nv * sizeof(Data_Type));
	for (i = 0; i < nv; i++)
	{
		printf("请输入顶点%d存储的数据:",i);
		scanf("%d", &Graph->Data[i]);
	}
	return Graph;
}

打印邻接矩阵

void Print_Graph(MGraph Graph)			//打印邻接矩阵
{
	int i, j, k;
	for (i = 0; i < Graph->nv; i++)
	{
		for (j = 0; j < Graph->nv; j++)
		{
			for (k = 0; k < 11; k++)
				printf("=");
		}

		printf("=\n");

		for (j = 0; j < Graph->nv; j++)
		{
			if (Graph->G[i][j] == INFINITY)
				printf("| INFINITY ");
			else
				printf("| %8d ", Graph->G[i][j]);
		}
			

		printf("|");
		printf("\n");
	}
	
	for (j = 0; j < Graph->nv; j++)
	{
		for (k = 0; k < 11; k++)
			printf("=");
	}

	printf("=\n");
}
                                          有向网图

关键参数的宏定义及类型重命名

#define INFINITY 65535
#define MaxVertexNum 100

typedef int Data_Type;
typedef int Vertex;
typedef int Weight_Type;

结构定义

typedef struct MGraphNode
{
	int nv;
	int ne;
	Weight_Type G[MaxVertexNum][MaxVertexNum];
	Data_Type* Data;

}*MGraph;
typedef struct EdgeNode
{
	Vertex v1, v2;
	Weight_Type Weight;

}*Edge;

初始化图

MGraph Init_MGraph(int VertexNum)
{
	int v1, v2;
	MGraph Graph = (MGraph)malloc(sizeof(struct MGraphNode));
	Graph->ne = 0;
	Graph->nv = VertexNum;
	for (v1 = 0; v1 < Graph->nv; v1++)
		for (v2 = 0; v2 < Graph->nv; v2++)
			Graph->G[v1][v2] = INFINITY;

	return Graph;
}

插入边

bool Insert_Edge(MGraph Graph, Edge E)
{
	if (Graph->G[E->v1][E->v2] != INFINITY) return false;
	Graph->G[E->v1][E->v2] = E->Weight;
	//由于为有向图,于是只需要填入一次即可

	Graph->ne++;
	return true;
}

删除边

bool Delete_Edge(MGraph Graph, Edge E)
{
	if (Graph->G[E->v1][E->v2] == INFINITY) return false;
	Graph->G[E->v1][E->v2] = INFINITY;
	Graph->ne--;
	return true;
}

正式构造图

MGraph Build_MGraph()
{
	int nv, ne;
	int i;

	printf("请输入顶点总数:");
	scanf("%d", &nv);
	MGraph Graph = Init_MGraph(nv);

	printf("请输入边总数:");
	scanf("%d", &ne);
	if (ne != 0)
	{
		Edge E = (Edge)malloc(sizeof(struct EdgeNode));
		for (i = 0; i < ne; i++)
		{
			printf("请依次输入边的信息(以起点 终点 权重的顺序输入):");
			scanf("%d%d%d", &E->v1, &E->v2, &E->Weight);
			Insert_Edge(Graph, E);
		}

	}

	Graph->Data = (Data_Type*)malloc(nv * sizeof(Data_Type));
	for (i = 0; i < nv; i++)
	{
		printf("请输入顶点%d存储的数据:", i);
		scanf("%d", &Graph->Data[i]);
	}
	
	return Graph;
}

打印邻接矩阵

void Print_Graph(MGraph Graph)
{
	int i, j, k;
	for (i = 0; i < Graph->nv; i++)
	{
		for (j = 0; j < Graph->nv; j++)
		{
			for (k = 0; k < 11; k++)
				printf("=");
		}

		printf("=\n");

		for (j = 0; j < Graph->nv; j++)
		{
			if (Graph->G[i][j] == INFINITY)
				printf("| INFINITY ");
			else
				printf("| %8d ", Graph->G[i][j]);
		}


		printf("|");
		printf("\n");
	}

	for (j = 0; j < Graph->nv; j++)
	{
		for (k = 0; k < 11; k++)
			printf("=");
	}

	printf("=\n");
}

2、邻接表(顺序存储+链式存储)

基本介绍

描述

邻接表法是对图G中的每一个顶点Vi,将所有邻接于Vi的顶点链成一个单链表,形成顶点的邻接表,再将所有顶点邻接表表头集中构成了一个顶点表,邻接表中各个邻接表表头即为顶点的体现,各个邻接点即为边的体现

概念

顶点表
为一个一维数组,每一个元素都是一个顶点表头结点(邻接表表头)

顶点表头结点(邻接表表头)
每一个顶点表头结点在顶点表中的下标即为此顶点表头结点对应顶点的编号,具有两个区域,一个用来存储顶点的数据信息,另一个用来存储邻接点的地址

邻接点
每一个邻接点体现的是该邻接表表头对应的顶点与其相邻的顶点之间那条边的相关信息(比如边的权重)
具有三个区域,一个用来存储邻接顶点的编号,一个用来存储该邻接点对应的边的信息(比如边的权重),还有一个用来存储这个顶点邻接表中下一个邻接点的地址

邻接表
对于有向图,邻接表反映的是顶点的出度情况
(1)顶点邻接表:
为一条单向链表,邻接点中存储的顶点编号表示的是此顶点邻接表的邻接表表头与这些编号的顶点具有路径可以到达,一个顶点邻接表中各个邻接点是没有直接的必然关系的

(2)图邻接表:
当所有顶点邻接表表头集中构成一个顶点表,图的邻接表就形成了

逆邻接表
对于有向图,逆邻接表反映的是顶点的入度情况
邻接表虽然在空间上有很大的优势,但是对于一个有向图,如果需要查找每个顶点的入度就需要遍历整个邻接表,在效率上很低下的。因此才有了逆邻接表的诞生

小贴士

为什么在无向图的代码实现中,邻接表对于同一条边,要进行两次插入?
这个问题和"在无向图的代码实现中,邻接矩阵对于同一条边,要进行两次插入?"的问题是一样的,因为要追求代码的普适性,使得存储结构中必须要添加边的方向属性,而这对于无向图来说是无用的,会导致冗余信息的出现,但是如果无向图要加入这个框架,使用邻接表进行数据存储,就必须要接受这个冗余,必须对同一条边,要进行两次插入,否则就会导致更大的问题出现

既然顶点表的元素(即邻接表表头)就是该元素所在邻接表表头的头节点,为什么在删除的时候还要对第一个邻接点进行额外判定?头节点不是就是为了简化代码,让第一个邻接点不需要额外判定而出现的吗?
因为邻接表表头并不是我们真正意义上的链表中的头节点,为一个假的头节点,因为邻接表表头对应的结构体类型和链表后续的邻接点对应的结构体类型并不相同(链表中的头节点的描述见数据结构——线性结构篇),于是删除的时候还要对第一个邻接点进行额外判定

代码实现

                                       无向网图

关键参数的宏定义及类型重命名

#define MaxVertexNum 100  //顶点表使用的是静态数组的形式,所以需要先进行元素数目的定义

typedef int Data_Type;	
typedef int Weight_Type;
typedef int Vertex;

结构定义

typedef struct EdgeNode		//在代码实现中起到一个搬运工的作用,把用户输入的边的信息,填入新创建的邻接点中
{
	Vertex v1, v2;
	Weight_Type Weight;

}*Edge;
typedef struct Adjacency_Node		//一个邻接点的结构定义
{
	Vertex AdjV;			//用来存储与 该顶点邻接表表头对应的顶点相邻的顶点编号
	Weight_Type Weight;		//用来存储 该顶点邻接表表头对应的顶点 与 其相邻的顶点 所连接的边的权重
	struct AdjVNode* Next;	//用来存储邻接点的地址

}*AdjVNode;
typedef struct VNode	//顶点表头结点(邻接表表头)定义
{
	Data_Type data;				//用来存储该顶点的数据
	AdjVNode FirstAdjVNode;		//用来存储邻接点地址

}AdjList[MaxVertexNum];		//重命名了一个存储MaxVertexNum个顶点表头结点的【数组类型】
typedef struct LGraphNode	//图的定义
{
	int nv;		//图中顶点的个数
	int ne;		//图中边的个数
	AdjList G;		//邻接表

}*LGraph;

初始化图

LGraph Init_LGraph(int VertexNum)		//进行图的初始化,形成图的顶点表,初始化的结果为一个只有顶点,无边的图,
{										//具体体现为各个顶点邻接表只有表头,每一个邻接表表头的地址区域全部赋值为NULL
	int v;
	LGraph Graph = (LGraph)malloc(sizeof(struct LGraph));
	Graph->ne = 0;
	Graph->nv = VertexNum;
	for (v = 0; v < Graph->nv; v++)
		Graph->G[v].FirstAdjVNode = NULL;		//由于图只有顶点没有边,于是每一个顶点表的元素(即各个顶点邻接表表头)的指针域皆赋值为NULL,即对于每一个顶点来说,没有一个邻接点

	return Graph;
}

判断该边是否存在

bool IsExist(LGraph Graph, Edge E)		//判断Edge E是否已经在图中了,如果存在,则返回true,否则返回false
{
	AdjVNode n = Graph->G[E->v1].FirstAdjVNode;		//由于使用的是邻接表,于是是去找编号为E->v1的顶点的邻接表,
													//遍历单链表查看是否具有到达编号为E->v2的顶点的边
	while (n != NULL)
	{
		if (n->AdjV == E->v2)
			break;

		n = n->Next;
	}

	if (n == NULL)
		return false;
	else
		return true;
}

插入边

bool Insert_Edge(LGraph Graph, Edge E)
{
	if (IsExist(Graph, E)) return false;	//如果该边在图中已经存在,无法进行边插入,返回false


	//下面三行代码表示构造一个新的邻接点
	AdjVNode n1 = (AdjVNode)malloc(sizeof(struct Adjacency_Node));
	n1->AdjV = E->v2;
	n1->Weight = E->Weight;

	//下面两行代码表示根据E->v1,找到对应的顶点邻接表表头,将新的邻接点插入到【该顶点邻接表表头的后一个位置】,这个过程就是链表的结点插入操作
	n1->Next = Graph->G[E->v1].FirstAdjVNode;
	Graph->G[E->v1].FirstAdjVNode = n1;

	//由于为无向图,于是v2顶点到v1顶点的边和v1顶点到v2顶点的边算是一条边,需要同时在两个顶点邻接表中添加邻接点,
	//所以还需要在相应的以顶点表下标为v2的邻接表表头所在的邻接表中插入一个邻接点
	//在邻接表中,对这v1到v2的边和v2到v1的边做了一个区分(为了兼顾有向图)
	AdjVNode n2 = (AdjVNode)malloc(sizeof(struct AdjVNode));
	n2->AdjV = E->v1;
	n2->Weight = E->Weight;

	n2->Next = Graph->G[E->v2].FirstAdjVNode;
	Graph->G[E->v2].FirstAdjVNode = n2;
			
	return true;
}

删除边

bool Delete_Edge(LGraph Graph, Edge E)
{
	if (!IsExist(Graph, E)) return false;	//如果该边在图中不存在,无法进行边删除,返回false
	AdjVNode t = NULL;

	AdjVNode n1 = Graph->G[E->v1].FirstAdjVNode;
	if (n1->AdjV == E->v2)		//针对要删除的邻接点就是该邻接表表头存储的指针所指向的邻接点的情况进行处理
	{
		t = n1;
		Graph->G[E->v1].FirstAdjVNode = n1->Next;
		free(t);

	}
	else		//针对要删除的邻接点不是该邻接表表头存储的指针所指向的邻接点的情况进行处理
	{
		while (n1->Next->AdjV != E->v2)
			n1 = n1->Next;

		t = n1->Next;
		n1->Next = t->Next;
		free(t);


	}

	//由于为无向图,于是需要删除两个顶点邻接表的邻接点

	AdjVNode n2 = Graph->G[E->v2].FirstAdjVNode;
	if (n2->AdjV == E->v1)
	{
		t = n2;
		Graph->G[E->v2].FirstAdjVNode = n2->Next;
		free(t);

	}
	else
	{
		while (n2->Next->AdjV != E->v1)
			n2 = n2->Next;

		t = n2->Next;
		n2->Next = t->Next;
		free(t);

	}

	return true;

}

正式构造图

LGraph Build_LGraph()		//调用init_MGraph建立图的顶点表以后,让用户输入信息,把一个个邻接点插入以各个顶点表元素为表头的邻接表中,正式建立图
{
	int nv, ne;
	int i;
	printf("请输入顶点数:");//用户输入顶点总数
	scanf("%d", &nv);

	LGraph Graph = Init_LGraph(nv);			//初始化的结果为一个只有顶点,无边的图

	printf("请输入边数");	//用户输入边总数
	scanf("%d", &ne);
	if (ne != 0)			//根据图的定义,图的边集合可以为空集
	{
		Edge E = (Edge)malloc(sizeof(struct EdgeNode));		//后面·的代码与邻接矩阵的代码类似,不再赘述
		
		for (i = 0; i < ne; i++)
		{
			printf("请依次输入边的信息(以起点 终点 权重的顺序输入):");
			scanf("%d%d%d", &E->v1, &E->v2, &E->Weight);
			Insert_Edge(Graph, E);
		}
	}

	for (i = 0; i < nv; i++)
	{
		printf("请输入顶点%d存储的数据:", i);
		scanf("%d", &Graph->G[i].data);
	}

	return Graph;
}

打印邻接表

void Print_Graph(LGraph Graph)
{
	int i;
	AdjVNode n = NULL;
	for (i = 0; i < Graph->nv; i++)
	{
		n = Graph->G[i].FirstAdjVNode;
		printf("顶点%d", i);
		while (n != NULL)
		{
			printf(" - 顶点%d", n->AdjV); 
			n = n->Next;
		}
		printf("\n");

	}
}
                                         有向网图

关键参数的宏定义及类型重命名

#define MaxVertexNum 100

typedef int Data_Type;
typedef int Weight_Type;
typedef int Vertex;

结构定义

typedef struct EdgeNode
{
	Vertex v1, v2;
	Weight_Type Weight;

}*Edge;
typedef struct Adjacency_Node
{
	Vertex AdjV;
	Weight_Type Weight;
	struct AdjVNode* Next;

}*AdjVNode;
typedef struct VNode
{
	Data_Type data;
	AdjVNode FirstAdjVNode;

}AdjList[MaxVertexNum];
typedef struct LGraphNode
{
	int nv;
	int ne;
	AdjList G;

}*LGraph;

初始化图

LGraph Init_LGraph(int VertexNum)
{
	int i;
	LGraph Graph = (LGraph)malloc(sizeof(struct LGraphNode));
	Graph->ne = 0;
	Graph->nv = VertexNum;
	for (i = 0; i < Graph->nv; i++)
		Graph->G[i].FirstAdjVNode = NULL;

	return Graph;
}

判断该边是否存在

bool IsExist(LGraph Graph, Edge E)
{
	AdjVNode n = Graph->G[E->v1].FirstAdjVNode;
	while (n != NULL)
	{
		if (n->AdjV == E->v2)
			break;

		n = n->Next;
	}

	if (n == NULL)
		return false;
	else
		return true;
}

插入边

bool Insert_Edge(LGraph Graph, Edge E)
{
	if (IsExist(Graph, E)) return false;

	//由于为有向图,于是只要在一个顶点邻接表中添加邻接点即可
	AdjVNode n = (AdjVNode)malloc(sizeof(struct Adjacency_Node));
	n->AdjV = E->v2;
	n->Next = Graph->G[E->v1].FirstAdjVNode;
	Graph->G[E->v1].FirstAdjVNode = n;
	Graph->ne++;

	return true;
}

删除边

bool Delete_Edge(LGraph Graph, Edge E)
{

	//由于为有向图,于是只需要删除一个顶点邻接表的邻接点

	if (!IsExist(Graph, E)) return false;	//如果该边在图中不存在,无法进行边删除,返回false
	AdjVNode t = NULL;

	AdjVNode n1 = Graph->G[E->v1].FirstAdjVNode;
	if (n1->AdjV == E->v2)		//要删除的邻接点就是该邻接表表头存储的指针所指向的邻接点的情况
	{
		t = n1;
		Graph->G[E->v1].FirstAdjVNode = n1->Next;
		free(t);

	}
	else		//要删除的邻接点不是该邻接表表头存储的指针所指向的邻接点的情况
	{
		while (n1->Next->AdjV != E->v2)
			n1 = n1->Next;

		t = n1->Next;
		n1->Next = t->Next;
		free(t);

	}

	return true;

}

正式构造图

LGraph Build_LGraph()
{
	int ne, nv;
	int i;

	printf("请输入顶点数");
	scanf("%d", &nv);
	LGraph Graph = Init_LGraph(nv);

	printf("请输入边数");
	scanf("%d", &ne);
	if (ne != 0)
	{
		Edge E = (Edge)malloc(sizeof(struct EdgeNode));
		for (i = 0; i < ne; i++)
		{
			printf("请依次输入边的信息(以起点 终点 权重的顺序输入):");
			scanf("%d%d%d", &E->v1, &E->v2, &E->Weight);
			Insert_Edge(Graph, E);
		}
	}

	for (i = 0; i < Graph->nv; i++)
	{
		printf("请输入顶点%d存储的数据:", i);
		scanf("%d", &Graph->G[i].data);
		
	}

	return Graph;

}

打印邻接表

void Print_Graph(LGraph Graph)
{
	int i;
	AdjVNode n = NULL;
	for (i = 0; i < Graph->nv; i++)
	{
		n = Graph->G[i].FirstAdjVNode;
		printf("顶点%d", i);
		while (n != NULL)
		{
			printf(" - 顶点%d", n->AdjV); 
			n = n->Next;
		}
		printf("\n");

	}
}

3、图的遍历

基本介绍

描述

图的遍历是指从图中的任一顶点出发,对图中的所有顶点访问一次并且只有一次的次序序列
图的遍历的实质就是通过边找到邻接点的过程,因此,对于同一种图的存储方式,广搜和深搜遍历图的时间复杂度是相同的,二者的不同之处就在于对于各个顶点的访问顺序的不同。当使用不同图的存储方式时,时间复杂度会有所不同,当使用邻接矩阵的形式来存储图时,时间复杂度为O(|V|^2),因为要访问二维邻接矩阵的所有元素;当使用邻接表的形式来存储图时,时间复杂度为O(|E|+|V|),因为使用邻接表进行存储,访问所有邻接点的时间复杂度为O(|E|)

概念

深度优先搜索(DFS)
为一种递归算法,使用的是递归工作栈(涉及到内存分配方面的知识)
遵循的搜索策略是进行从一个点开始,以一条线的形式进行访问,尽可能“深”地搜索。类似于树的先序遍历
算法思想为对于图G=(V, E),首先访问指定的起始顶点s,然后访问与s邻接且未被访问的任一顶点w1,再访问与w1邻接且未被访问的任一顶点w2…重复上述过程。当不能再继续向下访问时,退回到上一个最近被访问的顶点(“退回到上一个顶点”在递归中体现为递归的条件在循环的过程中持续不满足,递归调用停止,for循环结束,本函数代码执行完毕,于是会返回到上一个调用本函数的递归函数中继续执行其中的代码,即对应的操作就是继续尝试访问上一个顶点的其他邻接点,沿原路进行返回在深搜中是一个很重要的动作),若它还有邻接顶点未被访问过,则从该顶点开始继续上述搜索过程,不断重复上述过程,直到图中所有顶点均被访问过为止
特点:从何处入,就必须从何处出。在递归中体现为从主函数开始调用这个递归函数A,层层递归,调用A函数本身,但是如果要结束整个程序,必须层层从递归函数中出来,进行返回,直到又回到在主函数中被调用的那个A函数中

广度优先搜索(BFS)
为一种非递归算法,使用的是队列先入先出的特点
遵循的搜索策略是从一个面开始(这个面小到极致也就是最开始的点了),以一个面的形式进行访问,尽可能“广”地搜索。类似于树的层序遍历
算法思想为对于图G=(V,E),首先访问起始项点v,再依次访问v的各个未访问过的邻接项点w1,w2,…,wi,然后再依次访问w1,w2,…,wi的的所有未被访问过的邻接顶点,依此类推,直到图中所有顶点都被访问过为止。类似的思想还将应用于Dijkstra单源最短路径算法和Prim最小生成树算法

小贴士

代码实现

基础代码

访问函数(按照实际应用具体来实现该函数)

void visit(Vertex v)
{
	printf("正在访问顶点%d", v);
	return;
}

全局变量(用来记录每一个顶点的访问情况)

bool visited[MaxVertexNum];
深度优先搜索

邻接矩阵

void DFS_M(MGraph Graph, Vertex v)
{
	int i;

	//访问编号为v(编号从0开始)的顶点,并使用全局变量visited进行记录
	visit(v);
	visited[v] = true;

	//选定邻接矩阵的第v+1行,开始对该行的所有元素进行遍历
	for (i = 0; i < Graph->nv; i++)
	{
		if (Graph->G[v][i] != INFINITY && visited[i] == false)	//当元素值不为INFINITY(表示具有路径可以到达对应顶点),并且该顶点未被访问的时候,递归对该顶点进行深搜
		{					             //注意,v和i的位置,必须是v在前,i在后,因为每一行的元素值表示该行对应的顶点是否具有到达其他顶点的路径
			DFS_M(Graph, i);
		}
	}


}

邻接表

void DFS_L(LGraph Graph, Vertex v)
{
	AdjVNode p = NULL;

	//访问编号为v(编号从0开始)的顶点,并使用全局变量visited进行记录
	visit(v);
	visited[v] = true;

	//选定顶点表中的一个元素,进行对应的顶点邻接表元素的遍历
	for (p = Graph->G[v].FirstAdjVNode; p != NULL; p = p->Next)
	{
		if (visited[p->AdjV] == false)		//当邻接顶点未被访问的时候,递归对该顶点进行深搜
			DFS_L(Graph, p->AdjV);	//邻接表就不像邻接矩阵的深搜,邻接表不需要判断是否该顶点具有到达其他顶点的路径,
						//因为邻接表这个数据存储方式就决定了,邻接表头对应的顶点必定有路径可以到达顶点邻接表中的所有邻接点对应的顶点
	}
}
广度优先搜索

邻接矩阵

void BFS_M(MGraph Graph, Vertex v)
{
	visit(v);
	visited[v] = true;

	int i;
	Queue q = Init_Queue(100);
	Push(q, v);

	while (!IsEmpty(q))			//当栈空的时候,结束广搜
	{
		v = Pop(q);				//每一次出栈,就表示开始该出栈顶点的邻接点的搜索
		for (i = 0; i < Graph->nv; i++)			//依次尝试图中的各个顶点
		{
			if (Graph->G[v][i] != INFINITY && visited[v] == false)		//如果顶点v具有去往顶点i的路径,并且该顶点未被访问过,则进行访问,并将此访问过的邻接点其入栈
			{
				visit(v);
				visited[v] = true;

				Push(q, v);
			}
		}
	}
	
}

邻接表

void BFS_L(LGraph Graph, Vertex v)
{
	visit(v);
	visited[v] = true;

	AdjVNode p = NULL;
	Queue q = Init_Queue(100);
	Push(q, v);

	while (!IsEmpty(q))			//当栈空的时候,结束广搜
	{
		v = Pop(q);				//每一次出栈,就表示开始该出栈顶点的邻接点的搜索
		for (p = Graph->G[v].FirstAdjVNode; p != NULL; p = p->Next)		//依次尝试图中的各个邻接点
		{
			if (visited[p->AdjV] == false)			//如果该邻接点未被访问过,则进行访问,并将此访问过的邻接点其入栈
			{
				visit(p->AdjV);
				visited[p->AdjV] = true;

				Push(q, p->AdjV);
			}
		}
	}

}

来自作者的话:
此文章的内容还在逐步修进中,希望各位读者可以不吝赐教,有问题都可以在评论区提出来,我看到了会尽快进行更改

  • 8
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
邻接矩阵实现的深度优先遍历: 1. 从起点开始,将起点标记为已访问 2. 从起点开始,遍历起点的所有未访问的邻结点 3. 对于遍历到的邻结点,重复步骤1和2,直到遍历完整张 邻接矩阵实现的广度优先遍历: 1. 从起点开始,将起点入队 2. 当队列不为空时,取出队头元素 3. 将队头元素标记为已访问 4. 将队头元素的所有未访问的邻结点入队 5. 重复步骤2-4直到遍历完整张 邻接表实现的深度优先遍历: 1. 从起点开始,将起点标记为已访问 2. 从起点开始,遍历起点的所有未访问的邻结点 3. 对于遍历到的邻结点,重复步骤1和2,直到遍历完整张 邻接表实现的广度优先遍历: 1. 从起点开始,将起点入队 2. 当队列不为空时,取出队头元素 3. 将队头元素标记为已访问 4. 将队头元素的所有未访问的邻结点入队 5. 重复步邻接矩阵实现的深度优先遍历: 1. 从第一个顶点开始,标记该顶点已遍历 2. 遍历该顶点的所有邻接顶点,如果该邻接顶点未被遍历,则递归遍历 3. 遍历完所有邻接顶点后,返回上一层递归 邻接矩阵实现的广度优先遍历: 1. 从第一个顶点开始,标记该顶点已遍历 2. 将该顶点的所有邻接顶点加入队列 3. 从队列中取出一个顶点,如果该顶点未被遍历,则标记该顶点已遍历,并将该顶点的所有邻接顶点加入队列 4. 重复步骤3直到队列为空 邻接表实现的深度优先遍历: 1. 从第一个顶点开始,标记该顶点已遍历 2. 遍历该顶点的所有邻接顶点,如果该邻接顶点未被遍历,则递归遍历 3. 遍历完所有邻接顶点后,返回上一层递归 邻接表实现的广度优先遍历: 1. 从第一个顶点开始,标记该顶点已遍历 2. 将该顶点的所有邻接对于邻接矩阵邻接表实现的深度优先遍历,都是首先从起点开始遍历,标记起点为已访问,然后遍历起点的所有未访问的邻结点,对于遍历到的邻结点,重复步骤1和2,直到遍历完整张。对于邻接矩阵邻接表实现的广度优先遍历,都是首先从起点开始遍历,将起点入队,当队列不为空时,取出队头元素,标记为已访问,将队头元素的所有未访问的邻结点入队,重复步骤2-4直到遍历完整张.

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值