1、基本概念
图是一种多对多的关系的数据结构
1.2 图的定义
1.2.1 无向边
若顶点
vi
到
vj
之间的边没有方向,这条边称为无向边。如果任意两个顶点之间的边都是无向边,则该图称为无向图。
G=(V, {E}) V={A, B, C} E={(A,B), (B,C), (C,A)}
1.2.2 有向边
若顶点
vi
到
vj
之间的边有方向,这条边称为有向边,也成为弧。如果任意两个顶点之间的边都是有向边,则该图称为有向图。
G=(V, {E}) V={A, B, C} E={<A,B>, <B,C>, <C,A>} <A,B> A称为弧头,B称为弧尾,且顺序不能更改
无向边用() 有向边用<>
1.2.3 完全图
如果无向图的任意两个点之间都存在边,则该图为无向完全图。 n个顶点有(n*(n-1))/2条边
如果有向图的任意两个点都存在互为相反的弧,则该图为有向完全图。n个顶点有n*(n-1)条弧
1.2.4 稀疏图、稠密图
有很少条边或弧的图称为稀疏图,反之称为有向图
1.2.5 权、网
图的边或弧有与它相关的数字,这个数字称为权。带权的图称为网
1.2.6 子图
假设G=(V, {E})和G1=(V1, {E1}) 如果
V1⊆V
且
E1⊆E
,则G1是G的子图
1.3 图的顶点与边的关系
1.3.1 无向图
有G = (V, {E}),如果边(
vi
,
vj
)
⊆
E,则顶点
vi
,
vj
相邻接。边(
vi
,
vj
)依附于顶点
vi
,
vj
。
顶点
vi
的度是和
vi
相关联的边的数目,记为TD(v)
1.3.2 有向图
有G = (V, {E}),如果边<
vi
,
vj
>
⊆
E。以顶点
vi
为头的弧称为入度,记为ID(v)。以顶点
vi
为尾的度称为出度,记为OD(v)
1.3.3 路径
图中顶点到顶点之间的路径不是唯一的
路径的长度是路径上边或者弧的数目
回环:第一个顶点与最后一个顶点相同
简单路径:序列中,顶点不重复出现的路径
简单回路:除了第一个顶点与最后一个顶点之外,其余顶点不重复的回路
1.4 连通图
1.4.1 无向图
在无向图G中,如果顶点v,v1有路径,则成v和v1是连通的
对于图中,任意两个顶点(
vi
,
vj
)
⊆
E,
vi
,
vj
都是连通的,则称该图为连通图
无向图中的极大连通子图称为连通分量
1.4.2 有向图
在有向图G中,如果对于每一对(
vi
,
vj
)
⊆
E,
vi
!=
vj
,从
vi
到
vj
和从
vj
到
vi
都存在路径,则称G为强连通图。有向图中的极大强连通子图称为有向图的强连通分量
1.5 连通图的生成树
1.5.1 无向图
连通图的生成树是一个极小的连通子图,它含有图中全部的n个顶点,但只有足以构成一棵树的n-1条边
图中的每个点都连通且只有n-1条边,即一颗树的形态
1.5.2 有向图
如果一个图恰有一个顶点入度为0,其余顶点的入度都为1,则是一颗有向树
一个有向图的生成森林由若干颗有向树组成,含有图中全部顶点,但只有足以构成若干颗不相交的有向树的弧。即一个有向图可以分解为一个森林
2、图的存储结构
2.1 邻接矩阵
2.1.1 无向图
使用两个数组来表示图,一个一维数组存储顶点,一个二维数组(邻接矩阵)存储图中的边或者弧
若图G有n个顶点,则邻接矩阵是一个n*n的方阵,定义为
例如
v0
,
v1
=1存在边,
v1
,
v3
=0不存在边
无向图的边数组是一个对称矩阵
特点:
某点的度为这个顶点所在行的元素之和,如
v0
有三个边,矩阵中的和为3
某点的邻接点就是当前行为1的点
2.1.2 有向图
有向图的入度为当前元素所在列的和
有向图的出度为当前元素所在行的和
2.1.3 带权的图
W表示权值
0表示自身
若两个顶点不存在边,使用无限大表示
2.2 邻接表
邻接矩阵是一个不错的图的存储结构,但是对于顶点较少的图,浪费空间。
所以使用数组存储每个元素,元素的邻接点使用单链表的形式。
求某个顶点的度:数组所在下标的链表个数
求某个点的邻接点:同上
2.2.1 有向图的邻接表
有向图是有方向的,所以邻接表也是有方向的。
所以邻接表只能表示顶点的出度,如果想要获取入度,必须遍历整个图。也可以创建逆邻接表。
2.3 十字链表
十字链表是为了解决邻接表有向图的存储问题。十字链表就是将有向图的邻接表和逆邻接表结合在一起
链表中的数据结构:tailvex为弧起点在顶点表中的下标,headvex为弧终点在顶点表中的下标。即用tailvex和headvex表示当前顶点的弧,且为出度。headlink为入边表指针域,指向弧终点相同的下一条边。taillink为出边表指针域,指向弧起点相同的指针域。即实线部分代表出度,虚线代表入度。
2.4 邻接多重表
用邻接表表示无向图时,若要删除一条边(0,2),需要到两个链表删除,比较繁琐。
邻接多重表就是仿照十字链表的方式
链表的数据结构中ivex和jvex是某条边依附的两个顶点在顶点表中的下标。ilink是指向依附ivex顶点的下一条边。jlink是指向依附jvex顶点的下一条边。
若要删除边v0,v2 只需要将6号的jlink和9号的jlink置null
2.5 边集数组
边集数组由两个一维数组组成。一个存放顶点,另一个存放边信息。边信息起点下标(begin)、终点下标(end)、权组成
3、图的遍历
从图中某一顶点出发,访遍图中其余顶点,且使每个顶点仅被访问一次,之一过程称为图的遍历。
图遍历的次序分为深度优先遍历、广度优先遍历
3.1 深度优先遍历(DFS)
从图中某个顶点v出发,访问此顶点,然后从v的未访问的邻接点出发遍历图,知道图中所有和v有路径相同的顶点被访问到。类似二叉树的先序遍历
3.2 广度优先遍历(BFS)
按层进行遍历,类似二叉树的层遍历
public class GraphTest {
//数据数组
Vertex vertex[] = new Vertex[100];
int verNum=0; //顶点数
int edgeNum=0; //边数
//是否访问标记
boolean visited[] = new boolean[100];
//测试数据
private String item[] = {"v0", "v1", "v2", "v3"};
private String edge[][] = {{"v0","v1"}, {"v1","v2"}, {"v2","v3"}, {"v3","v0"}, {"v0","v2"}};
//数据类
class Vertex {
String data;
Edge first;
}
//边节点类
class Edge{
int index;//存放数据下标
Edge next;
}
//创建图
public void createGraph(){
verNum = item.length;
//构建顶点
for (int i = 0; i < item.length; i++) {
vertex[i] = new Vertex();
vertex[i].data = item[i];
vertex[i].first = null;
}
edgeNum = edge.length;
//构建边
for (int i = 0; i < edge.length; i++) {
//构建下标为edge[i][0]的数据
Edge node0 = new Edge();
int node0Index = getIndex(edge[i][1]);
node0.index = getIndex(edge[i][0]);
//链表头插法
node0.next = vertex[node0Index].first;
vertex[node0Index].first = node0;
//构建下标为edge[i][1]的数据
Edge node1 = new Edge();
int node1Index = getIndex(edge[i][0]);
node1.index = getIndex(edge[i][1]);
node1.next = vertex[node1Index].first;
vertex[node1Index].first = node1;
}
}
//邻接表深度优先遍历 递归
public void DFSTraverse(){
for (int i = 0; i < verNum; i++)
visited[i] = false;
for (int i = 0; i < verNum; i++)
if(!visited[i]) //如果没被访问过 就进行遍历 如果是连通图,递归一次就够了
DFS(i);
}
//深度优先遍历的递归方法
private void DFS(int i) {
visited[i] = true;
System.out.print(vertex[i].data + " ");
Edge edge = vertex[i].first;
while(edge != null){
if(!visited[edge.index]) //如果没被访问过 就进行递归
DFS(edge.index);
edge = edge.next;
}
}
//邻接表的广度优先遍历
public void BFSTraverse() {
for (int i = 0; i < verNum; i++)
visited[i] = false;
Queue<Vertex> queue = new LinkedList<Vertex>();
Edge edge = null;
for (int i = 0; i < verNum; i++) { //遍历所有点,避免漏掉未连通的顶点
if (!visited[i]) {
visited[i] = true;
System.out.print(vertex[i].data + " ");
queue.offer(vertex[i]);
while (!queue.isEmpty()) {
edge = queue.poll().first; //获取当前顶点的第一个边信息
while(edge != null) { //遍历链表的所有连通边的顶点,并将顶点放入队列
if(!visited[edge.index]) {
visited[edge.index] = true;
System.out.print(vertex[edge.index].data + " ");
queue.offer(vertex[edge.index]);
}
edge = edge.next;
}
}
}
}
}
//根据顶点获取下标
private int getIndex(String data){
for (int i = 0; i < item.length; i++)
if (item[i].equals(data))
return i;
return -1;
}
}
调用:
public static void main(String[] args) {
GraphTest graph = new GraphTest();
graph.createGraph();//创建
graph.DFSTraverse();//深度优先巴黎
System.out.println();
graph.BFSTraverse();//广度优先遍历
System.out.println();
}
参考:大话数据结构