图
图是一种比线性表和树更复杂的数据结构,在图中,结点之间的关系是任意的,任意两个数据元素之间都可能相关。图是一种多对多的数据结构。
基本概念
图是由顶点的有穷非空集合以及顶点的边的集合组成,通常表示为G(V,E)
; V是顶点的集合、E是边的集合;
线性表中可以没有元素,称为空表。树中可以没有结点,叫做空树。但是在图中不允许没有顶点,可以没有边。
基本术语
-
无向边
:若顶点Vi和Vj之间的边没有方向,称这条边为无向边(Edge),用(Vi,Vj)
来表示。 -
无向图
(Undirected graphs):图中任意两个顶点的边都是无向边。 -
有向边
:若从顶点Vi到Vj的边有方向,称这条边为有向边,也称为弧(Arc),用<Vi, Vj>
来表示,其中Vi称为弧尾
(Tail),Vj称为弧头
(Head)。 -
有向图
(Directed graphs):图中任意两个顶点的边都是有向边。 -
简单图
:不存在自环(顶点到其自身的边)和重边(完全相同的边)的图
-
无向完全图
:无向图中,任意两个顶点之间都存在边。 -
有向完全图
:有向图中,任意两个顶点之间都存在方向相反的两条弧。 -
稀疏图
;有很少条边或弧的图称为稀疏图,反之称为稠密图。 -
权
(Weight):表示从图中一个顶点到另一个顶点的距离或耗费。 -
网
:带有权重的图 - 度:与特定顶点相连接的边数;
-
出度、入度
:有向图中的概念,出度表示以此顶点为起点的边的数目,入度表示以此顶点为终点的边的数目; -
环
:第一个顶点和最后一个顶点相同的路径; -
简单环
:除去第一个顶点和最后一个顶点后没有重复顶点的环; -
连通图
:任意两个顶点都相互连通的图; -
极大连通子图
:包含尽可能多的顶点(必须是连通的),即找不到另外一个顶点,使得此顶点能够连接到此极大连通子图的任意一个顶点; -
连通分量
:极大连通子图的数量; -
强连通图
:此为有向图的概念,表示任意两个顶点a,b,使得a能够连接到b,b也能连接到a 的图; -
生成树
:n个顶点,n-1条边,并且保证n个顶点相互连通(不存在环); -
最小生成树
:此生成树的边的权重之和是所有生成树中最小的; - AOV网(Activity On Vertex Network ):在有向图中若以顶点表示活动,有向边表示活动之间的先后关系
- AOE网(Activity On Edge Network):在带权有向图中若以顶点表示事件,有向边表示活动,边上的权值表示该活动持续的时间
图的存储结构
1、邻接矩阵
邻接矩阵使用一个一维数组来存储顶点信息,一个二维数组来存储图中边或弧的信息。
- 无向图
无向图由于不分方向,所以是一个对称矩阵。主对角线上全是0表示途中没有自环。
- 有向图
带有权值的有向图中,数字表示权重,无穷表示弧不存在。这是因为0页有可能是权值,所以不存在使用无穷大表示的。
优缺点:
-
优点:
- 便于判断两点是否有边
- 便于计算一个点的度,对于无向图,该点所在的那一行,就表示它的度。对于有向图,对应的那一行的个数是他的出度,那一列的个数是入读。
-
缺点:
- 不便于增加和删除点
- 不便于计算边的数目
- 对于稀疏图,这种实现方式将浪费大量的空间。
代码实现
// 实现
#include <stdio.h>
#include <iostream>
using namespace std;
typedef char VertexType;
typedef int EdegeType;
#define MAXSIZE 100
#define IUNFINITY 65535
typedef struct {
VertexType vexs[MAXSIZE]; // 顶点信息
EdegeType arc[MAXSIZE][MAXSIZE]; // 边或者弧
int vnum,edgenum; // 定义点个边的个数
}MGraphy;
void createGraphy(MGraphy *q) {
cout<<"请输入点的个数和边数"<<endl;
cin>> q->vnum >> q->edgenum;
// 初始化顶点数组
for(int i = 0; i < q->vnum; i++) {
cout<<"请输入第"<<i+1<<"个顶点的信息"<<endl;
cin >> q->vexs[i];
}
// 初始化边/弧
for(int i = 0; i < q->vnum; i++) {
for(int j = 0; j < q->vnum; j++) {
q->arc[i][j] = IUNFINITY;
}
}
// 更改边的信息
for(int j = 0; j <q->edgenum; j++) {
cout<<"请输入第"<<j+1<<"个边的相关信息,例如: 1 1 2,表示(1,1)位置上权重为2"<<endl;
int a , b , c;
cin>>a>>b>>c;
// 有向图
q->arc[a][b] = c;
q->arc[b][a] = c;
// 无向图
// q->arc[a][b] = c;
}
}
void display(MGraphy *q) {
for(int i = 0 ; i < q->vnum; i++) {
for(int j = 0 ; j < q->vnum; j++) {
cout<< q->arc[i][j] <<"\t";
}
cout<<"\n";
}
}
int main() {
MGraphy q;
createGraphy(&q);
display(&q);
return 0;
}
// 结果
// 来一个更全的
#include<iostream>
using namespace std;
enum Graphkind{ DG, DN, UDG, UDN }; //{有向图,无向图,有向网,无向网}
typedef struct Node
{
int * vex; //顶点数组
int vexnum; //顶点个数
int edge; //图的边数
int ** adjMatrix; //图的邻接矩阵
enum Graphkind kind;
}MGraph;
void createGraph(MGraph & G,enum Graphkind kind)
{
cout << "输入顶点的个数" << endl;
cin >> G.vexnum;
cout << "输入边的个数" << endl;
cin >> G.edge;
//输入种类
//cout << "输入图的种类:DG:有向图 DN:无向图,UDG:有向网,UDN:无向网" << endl;
G.kind = kind;
//为两个数组开辟空间
G.vex = new int[G.vexnum];
G.adjMatrix = new int*[G.vexnum];
cout << G.vexnum << endl;
int i;
for (i = 0; i < G.vexnum; i++)
{
G.adjMatrix[i] = new int[G.vexnum];
}
for (i = 0; i < G.vexnum; i++)
{
for (int k = 0; k < G.vexnum; k++)
{
if (G.kind == DG || G.kind == DN)
{
G.adjMatrix[i][k] = 0;
}
else {
G.adjMatrix[i][k] = INT_MAX;
}
}
}
/*//输入每个元素的信息,这个信息,现在还不需要使用
for (i = 0; i < G.vexnum; i++)
{
cin >> G.vex[i];
}*/
cout << "请输入两个有关系的顶点的序号:例如:1 2 代表1号顶点指向2号顶点" << endl;
for (i = 0; i < G.edge; i++)
{
int a, b;
cin >> a;
cin >> b;
if (G.kind == DN) {
G.adjMatrix[b - 1][a - 1] = 1;
G.adjMatrix[a - 1][b - 1] = 1;
}
else if (G.kind == DG)
{
G.adjMatrix[a - 1][b - 1] = 1;
}
else if (G.kind == UDG)
{
int weight;
cout << "输入该边的权重:" << endl;
cin >> weight;
G.adjMatrix[a - 1][b - 1] = weight;
}
else {
int weight;
cout << "输入该边的权重:" << endl;
cin >> weight;
G.adjMatrix[b - 1][a - 1] = weight;
G.adjMatrix[a - 1][b - 1] = weight;
}
}
}
void print(MGraph g)
{
int i, j;
for (i = 0; i < g.vexnum; i++)
{
for (j = 0; j < g.vexnum; j++)
{
if (g.adjMatrix[i][j] == INT_MAX)
cout << "∞" << " ";
else
cout << g.adjMatrix[i][j] << " ";
}
cout << endl;
}
}
void clear(MGraph G)
{
delete G.vex;
G.vex = NULL;
for (int i = 0; i < G.vexnum; i++)
{
delete G.adjMatrix[i];
G.adjMatrix[i] = NULL;
}
delete G.adjMatrix;
}
int main()
{
MGraph G;
cout << "有向图例子:" << endl;
createGraph(G, DG);
print(G);
clear(G);
cout << endl;
cout << "无向图例子:" << endl;
createGraph(G, DN);
print(G);
clear(G);
cout << endl;
cout << "有向图网例子:" << endl;
createGraph(G, UDG);
print(G);
clear(G);
cout << endl;
cout << "无向图网例子:" << endl;
createGraph(G, UDN);
print(G);
clear(G);
cout << endl;
return 0;
}
2、邻接表
引入链式存储结构。他是将图中的每一个顶点都建立了一个链表,把与Vi
相连接的顶点都放在了这个链表中了。这个邻接表可以说是顶点的出度表,邻接表关心了出度,但是如果求入度,则需要遍历整个数组了。
从图中可以看出,顶点是通过一个结点类型的一维数组在存储的,其中的头节点是的firstarc
指向的是第一条依附在该节点上的顶点的信息,表结点的adjvex
表示的是该边的另一个节点的数组下标。nextarc
指向下一个与头节点相连的结点。weight
是权重域,对于普通图没有意义。
- 一个有向图的例子
代码实现
//
#include <iostream>
#include <string>
using namespace std;
typedef char VertexType;
// 表结构
struct arcNode {
int adjvex; // 节点索引
arcNode *nextarc; // 下一个节点
int weight; // 权重
};
// 头节点
struct Vnode{
VertexType data;
arcNode *firstNode;
};
struct Graph{
int kind; // (有向图:0,无向图:1,有向网:2,无向网:3)
int vexnum; // 点数
int edegnum; // 边数
Vnode *node; // 头节点首地址
};
// 创建
void createGraph(Graph *q, int kind) {
cout<<"请输入点数"<<endl;
cin>>q->vexnum;
cout<<"请输入边数"<<endl;
cin>>q->edegnum;
q->kind = kind;
q->node = new Vnode[q->vexnum]; // 动态分配内存
int i ;
cout << "输入每个顶点的信息:" << endl;//记录每个顶点的信息
for(i = 0 ; i < q->vexnum; i++) {
cin >> q->node[i].data;
q->node[i].firstNode = NULL;
}
cout << "请输入每条边的起点和终点的编号:" << endl;
for(i = 0; i<q->edegnum; i++) {
int a,b;
cin>>a >> b; // 起点 终点
arcNode *next = new arcNode;
next->adjvex = b-1;
if(kind == 0 || kind == 1) {
next->weight = -1;
} else {
cout<<"输入权重"<<endl;
cin>> next->weight;
}
next->nextarc = NULL;
// 如果是第一个
if(q->node[a-1].firstNode == NULL) {
q->node[a-1].firstNode = next;
} else {
// 如果不是第一个哦
arcNode *p ;
p = q->node[a-1].firstNode;
while(p->nextarc){
p = p->nextarc;
}
p->nextarc = next;
}
}
}
// 打印
void print(Graph q) {
int i;
cout<<"图的邻接表为"<<endl;
for(i = 0 ; i <q.vexnum; i++ ) {
cout<<q.node[i].data<< " ";
arcNode *p;
p = q.node[i].firstNode;
while(p) {
cout<<p->adjvex << " ";
p = p->nextarc;
}
cout<<endl;
}
}
int main() {
Graph g;
cout << "有向图的例子" << endl;
createGraph(&g,0);
print(g);
cout << endl;
cout << "无向图的例子" << endl;
createGraph(&g, 1);
print(g);
cout << endl;
return 0;
}
-
优点
- 便于增删
- 便于统计数目,按找顶点扫描所有边表可以得到边的个数。
- 空间利用率高。但是不适合稠密图
-
缺点
- 不便于判断顶点之间的是否有边,必须要扫描某一个边表。
- 不便于计算顶点的度。如果是无向图,顶点vi的度是第i个边表的结点的个数。如果是有向图,求出度较为方便,但是求出度则较为复杂,因为要遍历整个邻接表。如果是逆邻接表,那么是求入读比较容易,求出度较难。
3、十字链表
十字链表便于计算顶点的入度和出度。它是有向图的一个专门的链表结构。它跟邻接表一样也是两个结构,一个结构用于保存顶点信息,另一个用于保存各边的信息。
同样我们知道头结点就是用于保存每个顶点信息的结构,其中data主要是保存顶点的信息(如顶点的名称),firstin
是保存第一个入度的边的信息,firstout
保存第一个出度的边的信息。其中,表结点就是记录每条边的信息,其中tailvex
是记录这条边弧头的顶点的在顶点表中的下标(不是箭头那个),headvex
则是记录弧尾对应的那个顶点在顶点表中的下标(箭头的那个),hlink
是指向具有下一个具有相同的headvex
的表结点,tlink
指向具有相同的tailvex
的表结点,weight
是表示边的权重(网图才需要使用)。
代码实现
//
#include<iostream>
#include<string>
using namespace std;
typedef string Vertextype;
//表结点结构
struct ArcNode {
int tailvex; //弧尾的下标,一般都是和对应的头结点下标相同
int headvex; //弧头的下标
ArcNode * hlink; //指向下一个弧头同为headvex的表结点 ,边是箭头的那边
ArcNode * tlink; //指向下一个弧尾为tailvex的表结点,边不是箭头的那边
int weight; //只有网才会用这个变量
};
//头结点
struct Vnode
{
Vertextype data; //这个是记录每个顶点的信息(现在一般都不需要怎么使用)
ArcNode *firstin; //指向第一条(入度)在该顶点的表结点
ArcNode *firstout; //指向第一条(出度)在该顶点的表结点
};
struct Graph
{
int kind; //图的种类(有向图:0,有向网:1)
int vexnum; //图的顶点数
int edge; //图的边数
Vnode * node; //图的(顶点)头结点数组
};
void createGraph(Graph & g,int kind)
{
cout << "请输入顶点的个数:" << endl;
cin >> g.vexnum;
cout << "请输入边的个数(无向图/网要乘2):" << endl;
cin >> g.edge;
g.kind = kind; //决定图的种类
g.node = new Vnode[g.vexnum];
int i;
cout << "输入每个顶点的信息:" << endl;//记录每个顶点的信息
for (i = 0; i < g.vexnum; i++)
{
cin >> g.node[i].data;
g.node[i].firstin = NULL;
g.node[i].firstout = NULL;
}
cout << "请输入每条边的起点和终点的编号:" << endl;
for (i = 0; i < g.edge; i++)
{
int a, b;
cin >> a;
cin >> b;
ArcNode * next = new ArcNode;
next->tailvex = a - 1; //首先是弧头的下标
next-> headvex = b - 1; //弧尾的下标
//只有网图需要权重信息
if(kind==0)
next->weight = -1;
else
{
cout << "输入该边的权重:" << endl;
cin >> next->weight;
}
next->tlink = NULL;
next->hlink = NULL;
//该位置的顶点的出度还为空时,直接让你fisrstout指针指向新的表结点
//记录的出度信息
if (g.node[a - 1].firstout == NULL)
{
g.node[a - 1].firstout = next;
}
else
{
ArcNode * now;
now = g.node[a - 1].firstout;
while (now->tlink)
{
now = now->tlink;
}
now->tlink = next;
}
//记录某个顶点的入度信息
if (g.node[b - 1].firstin == NULL)
{
g.node[b - 1].firstin = next;
}
else {
ArcNode * now;
now = g.node[b - 1].firstin;
while (now->hlink)//找到最后一个表结点
{
now = now->hlink;
}
now->hlink = next;//更新最后一个表结点
}
}
}
void print(Graph g)
{
int i;
cout << "各个顶点的出度信息" << endl;
for (i = 0; i < g.vexnum; i++)
{
cout << g.node[i].data << " ";
ArcNode * now;
now = g.node[i].firstout;
while (now)
{
cout << now->headvex << " ";
now = now->tlink;
}
cout << "^" << endl;
}
cout << "各个顶点的入度信息" << endl;
for (i = 0; i < g.vexnum; i++)
{
cout << g.node[i].data << " ";
ArcNode * now;
now = g.node[i].firstin;
while (now)
{
cout << now->tailvex << " ";
now = now->hlink;
}
cout << "^" << endl;
}
}
int main()
{
Graph g;
cout << "有向图的例子" << endl;
createGraph(g, 0);
print(g);
cout << endl;
return 0;
}
3、邻接多重表
邻接多重表是无向图的另一种链式存储结构。我们之前也说了使用邻接矩阵来存储图比价浪费空间,但是如果我们使用邻接表来存储图时,对于无向图又有一些不便的地方,例如我们需要对一条已经访问过的边进行删除或者标记等操作时,我们除了需要找到表示同一条边的两个结点。这会给我们的程序执行效率大打折扣,所以这个时候,邻接多重表就派上用场啦。
首先,邻接多重表同样是对邻接表的一个改进得到来的结构,它同样需要一个头结点保存每个顶点的信息和一个表结点,保存每条边的信息,他们的结构如下:
其中,头结点的结构和邻接表一样,而表结点中就改变比较大了,其中mark为标志域,例如标志是否已经访问过,ivex和jvex代表边的两个顶点在顶点表中的下标,ilink指向下一个依附在顶点ivex的边,jlink指向下一个依附在顶点jvex的边,weight在网图的时候使用,代表该边的权重。
#include<iostream>
#include<string>
using namespace std;
//表结点
struct ArcNode
{
int mark; //标志位
int ivex; //输入边信息的那个起点
ArcNode * ilink; //依附在顶点ivex的下一条边的信息
int jvex; //输入边信息的那个终点
ArcNode * jlink; //依附在顶点jvex的下一条边的信息
int weight;
};
//头结点
struct VexNode {
string data; //顶点的信息,如顶点名称
ArcNode * firstedge; //第一条依附顶点的边
};
struct Graph {
int vexnum; //顶点的个数
int edge; //边的个数
VexNode *node; //保存顶点信息
};
void createGraph(Graph & g)
{
cout << "请输入顶点的个数:" << endl;
cin >> g.vexnum;
cout << "请输入边的个数(无向图/网要乘2):" << endl;
cin >> g.edge;
g.node = new VexNode[g.vexnum];
int i;
cout << "输入每个顶点的信息:" << endl;//记录每个顶点的信息
for (i = 0; i < g.vexnum; i++)
{
cin >> g.node[i].data;
g.node[i].firstedge = NULL;
}
cout << "请输入每条边的起点和终点的编号:" << endl;
for (i = 0; i < g.edge; i++)
{
int a, b;
cin >> a;
cin >> b;
ArcNode * next = new ArcNode;
next->mark = 0;
next->ivex = a - 1; //首先是弧头的下标
next->jvex = b - 1; //弧尾的下标
next->weight = -1;
next->ilink = NULL;
next->jlink = NULL;
//更新顶点表a-1的信息
if (g.node[a - 1].firstedge == NULL)
{
g.node[a - 1].firstedge = next;
}
else {
ArcNode * now;
now = g.node[a - 1].firstedge;
while (1) {
if (now->ivex == (a - 1) && now->ilink == NULL)
{
now->ilink = next;
break;
}
else if (now->ivex == (a - 1) && now->ilink != NULL) {
now = now->ilink;
}
else if (now->jvex == (a - 1) && now->jlink == NULL)
{
now->jlink = next;
break;
}
else if (now->jvex == (a - 1) && now->jlink != NULL) {
now = now->jlink;
}
}
}
//更新顶点表b-1
if (g.node[b - 1].firstedge == NULL)
{
g.node[b - 1].firstedge = next;
}
else {
ArcNode * now;
now = g.node[b - 1].firstedge;
while (1) {
if (now->ivex == (b - 1) && now->ilink == NULL)
{
now->ilink = next;
break;
}
else if (now->ivex == (b - 1) && now->ilink != NULL) {
now = now->ilink;
}
else if (now->jvex == (b - 1) && now->jlink == NULL)
{
now->jlink = next;
break;
}
else if (now->jvex == (b - 1) && now->jlink != NULL) {
now = now->jlink;
}
}
}
}
}
void print(Graph g)
{
int i;
for (i = 0; i < g.vexnum; i++)
{
cout << g.node[i].data << " ";
ArcNode * now;
now = g.node[i].firstedge;
while (now)
{
cout << "ivex=" << now->ivex << " jvex=" << now->jvex << " ";
if (now->ivex == i)
{
now = now->ilink;
}
else if (now->jvex == i)
{
now = now->jlink;
}
}
cout << endl;
}
}
int main()
{
Graph g;
createGraph(g);
print(g);
system("pause");
return 0;
}
图的遍历
从一个顶点出发,然后访问其余顶点,且每个顶点都必须访问一次。称为时图的遍历
。图的遍历有两种,一种是深度优先搜需(DFS)
、另一种是广度优先搜索(BFS)
。
为了避免重复访问,可设置一个标志顶点是否被访问过的辅助数组 visited []
思路: DFS 在访问图中某一起始顶点 v 后, 由 v 出发, 访问它的任一邻接顶点 w1; 再从 w1 出发,访问与 w1邻 接但还没有访问过的顶点 w2; 然后再从 w2 出发, 进行类似的访问, … 如此进行下去, 直至到达所有的邻接顶点都被访问过的顶点 u 为止。接着, 退回一步, 退到前一次刚访问过的顶点, 看是否还有其它没有被访问的邻接顶点。如果有, 则访问此顶点, 之后再从此顶点出发, 进行与前述类似的访问; 如果没有, 就再退回一步进行搜索。重复上述过程, 直到连通图中所有顶点都被访问过为止。代码实现
- 邻接矩阵的DFS
#include <stdio.h>
#include <iostream>
#include <string>
using namespace std;
typedef char VertexType;
typedef int EdegeType;
#define MAXSIZE 100
#define IUNFINITY 65535
bool visit[MAXSIZE]; // 是否已被遍历
typedef struct {
string vexs[MAXSIZE]; // 顶点信息
EdegeType arc[MAXSIZE][MAXSIZE]; // 边或者弧
int vnum,edgenum; // 定义点个边的个数
}MGraphy;
// 创建无向图
void createGraphy(MGraphy *q) {
cout<<"请输入点的个数和边数"<<endl;
cin>> q->vnum >> q->edgenum;
// 初始化顶点数组
for(int i = 0; i < q->vnum; i++) {
cout<<"请输入第"<<i+1<<"个顶点的信息"<<endl;
cin >> q->vexs[i];
}
// 初始化边/弧
for(int i = 0; i < q->vnum; i++) {
for(int j = 0; j < q->vnum; j++) {
q->arc[i][j] = 0;
}
}
// 更改边的信息
for(int j = 0; j <q->edgenum; j++) {
cout<<"请输入第"<<j+1<<"个边的相关信息,例如: 1 1 ,表示(1,1)位置上"<<endl;
int a , b ;
cin>>a>>b;
q->arc[a][b] = 1;
q->arc[b][a] = 1;
}
}
// 创建有向图
void createGraphy1(MGraphy *q) {
cout<<"请输入点的个数和边数"<<endl;
cin>> q->vnum >> q->edgenum;
// 初始化顶点数组
for(int i = 0; i < q->vnum; i++) {
cout<<"请输入第"<<i+1<<"个顶点的信息"<<endl;
cin >> q->vexs[i];
}
// 初始化边/弧
for(int i = 0; i < q->vnum; i++) {
for(int j = 0; j < q->vnum; j++) {
q->arc[i][j] = 0;
}
}
// 更改边的信息
for(int j = 0; j <q->edgenum; j++) {
cout<<"请输入第"<<j+1<<"个边的相关信息,例如: 1 1 ,表示(1,1)位置上"<<endl;
int a , b ;
cin>>a>>b;
q->arc[a][b] = 1;
}
}
// 输出展示
void display(MGraphy *q) {
for(int i = 0 ; i < q->vnum; i++) {
for(int j = 0 ; j < q->vnum; j++) {
cout<< q->arc[i][j] <<"\t";
}
cout<<"\n";
}
}
// 无向图的深度优先遍历
void DFS_AM(MGraphy q, int v) {
int w ;
visit[v] = true;
cout<<q.vexs[v]<<endl;
for(w = 0; w < q.vnum; w++) {
if(q.arc[v][w]==1 && !visit[w]) {
DFS_AM(q, w);
}
}
}
int main() {
// MGraphy q;
// createGraphy(&q); // 创建无向图
// display(&q);
// cout<<"深度优先遍历无向图"<<endl;
// DFS_AM(q, 0);
MGraphy p;
createGraphy1(&p); // 创建无向图
display(&p);
cout<<"深度优先遍历有向图"<<endl;
DFS_AM(p, 0);
return 0;
}
- 邻接表
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。