@java
图是一种比线性表和树更为复杂的数据结构。在线性表中,数据元素之间仅有线性关系,每一个数据元素只有一个直接前驱和一个直接后继;在树形结构中,数据元素之间有着明显的层次关系,而且每一层中的数据元素可能和下一层中的多个元素(即其孩子结点)相关,但只能和上一层中一个元素(即其双亲结点)相关; 而在图结构中,结点之间的关系能够是任意的,图中任意两个数据元素之间均可能相关。node
在计算机科学中的图是由点和边构成的。git
图(Graph) G由两个集合V和E组成,记为G=(V,E) , 其中V是顶点的有穷非空集合,E是V中顶点偶对的有穷集合,这些顶点偶对称为边。V(G)和E(G)一般分别表示图G的顶点集合和边集合,E(G)能够为空集。若 E(G)为空,则图G只有顶点而没有边。github
对于图G ,若边集E(G)为有向边的集合,则称该图为有向图;若边集E(G)为无向边的集合,则称该图为无向图。算法
在有向图中,顶点对<x, y>是有序的,它称为从顶点 x到顶点y的一条有向边。 所以<x,y>与<y, x>是不一样的两条边。 顶点对用一对尖括号括起来,x是有向边的始点,y是有向边的终点。<x, y>也称做一条弧,则 x为弧尾, y为弧头。segmentfault
在无向图中,顶点对<x, y>是无序的,它称为从顶点 x与顶点y相关联的一条边。这条边没有特定的方向,(x,y) 和 (y,x)是同一条边。为了区别于有向图,无向图的一对顶点用括号括起来。数组
用n表示图中顶点数目,用e表示边的数目, 来看看图结构中的一些基本术语。数据结构
无向彻底图和有向彻底图:对千无向图, 若具备 n(n- 1)/2 条边,则称为无向彻底图。对于有向图, 若具备 n(n- l)条弧,则称为有向彻底图。ide
稀疏图和稠密图:有不多条边或弧(如 e<nlog2n) 的图称为稀疏图, 反之称为稠密图。学习
权和网:在实际应用中,每条边能够标上具备某种含义的数值,该数值称为该边上的权。这些权能够表示从一个顶点到另外一个顶点的距离或耗费。这种带权的图一般称为网。
邻接点:对于 无向图 G, 若是图的边 (v, v')\(\in\)E, 则称顶点 v 和 v'互为邻接点, 即 v 和 v'相邻接。边 (v, v')依附于顶点 v 和 v', 或者说边 (v, v')与顶点 v 和 v'相关联。
度、入度和出度:顶知的度是指和v 相关联的边的数目,记为 TD(v) 。例如,图2 (b) 中G2的顶点 V3 的度是3。对于有向图,顶点v的度分为入度和出度。入度是以顶点v为头的弧的数目,记为 ID(v); 出度是以顶点 v 为尾的弧的数目,记为OD(v)。顶点 v 的度为 TD(v) = ID(v) + OD(可。例如,图2中 G1 的顶点v1的入度 ID(v1)=1, 出度 OD(v1)=2, 度TD(v1)= ID(v1) + OD(v1) =3。通常地,若是顶点 Vi 的度记为 TD(vi),那么一个有n个顶点,e条边的图,知足以下关系:
路径和路径长度:在无向图 G 中,从 顶点 v 到顶点 v'的 路径是一个顶点序列 (v = vi,0,Vi, 1,…, i;, m= v'), 其中 (vi,j-1, vi,j)\(\in\)E, 其中1\(\leq\)j\(\leq\)m。 若是 G 是有向图, 则路径也是有向的,顶点序列应满 足 <v;,1-1, vi,j)>\(\in\)E, 其中1\(\leq\)j\(\leq\)m。 路径长度是一条路径上通过的边或弧的数目。
回路或环:第一个顶点和最后一个顶点相同的路径称为回路或环。
简单路径、 简单回路或简单环:序列中顶点不重复出现的路径称为简单路径。除了第一个顶点和最后一个顶点以外, 其他顶点不重复出现的回路,称为简单回路或简单环。
连通、连通图和连通份量:在无向图 G 中,若是从顶点 v 到顶点 v'有路径,则称 v 和 v'是连通的。若是对于图中任意两个顶点 Vi、 Vj\(\in\)V, Vi 和 Vj 都是连通的,则称 G 是连通图。图 2
(b)中的 G2 就是一个连通图,而图 4 (a) 中的 G3 则是非连通图,但 G3 有 3个连通份量,如图
4 (b) 所示。所谓连通份量, 指的是无向图中的极大连通子图。
图的存储结构相较线性表与树来讲就更加复杂。
图的存储结构比较常见的有两种,邻接矩阵和邻接表。
具体地,若图 G 中包含 n 个顶点,咱们就使用一个 n×n 的方阵 A,并使每一顶点都分别对应于某一行(列)。既然图所描述的是这些顶点各自对应的元素之间的二元关系,故能够很天然地将任意一对元素 u 和 v 之间可能存在二元关系与矩阵 A 中对应的单元 A[u, v]对应起来: 1 或 true 表示存在关系, 0 或 false 表示不存在关系。这一矩阵中的各个单元分别描述了一对元素之间可能存在的邻接关系,故此得名。
(a)是无向图, (b)是有向图。无向图的邻接矩阵,是一个对称矩阵。在图中所示的矩阵,a[i][j] 值都为1,若是是带权的图,咱们能够将其设置为权值。
这一表示形式也能够推广至带权图,具体方法是,将每条边的权重记录在该边对应得矩阵单元中。
须要注意的是:
图的邻接矩阵表示方法简单实现以下:
/** * @Author 三分恶 * @Date 2020/11/28 * @Description 图的邻接矩阵存储实现 */ public class AMWGraph { private ArrayList vertexList;//存储点的链表 private int[][] edges;//邻接矩阵,用来存储边 private int numOfEdges;//边的数目 public AMWGraph(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(vertexList.size(),vertex); } //插入结点 public void insertEdge(int v1,int v2,int weight) { edges[v1][v2]=weight; numOfEdges++; } //删除结点 public void deleteEdge(int v1,int v2) { edges[v1][v2]=0; numOfEdges--; } //获得第一个邻接结点的下标 public int getFirstNeighbor(int index) { for(int j=0;j<vertexList.size();j++) { if (edges[index][j]>0) { return j; } } return -1; } //根据前一个邻接结点的下标来取得下一个邻接结点 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; } }
邻接矩阵虽然比较直观,可是空间利用率是上并不理想。其中大量的单元所对应的边有可能并未在图中出现,这也是静态向量结构广泛的不足。既然如此,咱们为何不将向量改成列表呢?
邻接表是图的一种连接存储结构。 邻接表表示法只关心存在的边,将顶点的邻接边用列表表示。
咱们来看一下具体的实现。
这是有向图的抽象接口定义。
/** * @Author 三分恶 * @Date 2020/11/28 * @Description 有向图接口 */ public interface IDirectGraph<V> { /** * 新增顶点 * * @param v 顶点 * @since 0.0.2 */ void addVertex(final V v); /** * 删除顶点 * * @param v 顶点 * @return 是否删除成功 * @since 0.0.2 */ boolean removeVertex(final V v); /** * 获取顶点 * * @param index 下标 * @return 返回顶点信息 * @since 0.0.2 */ V getVertex(final int index); /** * 新增边 * * @param edge 边 * @since 0.0.2 */ void addEdge(final Edge<V> edge); /** * 移除边 * * @param edge 边信息 * @since 0.0.2 */ boolean removeEdge(final Edge<V> edge); /** * 获取边信息 * * @param from 开始节点 * @param to 结束节点 * @since 0.0.2 */ Edge<V> getEdge(final int from, final int to); }
这是有向图的边的实现:
/** * @Author 三分恶 * @Date 2020/11/28 * @Description 边 */ public class Edge<V> { /** * 开始节点 * @since 0.0.2 */ private V from; /** * 结束节点 * @since 0.0.2 */ private V to; /** * 权重 * @since 0.0.2 */ private double weight; public Edge(V from, V to) { this.from = from; this.to = to; } public V getFrom() { return from; } public void setFrom(V from) { this.from = from; } public V getTo() { return to; } public void setTo(V to) { this.to = to; } public double getWeight() { return weight; } public void setWeight(double weight) { this.weight = weight; } @Override public String toString() { return "Edge{" + "from=" + from + ", to=" + to + ", weight=" + weight + '}'; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Edge<?> edge = (Edge<?>) o; return Double.compare(edge.weight, weight) == 0 && to.equals(edge.to) && from.equals(edge.from); } @Override public int hashCode() { return hashCode(); } }
这里咱们再也不单独作顶点的实现,因此 节点=顶点+边。
/** * @Author 三分恶 * @Date 2020/11/28 * @Description */ public class GraphNode<V> { /** * 顶点信息 * @since 0.0.2 */ private V vertex; /** * 以此顶点为起点的边的集合,是一个列表,列表的每一项是一条边 * * (1)使用集合,避免重复 */ private Set<Edge<V>> edgeSet; /** * 初始化一个节点 * @param vertex 顶点 */ public GraphNode(V vertex) { this.vertex = vertex; this.edgeSet = new HashSet<Edge<V>>(); } /** * 新增一条边 * @param edge 边 */ public void add(final Edge<V> edge) { edgeSet.add(edge); } /** * 获取目标边 * @param to 目标边 * @return 边 * @since 0.0.2 */ public Edge<V> get(final V to) { for(Edge<V> edge : edgeSet) { V dest = edge.getTo(); if(dest.equals(to)) { return edge; } } return null; } /** * 获取目标边 * @param to 目标边 * @return 边 * @since 0.0.2 */ public Edge<V> remove(final V to) { Iterator<Edge<V>> edgeIterable = edgeSet.iterator(); while (edgeIterable.hasNext()) { Edge<V> next = edgeIterable.next(); if(to.equals(next.getTo())) { edgeIterable.remove(); return next; } } return null; } public V getVertex() { return vertex; } public Set<Edge<V>> getEdgeSet() { return edgeSet; } @Override public String toString() { return "GraphNode{" + "vertex=" + vertex + ", edgeSet=" + edgeSet + '}'; } }
接下来是有向图的邻接表表示具体实现。
/** * @Author 三分恶 * @Date 2020/11/28 * @Description */ public class ListDirectGraph<V> implements IDirectGraph<V> { /** * 节点链表 * * @since 0.0.2 */ private List<GraphNode<V>> nodeList; /** * 初始化有向图 * * @since 0.0.2 */ public ListDirectGraph() { this.nodeList = new ArrayList<GraphNode<V>>(); } public void addVertex(V v) { GraphNode<V> node = new GraphNode<V>(v); // 直接加入到集合中 this.nodeList.add(node); } public boolean removeVertex(V v) { //1. 移除一个顶点 //2. 全部和这个顶点关联的边也要被移除 Iterator<GraphNode<V>> iterator = nodeList.iterator(); while (iterator.hasNext()) { GraphNode<V> graphNode = iterator.next(); if (v.equals(graphNode.getVertex())) { iterator.remove(); } } return true; } public V getVertex(int index) { return nodeList.get(index).getVertex(); } public void addEdge(Edge<V> edge) { //1. 新增一条边,直接遍历列表。 // 若是存在这条的起始节点,则将这条边加入。 // 若是不存在,则直接报错便可。 for (GraphNode<V> graphNode : nodeList) { V from = edge.getFrom(); V vertex = graphNode.getVertex(); // 起始节点在开头 if (from.equals(vertex)) { graphNode.getEdgeSet().add(edge); } } } public boolean removeEdge(Edge<V> edge) { // 直接从列表中对应的节点,移除便可 GraphNode<V> node = getGraphNode(edge); if (null != node) { // 移除目标为 to 的边 node.remove(edge.getTo()); } return true; } public Edge<V> getEdge(int from, int to) { // 获取开始和结束的顶点 V toVertex = getVertex(from); // 获取节点 GraphNode<V> fromNode = nodeList.get(from); // 获取对应结束顶点的边 return fromNode.get(toVertex); } /** * 获取图节点 * * @param edge 边 * @return 图节点 */ private GraphNode<V> getGraphNode(final Edge<V> edge) { for (GraphNode<V> node : nodeList) { final V from = edge.getFrom(); if (node.getVertex().equals(from)) { return node; } } return null; } /** * 获取对应的图节点 * * @param vertex 顶点 * @return 图节点 * @since 0.0.2 */ private GraphNode<V> getGraphNode(final V vertex) { for (GraphNode<V> node : nodeList) { if (vertex.equals(node.getVertex())) { return node; } } return null; } }
和树的遍历相似,图的遍历也是从图中某一顶点出发,按照某种方法对图中全部顶点访问且仅访问一次。然而, 图的遍历要比树的遍历复杂得多。 由于图的任一顶点均可能和其他的顶点相邻接。 因此在访问了某个顶点以后, 可能沿着某条路径搜索以后, 又回到该顶点上。
根据搜索路径的方向, 一般有两条遍历图的路径:深度优先遍历和广度优先遍历。 它们对无向图和有向图都适用。
深度优先(DepthFirst Search, DFS)遍历相似千树的先序遍历,是树的先序遍历的推广。
对于一个连通图,深度优先搜索遍历的过程以下。
初始条件下全部节点为白色,选择一个做为起始顶点,按照以下步骤遍历:
a. 选择起始顶点涂成灰色,表示还未访问
b. 从该顶点的邻接顶点中选择一个,继续这个过程(即再寻找邻接结点的邻接结点),一直深刻下去,直到一个顶点没有邻接结点了,涂黑它,表示访问过了
c. 回溯到这个涂黑顶点的上一层顶点,再找这个上一层顶点的其他邻接结点,继续如上操做,若是全部邻接结点往下都访问过了,就把本身涂黑,再回溯到更上一层。
d. 上一层继续作如上操做,直到全部顶点都访问过。
如下面一个有向图为例来展现这个过程:
具体代码实现:
@Override public List<V> dfs(V root) { List<V> visitedList = Guavas.newArrayList(); Stack<V> visitingStack = new Stack<>(); // 顶点首先压入堆栈 visitingStack.push(root); // 获取一个边的节点 while (!visitingStack.isEmpty()) { V visitingVertex = visitingStack.peek(); GraphNode<V> graphNode = getGraphNode(visitingVertex); boolean hasPush = false; if(null != graphNode) { Set<Edge<V>> edgeSet = graphNode.getEdgeSet(); for(Edge<V> edge : edgeSet) { V to = edge.getTo(); if(!visitedList.contains(to) && !visitingStack.contains(to)) { // 寻找到下一个临接点 visitingStack.push(to); hasPush = true; break; } } } // 循环以后已经结束,没有找到下一个临点,则说明访问结束。 if(!hasPush) { // 获取第一个元素 visitedList.add(visitingStack.pop()); } } return visitedList; }
广度优先(Breadth First Search, BFS)遍历相似于树的按层次遍历的过程。
广度优先搜索在进一步遍历图中顶点以前,先访问当前顶点的全部邻接结点。
a.首先选择一个顶点做为起始结点,并将其染成灰色,其他结点为白色。
b. 将起始结点放入队列中。
c. 从队列首部选出一个顶点,并找出全部与之邻接的结点,将找到的邻接结点放入队列尾部,将已访问过结点涂成黑色,没访问过的结点是白色。若是顶点的颜色是灰色,表示已经发现而且放入了队列,若是顶点的颜色是白色,表示尚未发现
d. 按照一样的方法处理队列中的下一个结点。
基本就是出队的顶点变成黑色,在队列里的是灰色,还没入队的是白色。
如下面一个有向图为例来展现这个过程:
来看一下具体代码实现:
@Override public List<V> bfs(final V root) { List<V> visitedList = Guavas.newArrayList(); Queue<V> visitingQueue = new LinkedList<>(); // 1. 放入根节点 visitingQueue.offer(root); // 2. 开始处理 V vertex = visitingQueue.poll(); while (vertex != null) { // 2.1 获取对应的图节点 GraphNode<V> graphNode = getGraphNode(vertex); // 2.2 图节点存在 if(graphNode != null) { Set<Edge<V>> edgeSet = graphNode.getEdgeSet(); //2.3 将不在访问列表中 && 再也不处理队列中的元素加入到队列。 for(Edge<V> edge : edgeSet) { V target = edge.getTo(); if(!visitedList.contains(target) && !visitingQueue.contains(target)) { visitingQueue.offer(target); } } } //3. 更新节点信息 // 3.1 放入已经访问的列表 visitedList.add(vertex); // 3.2 当节点设置为最新的元素 vertex = visitingQueue.poll(); } return visitedList; }
上一篇:重学数据结构(6、树和二叉树)
本博客为学习笔记,参考资料以下!
水平有限,不免错漏,欢迎指正!
参考:
【1】:邓俊辉 编著. 《数据结构与算法》
【2】:王世民 等编著 . 《数据结构与算法分析》
【3】: Michael T. Goodrich 等编著.《Data-Structures-and-Algorithms-in-Java-6th-Edition》
【4】:严蔚敏、吴伟民 编著 . 《数据结构》
【5】:程杰 编著 . 《大话数据结构》
【6】:图的理解:存储结构与邻接矩阵的Java实现
【7】:java 实现有向图(Direct Graph)
【8】:数据结构——图简介(java代码实现邻接矩阵)
【9】:图的理解:深度优先和广度优先遍历及其 Java 实现