这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战
本系列文章为个人学习总结,如果有发现错误或存在疑问之处,欢迎留言指点!
本文是重学数据结构系列的第六篇,系列文章如下:
1.算法时间复杂度和空间复杂度
2.重学数据结构--链表
3.重学数据结构--队列
4.重学数据结构--栈
5.重学数据结构--树
1.为什么会有图?
我们知道,数据之间的关系有3种,分别是:一对一、一对多和多对多,前两种关系的数据可分别用线性表和树结构存储,而多对多的关系就要用图表示。
2.图的相关概念
-
顶点
-
边:边有些地方又称为弧
- 在有向图中,无箭头一端的顶点通常被称为初始点或弧尾,箭头直线的顶点被称为终端点或弧头。
-
无向图
-
路径:上图中A->C的路径:
-
A->C
-
A->B->C
如果路径中第一个顶点和最后一个顶点相同,则此路径称为"回路"(或"环")。
-
-
有向图
-
带权图:带权的图又称网
-
入度与出度:上面有向图中A的入度为1,出度为2
图的表示方法有两种:一种用二维数组表示(邻接矩阵);另一种是用链表(邻接表)
3.用二维数组表示
首先我们拟定给各个顶点一个序号(从0开始),edges[v1][v2]=weight, 数组edges用于存储两个顶点之间的关系,其中 中v1和v2分别表示某两个顶点的序号,我们约定weight=0表示两个顶点之间没有接,weight为其他值表示两个顶点之间的权值(这里没有权值默认为1)。
具体实现
public class Graph {
// 顶点集合
private ArrayList<String> vertexList;
//存储图对应的邻接矩阵
private int[][] edges;
//表时边的数量
private int numOfEdges;
/**
* 构造器
* @param n 节点数
*/
public Graph(int n){
edges = new int[n][n];
vertexList = new ArrayList<>(n);
numOfEdges = 0;
}
/**
* 获取节点个数
* @return
*/
public int getNumOfVertex(){
return vertexList.size();
}
/**
* 显示图对应的矩阵
*/
public void showGraph(){
for (int[] link : edges){
System.out.println(Arrays.toString(link));
}
}
/**
* 获取边的数量
* @return
*/
public int getNumOfEdges(){
return numOfEdges;
}
/**
* 返回节点下标对应的数据
* @return
*/
public String getValueByIndex(int index){
return vertexList.get(index);
}
/**
* 返回对应权值
* @param v1
* @param v2
* @return
*/
public int getWeight(int v1,int v2){
return edges[v1][v2];
}
/**
* 插入节点
* @param vertex
*/
public void insertVertex(String vertex){
vertexList.add(vertex);
}
/**
* 插入边
* @param v1
* @param v2
* @param weight 权值
*/
public void insertEdge(int v1,int v2,int weight){
edges[v1][v2] = weight;
edges[v2][v1] = weight;
numOfEdges++;
}
}
测试
public static void main(String[] args) {
//测试一把图是否创建ok
int n = 5; //结点的个数
String Vertexs[] = {"A", "B", "C", "D", "E"};
//创建图对象
Graph graph = new Graph(n);
//循环的添加顶点
for(String vertex: Vertexs) {
graph.insertVertex(vertex);
}
//添加边
//A-B A-C B-C B-D B-E
graph.insertEdge(0, 1, 1);
graph.insertEdge(0, 2, 1);
graph.insertEdge(1, 2, 1);
graph.insertEdge(1, 3, 1);
graph.insertEdge(1, 4, 1);
graph.showGraph();
}
4.图的两种遍历方式
1.深度优先搜索
深度优先搜索又称深搜或DFS。
访问思路:
-
深度优先搜索,从初始访问结点出发,初始访问结点可能有多个邻接结点,深度优先遍历的策略就是首先访问
第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问它的第一个邻接结点, 可以这样理解:
每次都在访问完当前结点后首先访问当前结点的第一个邻接结点。
-
这样的访问策略是优先往纵向挖掘深入,而不是对一个结点的所有邻接结点进行横向访问。
-
显然,深度优先搜索是一个递归的过程
数组存储:String Vertexs[] = {"v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8"};
深度优先搜索的过程类似于树的先序遍历,我们以上图为例对图的进行深度优先遍历,遍历过程为:
- 首先任意找一个未被遍历过的顶点,这里以节点v1为初始节点进行访问,并标记v1为已访问
- 遍历V1的邻接节点,v1的第一个邻接节点为v2,标记v2已经访问,遍历v2的邻接节点,v2的第一个邻接节点是v3,但是v2跟v3没有联系,继续下一个邻接节点是v4(做标记),然后是v8(做标记),然后v5
- 当继续遍历v5的邻接节点时,根据之前做的标记,所有有联系的邻接节点都被访问过了。此时,从v5回退到v8,看v8是否有未被访问过的邻接节点,如果没有继续回退到v4,v2,v1;
- 通过遍历v1,找到一个未被访问过的顶点v3,然后访问v3的邻接节点v6,然后v7
- 由于v7没有未被访问的邻接节点,所以回退到v6,v3,v1,都没有未被访问过的
- 最后一步需要判断是否所有顶点都被访问,如果还有没被访问的,以未被访问的顶点为第一个顶点,继续依照上边的方式进行遍历。
根据上边的过程,可以得到上图通过深度优先搜索获得的顶点的遍历次序为:
V1 -> V2 -> V4 -> V8 -> V5 -> V3 -> V6 -> V7
具体实现
以上面图的二维数组表示为基准
//记录是否被访问
private boolean[] isVisited;
/**
* 得到第一个邻接节点的下标
* @param index
* @return
*/
public int getFirstNeighbor(int index){
for (int j = 0; j < vertexList.size(); j++) {
if(edges[index][j]>0){
return j;
}
}
return -1;
}
/**
* 根据前一个邻接节点的下标来获取下一个邻接节点
* @param v1 当前遍历节点序号
* @param v2 v1节点已被访问过的节点序号
* @return
*/
public int getNextNeighbor(int v1,int v2){
for (int j = v2+1; j < vertexList.size(); j++) {
if(edges[v1][j]>0){
return j;
}
}
return -1;
}
/**
* 深度优先遍历方法
* @param isVisited
* @param i
*/
private void dfs(boolean [] isVisited,int i){
System.out.print(getValueByIndex(i)+"->");
isVisited[i] = true;
// 查找节点i的第一个邻接节点
int w = getFirstNeighbor(i);
while (w != - 1){
if(!isVisited[w]){
dfs(isVisited,w);
}
// 如果w节点已经被访问过
w = getNextNeighbor(i,w);
}
}
public void dfs(){
isVisited = new boolean[vertexList.size()];
for (int i = 0; i < getNumOfVertex(); i++) {
if(!isVisited[i]){
dfs(isVisited,i);
}
}
}
测试
public static void main(String[] args) {
//测试一把图是否创建ok
int n = 8; //结点的个数
String Vertexs2[] = {"v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8"};
//创建图对象
Graph graph2 = new Graph(n);
//循环的添加顶点
for(String vertex: Vertexs2) {
graph2.insertVertex(vertex);
}
graph2.insertEdge(0, 1, 1);
graph2.insertEdge(1, 3, 1);
graph2.insertEdge(1, 4, 1);
graph2.insertEdge(3, 7, 1);
graph2.insertEdge(4, 7, 1);
graph2.insertEdge(0, 2, 1);
graph2.insertEdge(2, 5, 1);
graph2.insertEdge(2, 6, 1);
graph2.dfs();
}
2.广度优先搜索
广度优先搜索又称广搜或bfs。类似于一个分层搜索(树的层次遍历)的过程,广度优先搜索需要使用一个队列以保持访问过的结点的顺序,以便按这个顺序来访问这些结点的邻接结点。
访问思路:
- 从初始访问节点出发,让其入队,标记为已经访问
- 取队头元素,查找其未访问的邻接节点,标记为已经访问,并依次入队
- 如果列队不为空,继续取队头元素,查找其未访问的邻接节点标记为已经访问并依次入队
- 直到队列为空
以上图为例,遍历过程为:
- 取v1入队,标记为已经访问,出队,v1的邻接节点v2,v3入队,并标记为已经访问
- v2出队,v4,v5入队并标记;v3出队,v6,v7入队并标记
- v4出队,v8入队并标记;v5出队,v6出队,v7出队,v8出队。
最后遍历结果为:
V1 -> V2 -> v3 -> V4 -> V5 -> V6 -> V7 -> V8
具体实现
这里仍然以上面图的二维数组表示为基准
/**
* 广度优先遍历方法
* @param isVisited
* @param i
*/
private void bfs(boolean [] isVisited,int i){
// 表示队列的头节点对应下标
int u;
// 邻接节点w
int w;
// 队列,记录节点的访问顺序
LinkedList<Object> queue = new LinkedList<>();
System.out.print(getValueByIndex(i)+"->");
isVisited[i] = true;
queue.addLast(i);
while (!queue.isEmpty()){
// 取出队列的头节点下标
u = (Integer) queue.removeFirst();
// 得到第一个邻接节点的下标w
w = getFirstNeighbor(u);
while (w !=-1){
if(!isVisited[w]){
System.out.print(getValueByIndex(w)+"->");
isVisited[w] = true;
queue.addLast(w);
}
// 以u为前驱节点,找到w后面的下一个邻接节点
w = getNextNeighbor(u,w);
}
}
}
public void bfs(){
isVisited = new boolean[vertexList.size()];
for (int i = 0; i < getNumOfVertex(); i++) {
if(!isVisited[i]){
bfs(isVisited,i);
}
}
}
测试:
public static void main(String[] args) {
//测试一把图是否创建ok
int n = 8; //结点的个数
String Vertexs2[] = {"v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8"};
//创建图对象
Graph graph2 = new Graph(n);
//循环的添加顶点
for(String vertex: Vertexs2) {
graph2.insertVertex(vertex);
}
graph2.insertEdge(0, 1, 1);
graph2.insertEdge(1, 3, 1);
graph2.insertEdge(1, 4, 1);
graph2.insertEdge(3, 7, 1);
graph2.insertEdge(4, 7, 1);
graph2.insertEdge(0, 2, 1);
graph2.insertEdge(2, 5, 1);
graph2.insertEdge(2, 6, 1);
graph2.bfs();
}
\