文章目录
前言
千呼万唤始出来,终于开始学图喽 , 虽然学校的课程早结课了(水完了) , 话不多说,开始整活,本文用地是无向图
提示:以下是本篇文章正文内容,下面案例可供参考
一、图的基本概念
下面讲几个常用的概念,
1.图的定义
图(graph) ,是由两个集合组成的 ,其一为顶点集, 其二为边集 ,其中顶点集不允许为空 ,边集可为空, 也就是说,只有一个顶点没有边的图也可以叫做图
2.基本术语
1. 度,出/入度: 顶点的度是指与该顶点相关联的边的个数,对于有向图,度还细分
为入度和出度, 其度=入度+出度, 一般地, 度数 / 2 = 边数
2. 路径和路径长度: 路径指的是从顶点到顶点的弧或直线, 而路径长度是指从
某一顶点出发到另一顶点所经过的路径的条数
3.连通: 有路径即为连通,如果图中任意两个顶点间都有路径连接,就是连通图,
而连通分量指的是图中包含的最大连通子图
4.生成树: 树中包含了图的所有顶点,和n-1条边(也就是说生成树就是缺了一条边
,使其无法连通的图)
二、图的基本算法
讲图的算法前, 得先讲讲构建一张图需要哪些属性
我们都知道图是由边和顶点构成的,因此在构造数据结构的时候,需要一个二维数组存放边的两个顶点 (邻接矩阵) , 需要一个一维数组存放顶点, 一个布尔数组用于标记各个顶点是否已被遍历, 一个整型变量表示边的数量
1.初始化图
最方便的方式就是通过构造函数来初始化一个图, 在初始化时, 顶点个数是必须要作为参数传入的,因为一张图可以没有边,但是必须要有顶点
代码如下(示例):
//传入顶点个数numOdVertex
public Graph(Integer numOdVertex) {
this.vertexs = new ArrayList<>();
this.edges = new int[numOdVertex][numOdVertex]; //矩阵的大小等于顶点个数的平方
this.numOdEdges = 0; //边数默认为0
this.isVisited = new boolean[numOdVertex];
}
2.插入顶点和边
在对图完成初始化之后, 还要对图的点边关系进行构造 , 也就是插入顶点和边的操作.
在添加顶点的时候, 我们只需要简单地向顶点数组中加入元素即可, 因为这个顶点可以没有连接边
在添加边的时候, 我们需要按顶点的索引对边进行赋权值操作 , 由于是无向图, 两端顶点索引对应的值要双面赋值(这点详见下面代码)
代码如下(示例):
//添加边 ,三个要素,边的两个顶点(邻接矩阵的两个元素中的索引,无向图添加是双向的)
public void insertEdge(Integer vertex01,Integer vertex02,int weight) {
this.edges[vertex01][vertex02] = weight; //双面赋值
this.edges[vertex02][vertex01] = weight;
//边数++
this.numOdEdges++;
}
3.矩阵打印
这里有一个小技巧 ,也是第一次碰到的, 对于一个二维数组 , 我们可以将其看作是一个一维数组, 那么其中的元素其实就是一个个的一维数组, 对于一维数组, 我们完全可以通过Arrays
工具类直接输出它 ,这里能上一层for
循环, 代码优雅很多
//打印图(邻接矩阵)
public void showGraph(){
for (int[] row:
this.edges) {
System.out.println(Arrays.toString(row));
}
}
4.返回第一个邻接结点的下标
可以参见下文广度优先算法的那张矩阵图 , 我们将矩阵按行分为了5组 , 那么每组代表一个结点, 纵向的每一列可以视为该结点的邻接点 , 那么就拿下图来说, A点只与E点有连接, 因此值为1 ,其余为0
因此在遍历的过程中 , 结点个数将作为循环的上限
//返回第一个临接结点的下标
public Integer getFirstAdjust(int index){ //传入当前当前结点的下标
for(int i = 0 ; i < this.vertexs.size() ; i++){
if(this.edges[index][i] > 0){ //找到了第一个临接结点
return i;
}
}
return -1;
}
5.返回第一个邻接结点的下一个结点的下标
既然要求出下一个结点的下标 , 那么我们就需要两个参数 ,一个是当前结点的下标,一个是与当前结点邻接的结点的下标, 这个函数非常重要,在下面会着重介绍它的作用,这里先给出代码
//返回第一个临接结点的下一个结点的下标
public Integer getNextAdjust(int index01,int index02){
for(int i = index02+1 ; i < this.vertexs.size() ; i++){
if(edges[index01][i] > 0){
return i;
}
}
return -1;
}
本文的重头戏来了,最麻烦的DFS & BFS
三、深度优先算法
四、广度优先算法
广度遍历类似于层级遍历 , 需要将当前结点的全部邻接结点都遍历完, 才能开始下一层遍历
我们在搭建邻接矩阵的时候, 将两个连通的顶点间的权值设为了1
,不连通的顶点权值设为0
,这样在进行广度优先遍历的时候,当遇到矩阵中值为1的位置时, 说明它和所在行所代表的顶点是连通的
在遍历过程中还需一个LinkedList
作为辅助, 其原因也是在于这是广度遍历, 我们需要存下结点的邻接结点的下标, 在找到邻接结点的下标以后, 就要在邻接矩阵中,通过该下标找到该邻接结点的所在行,再对该行进行遍历,找出新的,还没被遍历过的结点 ,以此类推
就上图而言, 我们的第一个结点是A
,于是从第一行开始遍历, B,C,D
都与A
无连接 , 只有E
有, 通过getFirstAdjust
方法获取第一个临接结点的下标, 跳转到最后一行E
行 , 从第一列开始遍历, 找到了C
,再以此类推 ,与A点相邻的所有结点都被遍历完了 , 再开始下一个结点的广度优先遍历
,最后得出的广度优先遍历顺序为AECBD
,与深度遍历同
public void bfs(){
for(int i = 0 ; i < this.getNumOfVertex() ; i++){
if(!isVisited[i]){
bfs(i,isVisited);
}
}
}
private void bfs(int index,boolean[]isVisited) {
int head; //队列头结点对应的下标
int next; //临接结点对应的下标
LinkedList queue = new LinkedList();
System.out.println(this.getValueByIndex(index) + "=>");
isVisited[index] = true;
queue.addLast(index);
while(!queue.isEmpty()){ //队列不为空
head = (Integer)queue.removeFirst();
next = this.getFirstAdjust(head);
while(next != -1){
if(!isVisited[next]){ //临接结点存在且没被访问过
//输出并标记为已访问
System.out.println(this.getValueByIndex(next));
isVisited[next] = true;
//入队
queue.addLast(next);
}
next = this.getNextAdjust(head,next);
//this.dfs(head,isVisited);
}
}
}
该处使用的url网络请求的数据。
总结
这两个算法确实非常的难理解,
今天先写这么多.