1. 图(Graph)相关概念
1.1 基础概念
图由**顶点(vertex)和边(edge)**组成,通常表示为G = (V,E) 。 其中:G表示一个图,V是顶点集,E是边集。 V有穷且非空。 任意两个顶点之间都可以用边来表示它们之间的关系,E可以是空的。
1.2 有向图(Directed Graph)
边是有明确方向的。
- 有向无环图(Directed Acyclic Graph,简称 DAG)
如果一个有向图,从任意顶点出发无法经过若干条边回到改顶点,就是一个有向无环图。
- 出度(Out-degree):一个顶点的出度为
x ,是指由x 条边以该顶点为起点。
2. 入度(In-degree):一个顶点的入度为x ,是指由x 条边以该顶点为终点。
1.3 无向图(Undirected Graph)
无向图的边是无方向的。
1.4 混合图(Mixed Graph)
混合图的边可能是无向的,也可能是有向的。
1.5 简单图、多重图
- 无向图中,关联一对顶点的无向边如果多于一条,则称这些边为平行边;
- 有向图中,关联一对顶点的有向边多余一条且方向相同,则这些边为平行边。
- 多重图(Multigraph)
有平行边或者有自环的图。 - 简单图(Simple Graph)
既没有平行边也没有自环的图。
1.6 无向完全图(Directed Complete Graph)
无向图的任意两个顶点之间都存在边。n个顶点的无向完全图有n(n-1) / 2 条边。
1.7 无向完全图(Undirected Complete Graph)
任意两点之间都存在方向相反的两条边。n个顶点的有向完全图有n(n-1) 条边。
1.8 稠密图和稀疏图
- 稠密图:边的数量接近于或等于完全图。
- 稀疏图:边的数量远远小于完全图。
1.9 有权图(Weighted Graph)
有权图的边拥有权值。
1.10 连通图(Connected Graph)
如果顶点x 和y 之间存在可互相抵达的路径(直接或者间接),则称x 和y 是连通的。 如果无向图种任意两个顶点都是连通的,则称为连通图。
- 连通分量(Connected Component):无向图的极大连通子图。连通图只有一个连通分量,即它本身。非连通的无向图有多个连通分量。
如:下图中有三个连通分量。
1.11 强连通图(Strongly Connected Graph)
有向图中任意两个顶点都是连通的,称为强连通图。
- 强连通分量(Strongly Connected Component):有向图的极大强连通子图。连通图只有一个连通分量,即它本身。非连通的有向图有多个强连通分量。
如:下图中有三强个连通分量。
1.12 邻接矩阵(Adjacency Matrix)实现图的存储
- 一维数组存放顶点信息;
- 二维数组存放边信息。
- 无向图:二维数组中两个顶点有边用1表示,否则用0来表示。
1.13 邻接表(Adjacency List)实现图的存储
- 一维数组存放顶点信息;
- 顶点作为链表的头节点,后面指向直接可达的顶点。
- 无向图:在链表后面存储当前顶点可以直接通往的顶点。
- 有向图:在链表后面存储当前顶点作为出度直接可达的顶点。
- 逆邻接表:在链表后面存储当前顶点作为入度直接可达的顶点。
- 有权图:在链表后面存储当前顶点作为出度直接可达的顶点,并在顶点的节点类中存储权值。
2. 图的实现
2.1 接口设计
public interface Graph<V,E>{
int edgeSize();
int verticesSize();
void addVertex(V v);
void addEdge(V from, V to);
void addEdge(V from, V to, E weight);
void removeVertex(V v);
void removeEdge(V from, V to);
public abstract void bfs(V v, VertexVisitor<V> visitor);
public abstract void dfs(V v, VertexVisitor<V> visitor);
public abstract void difOfStack(V v, VertexVisitor<V> visitor);
public interface VertexVisitor<V> {
boolean visit(V v);
}
}
2.2 HashMap + HashSet 实现图
private static class Vertex<V,E>{
V v;
Set<Edge<V, E>> inEdges = new HashSet<>();
Set<Edge<V, E>> outEdges = new HashSet<>();
public Vertex(V v) {
this.v = v;
}
@Override
public boolean equals(Object o) {
return Objects.equals(v, ((Vertex<V,E>) o).v) ;
}
@Override
public int hashCode() {
return Objects.hash(v);
}
}
private static class Edge<V,E>{
Vertex<V, E> from;
Vertex<V, E> to;
E weight;
@Override
public boolean equals(Object o) {
return Objects.equals(from, ((Edge<V,E>) o).from) && Objects.equals(to, ((Edge<V,E>) o).to);
}
@Override
public int hashCode() {
return Objects.hash(from, to);
}
}
private Map<V, Vertex<V, E>> vertices = new HashMap<>();
private Set<Edge<V, E>> edges = new HashSet<>();
@Override
public int edgeSize() {
return edges.size();
}
@Override
public int verticesSize() {
return vertices.size();
}
@Override
public void addVertex(V v) {
if (vertices.containsKey(v)) return;
vertices.put(v,new Vertex<>(v));
}
@Override
public void addEdge(V from, V to) {
addEdge(from, to ,null);
}
@Override
public void addEdge(V from, V to, E weight) {
Vertex<V, E> fromVertex = vertices.get(from);
if (fromVertex == null){
fromVertex = new Vertex<>(from);
vertices.put(from,fromVertex);
}
Vertex<V, E> toVertex = vertices.get(to);
if (toVertex == null){
toVertex = new Vertex<>(to);
vertices.put(to,toVertex);
}
Edge<V, E> edge = new Edge<>(fromVertex, toVertex, weight);
if (fromVertex.outEdges.remove(edge)) {
toVertex.inEdges.remove(edge);
edges.remove(edge);
}
fromVertex.outEdges.add(edge);
toVertex.inEdges.add(edge);
edges.add(edge);
fromVertex.outEdges.add(edge);
toVertex.inEdges.add(edge);
edges.add(edge);
}
@Override
public void removeEdge(V from, V to) {
Vertex<V, E> fromVertex = vertices.get(from);
if (fromVertex == null) return;
Vertex<V, E> toVertex = vertices.get(to);
if (toVertex == null) return;
Edge<V, E> edge = new Edge<>(fromVertex,toVertex);
if (fromVertex.outEdges.remove(edge)) {
fromVertex.inEdges.remove(edge);
edges.remove(edge);
}
}
@Override
public void removeVertex(V v) {
Vertex<V, E> vertex = vertices.remove(v);
if (vertex == null) return;
Iterator<Edge<V, E>> outIterator = vertex.outEdges.iterator();
while (outIterator.hasNext()) {
Edge<V, E> edge = outIterator.next();
edge.to.inEdges.remove(edge);
outIterator.remove();
edges.remove(edge);
}
Iterator<Edge<V, E>> inIterator = vertex.inEdges.iterator();
while (inIterator.hasNext()) {
Edge<V, E> edge = inIterator.next();
edge.from.outEdges.remove(edge);
inIterator.remove();
edges.remove(edge);
}
}
3. 广度优先搜索(Breadth First Search,简称 BFS)
又称宽度优先搜索、横向优先搜索,是一种图的遍历实现方式。 图的遍历是指从图中某一个顶点出发访问图的其余顶点,且每个顶点仅被访问一次。 二叉树的层序遍历就是一种广度优先算法。
遍历结果中的顶点没有规定的顺序。
- 无序图的遍历:以顶点
A 开始,作为第一层;以顶点A 直接可达的顶点B、F 为第二层;依次类推G、I、C、E 为第三层;H、D 为第四层。 - 有序图的遍历:遍历的顺序与无序图一致,但是要遵守方向。
- 实现:与二叉树的层序遍历思路完全相同,即使用一个队列将遍历得到的顶点加入到队列中;但因为需要保证每个顶点只到达一次,需要使用
hashset 来实现。
@Override
public void bfs(V v, VertexVisitor<V> visitor) {
if (visitor == null) return;
Vertex<V, E> beginVertex = vertices.get(v);
if (beginVertex == null) return;
Set<Vertex<V, E>> set = new HashSet<>();
set.add(beginVertex);
Queue<Vertex<V, E>> queue = new LinkedList<>();
queue.offer(beginVertex);
while (!queue.isEmpty()){
Vertex<V, E> vertex = queue.poll();
if (visitor.visit(vertex.v)) return;
set.add(vertex);
for (Edge<V,E> outEdge : vertex.outEdges) {
if (set.contains(outEdge.to)) continue;
queue.offer(outEdge.to);
}
}
}
4. 深度度优先搜索(Depth First Search,简称 DFS)
二叉树的前序遍历其实就是深度优先搜索的一种。 其实就是从选定的一个顶点,一直往下走,直到走不动就倒退一个顶点选择其它的方向走,走不动再倒退顶点,类推…。
- 无序图的遍历:
- 有序图的遍历:如下图中:先走蓝线,等到再次走到顶点
e 时,无法再进行遍历,就回退,一直回退到节点e ,然后选择其它方向f 。
- 实现:与前序遍历思路相同,将切换到左右子节点的代码换成切换成当前顶点往外出去的线的终点的点,再附加
set 保证唯一即可。
@Override
public void dfs(V v, VertexVisitor<V> visitor) {
if (visitor == null) return;
Vertex<V, E> beginVertex = vertices.get(v);
if (beginVertex == null) return;
dfs(beginVertex, new HashSet<>(), visitor);
}
private void dfs(Vertex<V, E> vertex, Set<Vertex<V,E>> set, VertexVisitor<V> visitor){
if (visitor.visit(vertex.v)) return;
set.add(vertex);
for (Edge<V,E> outEdge : vertex.outEdges) {
if (set.contains(outEdge.to)) continue;
dfs(outEdge.to, set, visitor);
}
}
- 非递归方式,使用栈实现:
这里以顶点 1 作为开始顶点进行遍历。
- 将开始顶点入栈,并进行输出和出栈:
入栈: 出栈: - 根据当前栈顶的顶点元素获取一条边,将改边的起点和终点按顺序加入到栈中,然后输出栈顶顶点;
break 退出循环保证只访问一条边。 这里选择1,3这条边。 入栈: - 弹出栈顶元素,再选择一条边,将起点和终点按照顺序加入到栈中,打印此时的栈顶元素。
弹出栈顶元素: 入栈起点和终点: - 此时顶点7并没有其它的边可以遍历,只能弹出再弹出,直到弹出到顶点1时才能够选择其它的边,再进行入栈操作。
@Override
public void difOfStack(V v, VertexVisitor<V> visitor) {
Vertex<V, E> beginVertex = vertices.get(v);
if (beginVertex == null) return;
Set<Vertex<V, E>> set = new HashSet<>();
Stack<Vertex<V ,E>> stack = new Stack<>();
stack.push(beginVertex);
set.add(beginVertex);
if (visitor.visit(beginVertex.v)) return;
while (!stack.isEmpty()){
Vertex<V, E> vertex = stack.pop();
for (Edge<V, E> edge : vertex.outEdges) {
if (set.contains(edge.to)) continue;
stack.push(edge.from);
stack.push(edge.to);
set.add(edge.to);
if (visitor.visit(edge.to.v)) return;
break;
}
}
}
|