线性 -> 树 -> 图
为了解决问题,从线性结构 -> 树形结构 -> 图形结构
- 线性结构:只有前驱、后继结点,元素是线性关系
- 树形结构:元素有层次关系,每一层上的结点只能和上一层中的至多一个结点相关,但可能和下一层的多个结点相关
- 图形结构:元素之间具有任意关系,任意两个元素都可能相关
图
图由有限个顶点集合与顶点之间的边集合组成,用G(V,E)表示, 其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
有一些专业术语:
- 无向边:顶点间没有方向的边,上图中的所有边都是无向边
- 有向边:顶点间有方向的边
- 子图:图G’(V’,E’)中V’是V的子集,E’是E的子集,G’是G的子图
- 权:顶点或边上相关的数
- 邻接点:对于无向图G= (V,E), 如果边(v,v’)属于E, 顶点v和v‘互为邻接点
上图【2,4】、【4,6】等互为邻接点
- 度、入度和出度:点v的度是和v相关联的边的数目;有向图中,指向该结点的边数目是入度,由该结点出发的边数是出度
结点2的入度为1,出度为1,度为2
- 路径和路径的长度:从顶点v 到顶点v’的路径是一个顶点序列,路径长度是经过的边数(有向图的路径也是有向的)
上图顶点4到5的路径:4->2->5,长度为2
- 回路/环:起点等于终点的路径称为回路或环
- 简单路径、简单回路或简单环:序列中顶点不重复出现的路径是简单路径
简单回路即除起点/终点,其他顶点不重复的回路
图的存储结构
如同树的顺序存储、链式存储,图也有两种存储方式
- 邻接矩阵:根据顶点数组生成矩阵
邻接矩阵优点:便于计算度和判断边
缺点:需要确定顶点数,不利于增删顶点;空间复杂度大,浪费了很多空间
- 邻接表:数组+链表的形式,将每个顶点的邻接结点存入链表
邻接表优点:便于增删结点;便于统计边数;空间复杂度低
缺点:不便于判断结点是否连通、计算度
本文使用邻接矩阵
遍历方式
树有四种遍历方式:层次遍历、先序遍历、中序遍历、后序遍历
图有两种遍历方式:深度遍历、广度遍历
深度遍历
类似与树的先序遍历,对于一个图,遍历的顺序是:
- 从起点开始,先访问第一个邻接结点,然后以该结点为起点,重复该步,直到起点没有未被访问的邻接点为止
- 回到上一结点,继续访问下一邻接结点
- 重复上一步,直到访问完毕
以4为起点,深度遍历:4->2->1->3->5->6,访问步骤
- 访问起点4
- 访问第一个邻接点2,以2为起点,访问第一个邻接点1
- 以1为起点,没有未被访问的邻接点,回到上一结点2,访问第二个连接点3
- 以3为起点,第一个邻接点为5;以5为起点,第一个邻接点为6;以6为起点,没有为被访问的邻接点,回到结点5,没有为被访问的邻接点,回到结点3,没有为被访问的邻接点,回到结点2,没有为被访问的邻接点,回到结点4,没有为被访问的邻接点
- 既然回到起点也没有为被访问的邻接点,遍历结束
通过递归可以实现
广度遍历
广度遍历有些类似与树的层次遍历:总是先遍历完一个顶点的所有邻接点再遍历下一个顶点
如:
遍历方法:
- 先访问起点4,得到4的所有邻接结点并存入队列
- 按顺序访问,每访问一个结点就往队列中存放它的所有邻接结点
- 访问完队列即广度遍历
广度优先遍历:4->2->6->1->3->5
广度优先遍历需要借助队列实现
Java实现
package com.company.graph;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
/**
* @author zfk
*
* 数据结构:图 -> 深度优先遍历、广度优先遍历
*/
public class Graph {
//存储顶点集合
private ArrayList<String> vertexList;
//存储图对应的邻结矩阵
private int[][] edges;
//表示边的数目
private int num;
//定义一个数组,记录结点是否被访问
private boolean[] isVisited;
public static void main(String[] args) {
//创建图
int n = 6;
String[] vertexValue = {"4","2","1","3","5","6"};
Graph graph = new Graph(n);
//添加顶点
for (String value : vertexValue){
graph.insertVertex(value);
}
//添加边 A-B,A-C,B-C,B-D,B-E
graph.insertEdge(0,1,1);
graph.insertEdge(0,5,1);
graph.insertEdge(1,2,1);
graph.insertEdge(1,3,1);
graph.insertEdge(3,4,1);
graph.insertEdge(3,5,1);
graph.insertEdge(4,5,1);
graph.showGraph();
System.out.println("=== 深度遍历 ===");
graph.dfs();
System.out.println();
System.out.println("=== 广度优先 ===");
graph.bfs();
}
/**
* @param n 图的顶点数
*/
public Graph(int n){
edges = new int[n][n];
vertexList = new ArrayList<>(n);
num = 0;
isVisited = new boolean[n];
}
//插入结点
public void insertVertex(String vertex){
vertexList.add(vertex);
}
/**
* @param v1 顶点1的下标
* @param v2 顶点2的下标
* @param weight 边的权值
* 添加边
*/
public void insertEdge(int v1,int v2,int weight){
edges[v1][v2] = weight;
edges[v2][v1] = weight;
num++;
}
//返回结点个数
public int getNumOfVertex(){
return vertexList.size();
}
//返回边的数目
public int getNum(){
return num;
}
//返回结点i(下标)对应的数据 0->A 1->B
public String getValueByIndex(int i){
return vertexList.get(i);
}
//返回v1和v2边的权值
public int getWeight(int v1,int v2){
return edges[v1][v2];
}
//显示图对应的矩阵
public void showGraph(){
for (int[] i : edges){
System.out.println(Arrays.toString(i));
}
}
/**
* @param index 查找的结点
* @return 如果存在返回对应的下标
* 得到当前查找结点的第一个邻结结点的下标,否则返回-1
*/
private int getFirstNeighbor(int index){
for (int j = 0;j < vertexList.size();j++){
//下一个邻结点存在
if (edges[index][j] > 0){
return j;
}
}
return -1;
}
//根据前一个邻结结点的下标获取下一个邻结结点
private int getNextNeighbor(int v1,int v2){
//循环二维数组中该结点的数组:B[1,0,1,1,1]
for (int j = v2 + 1;j < vertexList.size();j++){
if (edges[v1][j] > 0){
return j;
}
}
return -1;
}
/**
* @param isVisited 判断该结点是否被访问
* @param i 访问结点的下标,第一次是0
* 深度优先遍历算法
*/
private void dfs(boolean[] isVisited,int i){
//访问结点输出
System.out.print(getValueByIndex(i) + " - > ");
//将该结点设置为已访问
isVisited[i] = true;
//找到结点i的邻接结点
int w = getFirstNeighbor(i);
//存在邻接结点
while (w != -1){
//w没有被访问
if (isVisited[w] == false){
dfs(isVisited,w);
}
//如果w被访问,查找当前结点的下一个结点
w = getNextNeighbor(i,w);
}
}
//重载dfs,遍历所有节点
public void dfs(){
for (int i = 0;i < getNumOfVertex();i++){
if (!isVisited[i]){
dfs(isVisited,i);
}
}
}
/**
* @param isVisited 结点是否被访问的数组
* @param i 当前结点对应的下标
* 广度优先
*/
private void bfs(boolean[] isVisited,int i){
//表示队列的头结点对应的下标
int u;
//邻接结点w
int w;
//队列,记录结点访问的顺序
LinkedList linkedList = new LinkedList();
//访问结点,输出结点
System.out.print(getValueByIndex(i)+" - > ");
//标记当前结点为已访问
isVisited[i] = true;
//将结点加入队列
linkedList.addLast(i);
//当队列不为空,遍历队列
while (!linkedList.isEmpty()){
//取出队列头结点
u = (int) linkedList.removeFirst();
//得到该结点的第一个邻接结点的下标
w = getFirstNeighbor(u);
//当找到邻接结点
while (w != -1){
//判断邻接结点是否访问过
if (!isVisited[w]){
//输出邻接结点
System.out.print(getValueByIndex(w)+" - > ");
//标记为已访问
isVisited[w] = true;
//加入队列
linkedList.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);
}
}
}
}
遍历结果: