1.1图的概念

图(Graph),是一种复杂的非线性表结构,图的元素我们就叫做顶点(vertex),一个顶点可以与任意其他顶点建立连接关系,这种建立的关系叫做边(edge),顶点相连接的边的条数叫做度(degree) 。边有方向的图叫做有向图 ,边无方向的图叫无向图 。每条边都有一个权重(weight),可以通过这个权重来表示 一些可度量的值 ,这样的图叫做带权图(weighted graph) 。

java 图片后缀 大全 java中的图_java 图片后缀 大全

1.2 图的存储原理

1.2.1 邻接矩阵存储

图最直观的一种存储方法就是,邻接矩阵(Adjacency Matrix),邻接矩阵的底层是一个二维数组。

java 图片后缀 大全 java中的图_邻接矩阵_02

如果是无向图,如果顶点 i 与顶点 j 之间有边,我们就将A[i][j]和 A[j][i]标记为 1。

如果是有向图,如果顶点 i 到顶点 j 之间,有一条箭头从顶点 i 指向顶点 j 的边,那我们就将A[i][j]标记为 1。同理,如果有一条箭头从顶点 j 指向顶点 i 的边,我们就将 A[j][i]标记为 1 .

java 图片后缀 大全 java中的图_java 图片后缀 大全_03

如果是带权图,数组中就存储相应的权重。

java 图片后缀 大全 java中的图_邻接表_04

1.2.2 邻接矩阵的代码实现

public class Graph {private List vertexList;//存储点的链表 private int[][] edges;//邻接矩阵,用来存储边,值是权值 private int numOfEdges;//边的数目 public Graph(int n) { //初始化矩阵,一维数组,和边的数目 edges=new int[n][n]; vertexList=new ArrayList(n); numOfEdges=0; } //得到结点的个数 public int getNumOfVertex() { return vertexList.size(); } //得到边的数目 public int getNumOfEdges() { return numOfEdges; } //返回结点i的数据 public Object getValueByIndex(int i) { return vertexList.get(i); } //返回v1,v2的权值 public int getWeight(int v1,int v2) { return edges[v1][v2]; } //插入结点 public void insertVertex(Object vertex) { vertexList.add(vertex); } //插入边 public void insertEdge(int v1,int v2,int weight) { edges[v1][v2]=weight; numOfEdges++; }}

1.2.3 邻接表存储

用邻接矩阵来表示一个图,虽然简单、直观,但是比较浪费存储空间,对于无向图来说,如果 A[i][j]等于 1,那 A[j][i]也肯定等于 1。实际上,我们只需要存储一个就可以了。 如果我们存储的是稀疏图(Sparse Matrix),也就是说,顶点很多,但每个顶点的边并不多,那邻接矩阵的存储方法就更加浪费空间了。针对上面邻接矩阵比较浪费内存空间的问题,有另外一种图的存储方法,邻接表(Adjacency List)。

java 图片后缀 大全 java中的图_List_05

存储原理

java 图片后缀 大全 java中的图_邻接矩阵_06

每个顶点对应一条链表,链表中存储的是与这个顶点相连接的其他顶点。图中画的是一个有向图的邻接表存储方式,每个顶点对应的链表里面,存储的是指向的顶点。 前面的数组存储的是所有的顶点,每一个顶点后面连接的块代表前面顶点所指向的顶点和路线的权值。如果该点还指向其他顶点,则继续在块后面添加。例如A指向了B权值是4,那么A后面就加上一块,之后发现A还指向D权值是5,那么就在块尾继续添加一块。其实也就是数组+链表的结构.

1.2.4邻接表代码实现

根据邻接表的结构和图,图其实是由顶点和边组成的。抽象出两种类,一个是Vertex顶点类,一个是Edge边类

顶点

class Vertex{String name;//顶点名称 Edge next;//从该定点出发的边 public Vertex(String name, Edge edge) { this.name = name; this.next = edge; }}

边:

class Edge{String name;//指向下一个顶点的名字 int weight;//权重 Edge next;//被指向的下一个边 public Edge(String name, int weight, Edge next) { this.name = name; this.weight = weight; this.next = next; }}

添加和遍历的实现

public class Graph2 {Map vertexsMap; Graph2(){ this.vertexsMap = new HashMap<>(); } /** * 添加顶点 * @param name 顶点名称 */ public void insertVertex(String name) { Vertex vertex = new Vertex(name,null); vertexsMap.put(name,vertex); } /** * 插入变 * @param start 开始顶点名称 * @param end 结束顶点名称 * @param weight 权重 */ public void insertEdge(String start,String end,int weight){ //现获取开始顶点 Vertex startVertex = vertexsMap.get(start); //开始顶点为null,直接返回,也可以新建一个顶点// if (startVertex==null){// return;// } if (startVertex==null){ startVertex = new Vertex(start,null); vertexsMap.put(start,startVertex); } //新建边 Edge edge = new Edge(end, weight, null); if (startVertex.next==null){ startVertex.next=edge; }else{ //如果不为null 寻找节点的next==null的位置,挂上这个边 Edge lastEdge = startVertex.next; while(lastEdge.next!=null){ lastEdge=lastEdge.next; } lastEdge.next=edge; } } public void print(){ Set> set = vertexsMap.entrySet(); Iterator> iterator = set.iterator(); while (iterator.hasNext()){ Map.Entry entry = iterator.next(); Vertex vertex = entry.getValue(); Edge edge = vertex.next; while(edge!=null){ System.out.println(vertex.name+"指向:"+edge.name+"权重是:"+edge.weight); edge= edge.next; } } } public static void main(String[] args) { Graph2 graph = new Graph2(); graph.insertVertex("A"); graph.insertVertex("B"); graph.insertVertex("C"); graph.insertVertex("D"); graph.insertVertex("E"); graph.insertVertex("F"); graph.insertEdge("C", "A", 1); graph.insertEdge("F", "C", 2); graph.insertEdge("A", "B", 4); graph.insertEdge("E", "B", 2); graph.insertEdge("A", "D", 5); graph.insertEdge("D", "F", 4); graph.insertEdge("D", "E", 3); graph.print(); }}

测试验证:

按照此图来存储

java 图片后缀 大全 java中的图_java算法 数据结构_07

先存储A-F6个顶点,然后存7个边:

java 图片后缀 大全 java中的图_邻接表_08

1.3 图的遍历

遍历是指从某个节点出发,按照一定的的搜索路线,依次访问对数据结构中的全部节点,且每个节点仅访问一次.

从给定图中任意指定的顶点(称为初始点)出发,按照某种搜索方法沿着图的边访问图中的所有顶点,使每个顶点仅被访问一次,这个过程称为图的遍历。遍历过程中得到的顶点序列称为图遍历序列

图的遍历可以分为2中策略:深度优先搜索 (DFS,Depth First Search )、广度优先搜索(BFS,Breadth First Search )

1.3.1 深度优先搜索DFS

深度优先搜索,从起点出发,从规定的方向中选择其中一个不断地向前走,直到无法继续为止,然后尝试另外一种方向,直到最后走到终点。就像走迷宫一样,尽量往深处走.

DFS 解决的是连通性的问题,即,给定两个点,一个是起始点,一个是终点,判断是不是有一条路径能从起点连接到终点。起点和终点,也可以指的是某种起始状态和最终的状态。问题的要求并不在乎路径是长还是短,只在乎有还是没有。

遍历过程

以下图为例,需要依赖栈(Stack),特点是后进先出(LIFO)。

java 图片后缀 大全 java中的图_邻接矩阵_09

第一步选择一个起始顶点,顶点A,把A压栈标记它为访问过(用红色标记),并输出到结果中。

java 图片后缀 大全 java中的图_邻接表_10

第二步 寻找与 A 相连并且还没有被访问过的顶点,顶点 A 与 B、D、G 相连,而且它们都还没有被访问过,按照字母顺序处理,所以将 B 压入栈,标记它为访问过,并输出到结果中。

java 图片后缀 大全 java中的图_邻接矩阵_11

第三步,现在在顶点 B 上,重复上面的操作,由于 B 与 A、E、F 相连,如果按照字母顺序处理的话,A 应该是要被访问的,但是 A 已经被访问了,所以我们访问顶点 E,将 E 压入栈,标记它为访问过,并输出到结果中

java 图片后缀 大全 java中的图_邻接矩阵_12

第四步,从 E 开始,E 与 B、G 相连,但是B刚刚被访问过了,所以下一个被访问的将是G,把G压入栈,标记它为访问过,并输出到结果中

java 图片后缀 大全 java中的图_java 图片后缀 大全_13

第五步,现在在顶点 G 的位置,由于与 G 相连的顶点都被访问过了,类似于走到了一个死胡同,必须尝试其他的路口了。所以这里要做的就是简单地将 G 从栈里弹出,表示从 G 这里已经无法继续走下去了,看看能不能从前一个路口找到出路。

java 图片后缀 大全 java中的图_List_14

第六步,现在栈的顶部记录的是顶点 E,来看看与 E 相连的顶点中有没有还没被访问到的,发现它们都被访问了,所以把 E 也弹出去。

java 图片后缀 大全 java中的图_邻接表_15

第七步,当前栈的顶点是 B,看看它周围有没有还没被访问的顶点,有,是顶点 F,于是把 F 压入栈,标记它为访问过,并输出到结果中。

java 图片后缀 大全 java中的图_java 图片后缀 大全_16

第八步,当前顶点是 F,与 F 相连并且还未被访问到的点是 C 和 D,按照字母顺序来,下一个被访问的 点是 C,将 C 压入栈,标记为访问过,输出到结果中。

java 图片后缀 大全 java中的图_java 图片后缀 大全_17

第九步,当前顶点为 C,与 C 相连并尚未被访问到的顶点是 H,将 H 压入栈,标记为访问过,输出到结 果中。

java 图片后缀 大全 java中的图_java 图片后缀 大全_18

第十步,当前顶点是 H,由于和它相连的点都被访问过了,将它弹出栈。

java 图片后缀 大全 java中的图_邻接矩阵_19

第十一步,当前顶点是 C,与 C 相连的点都被访问过了,将 C 弹出栈

java 图片后缀 大全 java中的图_List_20

第十二步,当前顶点是 F,与 F 相连的并且尚未访问的点是 D,将 D 压入栈,输出到结果中,并标记为 访问过。

java 图片后缀 大全 java中的图_List_21

第十三步,当前顶点是 D,与它相连的点都被访问过了,将它弹出栈。以此类推,顶点 F,B,A 的邻居 都被访问过了,将它们依次弹出栈就好了。最后,当栈里已经没有顶点需要处理了,我们的整个遍历结 束。

java 图片后缀 大全 java中的图_java 图片后缀 大全_22

时间复杂度

邻接表:访问所有顶点的时间为 O(V),而查找所有顶点的邻居一共需要 O(E) 的时间,所以总的时间复杂度是O(V + E)。

邻接矩阵:查找每个顶点的邻居需要 O(V) 的时间,所以查找整个矩阵的时候需要 O(V^2) 时间

1.3.2 广度优先搜索 BFS

就是一种“地毯式”层层推进的搜索策略,即先查找离起始顶点最近的,然后是次近的,依次往外搜索.

遍历过程

还是以下图为例,需要依赖队列(Queue),先进先出(FIFO),一层一层地把与某个点相连的点放入队列中,处理节点的时候正好按照它们进入队列的顺序进行

java 图片后缀 大全 java中的图_java算法 数据结构_23

第一步,选择一个起始顶点,顶点 A 开始,把 A 压入队列,标记它为访问过(用红色标记)

java 图片后缀 大全 java中的图_邻接矩阵_24

第二步,从队列的头取出顶点 A,打印输出到结果中,同时将与它相连的尚未被访问过的点按照字母大小顺序压入队列,同时把它们都标记为访问过,防止它们被重复地添加到队列中。

java 图片后缀 大全 java中的图_邻接表_25

第三步,从队列的头取出顶点 B,打印输出它,同时将与它相连的尚未被访问过的点(也就是 E 和 F)压入队列,同时把它们都标记为访问过。

java 图片后缀 大全 java中的图_java 图片后缀 大全_26

第四步,继续从队列的头取出顶点 D,打印输出它,此时我们发现,与 D 相连的顶点 A 和 F 都被标记访问过了,所以就不要把它们压入队列里

java 图片后缀 大全 java中的图_java算法 数据结构_27

第五步,接下来,队列的头是顶点 G,打印输出它,同样的,G 周围的点都被标记访问过了,不做任何处理。

java 图片后缀 大全 java中的图_邻接矩阵_28

第六步,队列的头是 E,打印输出它,它周围的点也都被标记为访问过了,不做任何处理。

java 图片后缀 大全 java中的图_java算法 数据结构_29

第七步,接下来轮到顶点 F,打印输出它,将 C 压入队列,并标记 C 为访问过。

java 图片后缀 大全 java中的图_邻接表_30

第八步,将 C 从队列中移出,打印输出它,与它相连的 H 还没被访问到,将 H 压入队列,将它标记为访问过。

java 图片后缀 大全 java中的图_邻接表_31

第九步,队列里只剩下 H 了,将它移出,打印输出它,发现它的邻居都被访问过了,不做任何事情。

java 图片后缀 大全 java中的图_List_32

第十步,队列为空,表示所有的点都被处理完毕了,程序结束

广度优先一般是用于查找最短路径问题。

时间复杂度

邻接表:每个顶点都需要被访问一次,时间复杂度是 O(V);相连的顶点(也就是每条边)也都要被访问一次,加起来就是 O(E)。因此整体时间复杂度就是 O(V+E)

邻接矩阵:V 个顶点,每次都要检查每个顶点与其他顶点是否有联系,因此时间复杂度是O(v^2)

广度优先的搜索可以同时从起始点和终点开始进行,称之为双端 BFS。这种算法往往可以大大地提高搜索的效率。