对于图的基础知识,我们主要分两部分:
目录
第一:图的概念
第二:图的储存和基本代码
下面我们先来看第一部分
定义:图示由顶点的有限的非空的集合以及顶点之间的边的集合组成的。
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计科陈昊飏
欢迎大家报考南京大学哈哈