图(graph)的基础知识详解

对于图的基础知识,我们主要分两部分:

目录

第一:图的概念

第二:图的储存和基本代码


下面我们先来看第一部分

第一部分:图的概念

定义:图示由顶点的有限的非空的集合以及顶点之间的边的集合组成的。

G(V,E),V是顶点的集合,E是边的集合

:我们分为无向边和有向边(从vi到vj,我们把vi叫做弧尾,vj叫做弧头)

特殊的图:1.无向图中,任意顶点间都存在边,那么它叫做无向完全图;同理可以定义有向完全图。2.有较少的边的图叫稀疏图,较多边的叫稠密图(这些是相对的)3.和树的知识一样,这些边可以带上相关的数据,叫做权4.如果图A是图B的一部分,那么A是B的子图

度:1.无向图中顶点A的度是和顶点A关联的边的数目

2.有向图中,度分为出度和入度。A的入度是以顶点A为头的弧(边)的数目,A的出度是以顶点A为尾的弧的数目。

路径:路径的长度是路径上边的数目

环:第一个顶点和最后一个顶点相同的路径叫做回路或者环

连通图:1,无向图中,任意两点都是连通的,则叫连通图;有向图,任意两点间存在路径则叫强连通图  2.无向图的极大连通子图叫做连通分量(极大其实就是,这个连通子图包含尽可能多的顶点数,当然它本身也得是子图,也得是连通的);同理,可以定义有向图中的强连通分量(就多了一个强字而已)

连通图的生成树:是一个连通子图,它应该是极小的,他含有图中全部n个顶点,有且仅有n-1条边。同样,如果一个有向图恰有一个顶点的入度是0,其他顶点入度都是1,那就成一个有向树了

第二部分:图的储存和基本代码

第一,最简单和自然的——邻接矩阵

需要给出两个数组,一个数组储存顶点信息,一个储存图里面边的信息,边又涉及两个顶点,我们完全可以用一个二维数组去做,而一个二维数组又可以形象地表示为矩阵,于是我们得到了邻接矩阵:

//邻接矩阵
avex[n]//n个顶点
arc[i][j]//想象成矩阵,比如四个顶点得无向图,和他们的边构成正方形,即AB BC CD DA
//我们假设在矩阵里把对角线元素变成0,若这条边存在就是1,没有边就是0
     A   B   C    D
A   0   1   0    1
B   1   0   1    0
C   0   1   0    1
D   1   0   1   0

发现这其实是一个对称的矩阵

下面给代码:

typedef struct
{
	int vexs[MAXVEX]; /* 顶点表 ,int类型是假设的*/
	int arc[MAXVEX][MAXVEX];/* 邻接矩阵,可看作边表 */
	int numNodes, numEdges; /* 图中当前的顶点数和边数  */
}MGraph;

/* 建立无向网图的邻接矩阵表示 */
void CreateMGraph(MGraph *G)
{
	int i,j,k,w;
	printf("输入顶点数和边数:\n");
	scanf("%d,%d",&G->numNodes,&G->numEdges); /* 输入顶点数和边数 */
	for(i = 0;i numNodes;i++) /* 读入顶点信息,建立顶点表 */
		scanf(&G->vexs[i]);
	for(i = 0;i numNodes;i++)
		for(j = 0;j numNodes;j++)
			G->arc[i][j]=GRAPH_INFINITY;	/* 邻接矩阵初始化 */
//!!!!这个GRAPH_INFINITY代表的图中两点没有边,我们给它设置一个特殊数
	for(k = 0;k numEdges;k++) /* 读入numEdges条边,建立邻接矩阵 */
	{
		printf("输入边(vi,vj)上的下标i,下标j和权w:\n");
		scanf("%d,%d,%d",&i,&j,&w); /* 输入边(vi,vj)上的权w */
		G->arc[i][j]=w; 
		G->arc[j][i]= G->arc[i][j]; /* 因为是无向图,矩阵对称 */
	}
}

如果对于有向图,其实本质是一样的,不过我们读入arc边数组的信息是要考虑头尾,比如从A到B有一条边,边上的权是999,那么arc[A][B]=999,而没有B到A的边,arc[B][A]=GRAPH_INFINITY,这是一个我们自己设置的“不可能的数据”,上面那个正方形里面我们就假设为0了

第二:邻接表

我们学链表的时候就已经知道了,哦数组和链式结构的优缺点,图这里也是一样的。

而在矩阵里面我们也发现很多地方我们要设置GRAPH_INFINITY,这浪费了许多空间,所以我们来看一下邻接链表:我们把结点仍然存入数组,而把边存在链表

那我们先瞧一下链表结点的结点:

typedef struct EdgeNode /* 边表结点  */
{
    int adjvex;    /* 邻接点域,存储该顶点对应的下标 */
    int info;        /* 用于存储权值,对于非网图可以不需要 */
    struct EdgeNode *next; /* 链域,指向下一个邻接点 */
}EdgeNode;

这很好理解,那么我们怎么划分链表呢?

答案是每一个顶点都需要一个链表。所以每个顶点就是它自己链表的表头,这也很显然需要数组了

typedef struct VertexNode /* 顶点表结点 */
{
    int data; /* 顶点域,存储顶点信息 */
    EdgeNode *firstedge;/* 边表头指针 */
}VertexNode, AdjList[MAXVEX];

最后由这些顶点的结构组成图

typedef struct
{
    AdjList adjList; 
    int numNodes,numEdges; /* 图中当前顶点数和边数 */
}GraphAdjList;

下面以无向图为例创建:

void  CreateALGraph(GraphAdjList *G)
{
	int i,j,k;
	EdgeNode *e;
	printf("输入顶点数和边数:\n");
	scanf("%d,%d",&G->numNodes,&G->numEdges); /* 输入顶点数和边数 */
	for(i = 0;i < G->numNodes;i++) /* 读入顶点信息,建立顶点表 */
	{
		scanf(&G->adjList[i].data); 	/* 输入顶点信息 */
		G->adjList[i].firstedge=NULL; 	/* 将边表置为空表 */
	}
	
	
	for(k = 0;k < G->numEdges;k++)/* 建立边表 */
	{
		printf("输入边(vi,vj)上的顶点序号:\n");
		scanf("%d,%d",&i,&j); /* 输入边(vi,vj)上的顶点序号 */
		e=(EdgeNode *)malloc(sizeof(EdgeNode)); /* 向内存申请空间,生成边表结点 */
		e->adjvex=j;					/* 邻接序号为j */                         
		e->next=G->adjList[i].firstedge;	/* 将e的指针指向当前顶点上指向的结点 */
		G->adjList[i].firstedge=e;		/* 将当前顶点的指针指向e */               
		
		e=(EdgeNode *)malloc(sizeof(EdgeNode)); /* 向内存申请空间,生成边表结点 */
		e->adjvex=i;					/* 邻接序号为i */                         
		e->next=G->adjList[j].firstedge;	/* 将e的指针指向当前顶点上指向的结点 */
		G->adjList[j].firstedge=e;		/* 将当前顶点的指针指向e */               
	}
}

这是标准c风格的代码,注释它好像有点暗,可能的仔细看一下。

我可以说一下思路,第一部分的循环,是对每个顶点进行初始化,第二部分的循环则是一次次读入边表的信息,注意这里的NULL和动态分配问题,然后按照以前学过的链表,头插法。

而有向图,还是同理的,无非是第二部分循环的条件要变化一下,不是简单的一个边的两个顶点都进行边表的插入了,需要根据这个边的方向来处理,同时还要选择出度来处理还是入度来处理,是的有向图的边表,只能选择一个规则来排序,比如有一条从A-->B的边,有一条C-->A的边,如果我们选择出度作为规则,那么A作为顶点,A-->firstedge.adjvex=B; 而如果我们以入度作为规则,那么A-->firstedge.adjvex=C,显然这两种情况下,顶点A的链表储存内容是不一样的。

第三,十字链表

这其实是一个难点,但同样也是图的储存结构的一个亮点,希望大家看了之后能明白。

上面我们刚刚说了,对于有向图的边表我们只能选择出度或者入度一种规则来处理链表,那么有不有一种更优更方便的方法,让我们能同时知道入度和出度呢?那我们可以把出度的链表和入度的链表结合起来,既然都是指针,我多添一个不就好了。

我们重新定义一下边结点的结构:

typedef struct pparc {
    int tail;
    int head;//一条有向边的信息
    struct pparc* headlink;//入度的链表
    struct pparc* taillink;//出度的链表
}pparc;

那么顶点结构也就很自然了:

typedef struct ppnode {
    int data;
    struct pparc* firstin;//指向入度的
    struct pparc* firstout;//指向出度的
}ppnode[100];

然后图

typedef struct ppgraph{
    ppnode ad;
    int numad, numarc;
}ppgraph;

至于创建的函数就不重复了,依旧是先一个for循环初始化顶点ad[],然后在一个for循环,这个循环里面不过是把有向图的入边和出边操作都做一下,使用个头插法,然后fisrtin 对headlink ,firstout对taillink......都是独立的,真的可以把入边和出边操作直接拼起来。

第四,邻接多重表

终于讲到最后一个重点了。如果我们不单单看储存,我们关注一下对边的操作,即删除或者插入边,而上面的方法中我们对边进行操作其实都是有点麻烦的,那么应该有什么优化的办法呢?

我们可以对边表结点进行一些修改

typedef struct arc {
	int ivex;
	int jvex;
	struct arc* ilink;
	struct arc* jlink;
}arc;

ivex,jvex是与一条边所联系的两个顶点的下标,ilink指向的是依附于顶点ivex的下一条边,jlink指向依附于jvex的下一条边

然后是顶点:

typedef struct gad {
	int data;
	arc* firstedge;
}gad,Ad[100];

其实优点已经显示出来了,不过我们说完再总结。

图的结构:

typedef struct {
	Ad ad;
	int nodenum;
	int arcnum;
}graph;

然后创建的代码:(依旧是无向)

void Creategraphmulti(graph* G) {
	int i, j, k;
	arc* e;
	cout << "请输入结点数和边数" << endl;
	cin >> G->nodenum >> G->arcnum;
	for (i = 0; i < G->nodenum; ++i)
	{
		cin >> G->ad[i].data;
		G->ad[i].firstedge = NULL;
	}
	for (k = 0; k < G->arcnum; ++k)
	{
		cout << "请输入边(v1,v2)的顶点序号" << endl;
		cin >> i >> j;
		e = new arc[1];
		e->ivex = i; e->jvex = j;
		if (!G->ad[i].firstedge) {
			G->ad[i].firstedge = e;
		}
		else {
			e->ilink=G->ad[i].firstedge;
			G->ad[i].firstedge = e;
		}

		if (!G->ad[j].firstedge) {
			G->ad[j].firstedge = e;
		}
		else {
			e->ilink = G->ad[j].firstedge;
			G->ad[j].firstedge = e;
		}
	}
}

我来给大家解释一下:第一个for循环依旧是初始化顶点,第二个就是在分配边的信息,而这里有小小的难点,讲一下两个if的判断:如果这个顶点还没有它的边表或者说是第一次给它分配边的信息,那么我们直接把这个边的结点给这个顶点就行了,而如果不是空的,那么我们要利用头插法对其插入,ilink作为和ivex这个下标的顶点相关的边的位置,只要和ivex关联就行了,jlink也是这样,其实还是很独立清晰地。

那么优点就来了,邻接表里面,无向图的同一条边是有两个结点表示的(有A-B这一条边,那么A的链表里面有,B的链表里面也有),而邻接多重表是一个结点表示。

好了,今天的内容就到这里。不过在最后,我想给出关于十字链表的打印,删除,插入等等操作的代码。但是这一部分的十字链表的插入我以前没有自己亲自写过,这是我以前学习别人时摘录的。【数据结构】十字链表_bible_reader的博客-CSDN博客_十字链表

#include "stdafx.h"
#include 
 
using namespace std;
 
#define MAX_VERTEX_NUM 20
 
typedef int Status;
typedef int infoType;
typedef char vertexType;
 
typedef struct arcBox{
	int tailVex, headVex;
	struct arcBox *hLink, *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 locateVertex(OLGraph &G, vexNode node){
	int index = -1;
	for(int i = 0; i>G.xList[i].data;
		G.xList[i].firstIn = NULL;
		G.xList[i].firstOut = NULL;
	}//初始化
 
	for(int i = 0; i>node1.data>>node2.data;
 
		insertArc(G, node1, node2); 
	}
	return 1;
}
 
Status printDG(OLGraph &G){
	for(int i = 0; i"<<"|"<tailVex<<"|"<headVex;
			ptail = ptail-> tLink;
		}
		cout<<"-->NULL"<"<<"|"<tailVex<<"|"<headVex;
			phead = phead->hLink;
		}
		cout<<"-->NULL"<tailVex = index1;
	pArc->headVex = index2;
	pArc->info = NULL;
 
	arcBox *ptail = G.xList[index1].firstOut;
	arcBox *phead = G.xList[index2].firstIn;
 
	if(!ptail){pArc->tLink = NULL;}
	else{pArc->tLink = ptail;}
 
	if(!phead){pArc->hLink = NULL;}
	else{pArc->hLink = phead;}
 
	G.xList[index1].firstOut = pArc;//链头部插入弧结点
	G.xList[index2].firstIn = pArc;
}
 
Status insertArc(OLGraph &G, vexNode node1, vexNode node2){
 
	int index1 = locateVertex(G, node1);
	int index2 = locateVertex(G, node2);
 
	insertArcAction(G, index1, index2);
	return 1;
}
 
Status insertNode(OLGraph &G, vexNode node){
 
	G.xList[G.vexNum].data = node.data;
	G.xList[G.vexNum].firstIn = NULL;
	G.xList[G.vexNum].firstOut = NULL;
	G.vexNum = G.vexNum + 1;
	return 1;
}
 

谢谢观看

21计科陈昊飏

欢迎大家报考南京大学哈哈

你可能感兴趣的:(算法学习,数据结构,算法,c++)