概述
常用概念
- 顶点:图中的每一个结点称为一个顶点
- 边:图中两个相邻顶点的路径称为边
- 路径:两个顶点直接的边的集合
- 无向图:顶点之间的路径没有方向
- 有向图:顶点之间的路径有方向
- 带权图:每个边带有权值
表示方式
图的表示方式有两种:二维数组(邻接矩阵);链表(邻接表)
邻接矩阵
表示图中顶点之间相邻关系的矩阵,二维数组的行下标表示边的起点,二维数组的列下标表示边的终点,行下标和列下标都表示顶点,即矩阵的行列数表示顶点个数,arr[row][col]
的值表示是否两个之间存在边
邻接表
- 邻接矩阵需要为每个顶点分配n个边的空间,但是有或多边是不存在的,这样就造成了空间的损失
- 邻接表的实现只关心存在的边,不关心不存在的边,因此没有空间浪费,邻接表由数组+链表组成
- 邻接表中数组长度表示顶点个数,数组的下标表示当前顶点,数组中的元素为一个链表,链表中的每个结点表示能够与当前顶点相连的顶点
图的创建
/***
* 邻接矩阵实现图
*
* @author laowa
*
*/
class MatrixGraph {
/**
* 顶点集合
*/
ArrayList<String> vertexList;
/**
* 存储图对应的邻接矩阵
*/
int[][] edges;
/**
* 边数
*/
int numOfEdges;
/**
* 传入顶点数构造图
*
* @param vertexNum
* 顶点个数
*/
public MatrixGraph(int vertexNum) {
this.vertexList = new ArrayList<>(vertexNum);
this.edges = new int[vertexNum][vertexNum];
this.numOfEdges = 0;
}
/**
* 添加顶点
*
* @param vertex
*/
public void addVertex(String vertex) {
this.vertexList.add(vertex);
}
/**
* 添加边
*
* @param v1
* 顶点1的下标
* @param v2
* 顶点2的下标
* @param weight
* 边的权重
*/
public void addEdge(int v1, int v2, int weight) {
this.edges[v1][v2] = weight;
this.edges[v2][v1] = weight;
this.numOfEdges++;
}
/**
* 获取顶点个数
*
* @return 顶点个数
*/
public int getNumOfVertex() {
return this.vertexList.size();
}
/**
* 获取边数
*
* @return 边数
*/
public int getNumOfEdges() {
return this.numOfEdges;
}
/**
* 通过顶点下标获取顶点
*
* @param index
* 下标
* @return 顶点信息
*/
public String valueOf(int index) {
return vertexList.get(index);
}
/**
* 通过顶点下标获取边的权重
*
* @param v1
* 第一个顶点的下标
* @param v2
* 第二个顶点的下标
* @return 权重
*/
public int getWeight(int v1, int v2) {
return edges[v1][v2];
}
/**
* 打印邻接矩阵
*/
public void printMatrix() {
for(int[] i:edges) {
System.out.println(Arrays.toString(i));
}
}
}
图的遍历
图的遍历即对图中的所有结点的访问,一般由两种访问策略:深度优先遍历、广度优先遍历
深度优先遍历
- 从初始访问结点出发,初始访问结点可能有多个邻接结点,深度优先遍历的策略就是首先访问第一个邻接结点,然后这个被访问的邻接结点作为初始结点,访问他的第一个邻接结点,每次都在访问完当前结点之后首先访问当前节点的第一个邻接结点
- 这样的访问策略是优先纵向深入,而不是对一个结点的所有邻接结点进行横向访问
算法步骤
- 访问初始节点v,并标记结点为已访问
- 查找结点v的第一个邻接结点next
- 若next存在,则往下进行;如果next不存在,则回到第一步,将从v的下一个结点继续
- 若next未被访问,对w进行深度优先递归
- 若next已被访问,则找到next的下一个邻接结点,此时不能从头开始找,要从v后面找到next的第一个邻接结点
代码实现
/**
* 深度优先遍历
*/
public void dfs() {
//通过点集的大小创建一个标记访问的数组,数组中的元素和结点一一对应
boolean isVisited[] = new boolean[vertexList.size()];
//遍历所有结点,对每个结点进行深度优先遍历
for (int i = 0; i < this.vertexList.size(); i++) {
//如果之前的深度优先遍历没有遍历过当前结点,则对当前结点进行深度优先遍历
if (!isVisited[i]) {
this.dfs(isVisited, i);
}
}
}
/**
* 深度优先搜索
* @param isVisited 存储是否已经遍历过的标记
* @param i 当前遍历到的结点,再邻接矩阵中的每一个下标对应了一个结点,通过下标可以直接表示结点
*/
private void dfs(boolean[] isVisited, int i) {
//输出当前节点
System.out.print(this.valueOf(i)+" -> ");
//标记当前结点已经遍历
isVisited[i] = true;
//找到当前结点的下一个邻接结点
int next = this.getFirstNeighbor(i);
//开始循环直到找不到下一个邻接结点
while (next != -1) {
//如果下一个邻接结点没有遍历过,对他进行遍历
if (!isVisited[next]) {
dfs(isVisited, next);
} else {
//如果下一个邻接结点已经遍历过了,直接找到下一个邻接结点的下一个邻接结点
next = this.getNextNeighbor(i, next);
}
}
}
/**
* 根据前一个邻接结点获取下一个邻接结点的下标
*
* @param v1
* 当前遍历的节点
* @param v2
* 已经找到的前一个邻接结点(该结点已经遍历过了)
* @return 存在则返回邻接结点的下标,否则返回-1
*/
private int getNextNeighbor(int v1, int v2) {
//从前一个邻接结点v2后面开始,向后面找与v1邻接的结点,调用该方法是因为v2已经遍历过了,所以要找到v2以外的邻接结点
for (int i = v2 + 1; i < vertexList.size(); i++) {
//如果存在该边,表示存在邻接关系
if (this.edges[v1][i] > 0) {
return i;
}
}
return -1;
}
/**
* 获取邻接结点的下标
*
* @param index
* 传入当前结点的下标
* @return 存在则返回邻接结点的下标,否则返回-1
*/
private int getFirstNeighbor(int index) {
// 遍历邻接矩阵
for (int i = 0; i < vertexList.size(); i++) {
// 如果该边权重>0表示两者之间存在邻接关系
if (edges[index][i] > 0) {
return i;
}
}
return -1;
}
广度优先遍历
广度优先搜索类似于一个分层搜索的过程,广度优先遍历需要使用一个队列保持访问过都节点的顺序,一边按照这个顺序来访问这些节点的邻接结点
算法步骤
- 访问初始节点v并标记结点v已访问
- 结点v入队
- 当队列非空时,继续执行;否则算法结束
- 出队列,取得队列头节点u
- 查找结点u的第一个邻接结点w
- 若结点u的邻接结点w不存在,则转到步骤3对队列中的下一个元素出队;否则循环执行以下步骤
- 若结点w尚未访问,标记为已访问
- 结点w入队
- 查找结点u的继w结点后的下一个邻接结点,转到步骤6
代码实现
/**
* 广度优先遍历
*/
public void bfs() {
// 通过点集的大小创建一个标记访问的数组,数组中的元素和结点一一对应
boolean isVisited[] = new boolean[vertexList.size()];
// 遍历所有结点,对每个结点进行广度优先遍历
for (int i = 0; i < this.vertexList.size(); i++) {
// 如果之前的广度优先遍历没有遍历过当前结点,则对当前结点进行深度优先遍历
if (!isVisited[i]) {
this.dfs(isVisited, i);
}
}
}
/**
* 广度优先遍历
* @param isVisited 标记顶点是否被访问的数组
* @param i 当前进行广度优先遍历的顶点
*/
public void bfs(boolean[] isVisited, int i) {
int u;// 表示队列头的顶点
int w;// 表示下一个邻接结点
// 使用一个链表表示队列,记录结点访问的顺序
LinkedList<Integer> queue = new LinkedList<>();
//输入当前结点
System.out.println(this.valueOf(i) + " -> ");
//记录当前结点已经被访问
isVisited[i] = true;
//将当前结点入队
queue.addLast(i);
//开始遍历队列中的元素
while (!queue.isEmpty()) {
//获取队头的顶点
u = queue.removeFirst();
//获取队头顶点的下一个邻接结点
w = this.getFirstNeighbor(u);
//循环找当前结点的下一个邻接结点,找完所有的邻接结点
while (w != -1) {
//找到邻接结点,如果没有遍历则进行遍历
//然后将这个结点入队,以记录遍历的顺序,这一层邻接结点找完之后,需要找下一层的邻接结点,下一层就根据队列中的顺序找
if (!isVisited[w]) {
System.out.println(this.valueOf(w) + " -> ");
isVisited[w] = true;
queue.addLast(w);
} else {
//如果下一个邻接结点已经遍历过了,那么再找到后面一个邻接结点,直到当前结点的所有邻接结点都被遍历完
w = this.getNextNeighbor(u, w);
}
}
}
}