一、图的应用
图作为数据结构中的重要内容,相比于线性表的一对一的前后关系、树的一对多的层级,图的结构更为复杂,具体表现为多对多的任意关系。但是图在实际应用中有着极为广泛的应用,常见的有:
- 地图
地图APP大家应该都用过。地图将地点抽象成一个个顶点,路径抽象成边,再通过一系列算法来得出自己想要的数据,是图的一种经典用法。
- 社交网络
有一个著名的定理叫做“六度空间理论”,就是任何人至多只要通过六个人就能认识全世界的任意一个人。将人抽象成顶点,任何人的关系抽象成边,一张社交网络图就出现了。
- 推荐算法
- 思维导图
- 等等
二、图的术语
- 顶点
图中的每一个节点被称为顶点
- 边
连通顶点的线叫做边,分为有向边,和无向边
- 路径
由边顺序连接的一系列顶点组成
- 环
路径的首尾相连
- 权
顶点或边的相对重要程度,在图中一般指成本、代价
- 等等
三、图的定义和种类
- 有向图
边不仅连接顶点,还具有方向
- 无向图
边仅连接顶点,没有其他含义
- 特殊图
1、平行边:图中有顶点和另一个顶点同时拥有两条边
2、自环图:图中有环
四、图的数据结构
- 邻接矩阵
使用二维数组组成,用num[i][j]=1和num[j][i]=1表示两个顶点相连
缺陷:空间复杂度过高,O(N^2)
- 邻接表
数组加链表组成,每个索引代表一个顶点,里面的链表表示该顶点的每一个相邻的顶点,一般采用这个来表示图。
五、图的算法
搜索
- 深度优先搜索(DFS)
利用递归,优先搜索更深的路径
- 广度优先搜索(BFS)
利用队列,将同级顶点加入队列,优先搜索同级的路径
无向图
- 不加权无向图
/*
路径查找
查询一张图中两个顶点是否有路径,路线是什么。
*/
//使用深度优先搜索找出G图中v顶点的所有相邻顶点
private void dfs(Graph G, int v) {
marked[v] = true;
for (Integer w : G.adj(v)) {
//如果该顶点未被标识过则递归
if (!marked[w]) {
//每个W索引存它的上一个顶点V
edgeTo[w] = v;
dfs(G, w);
}
}
}
/*
如果需要查询是否有s起点和V顶点路径,查询 marked[v]是否为true就行了,
如果查询v到s的路线是啥那就迭代edgeTo[v]不断的获得上一个顶点,
直到v等于起点结束,获得一条路径
*/
- 加权无向图
/*
最小生成树:
在一副连通图中,连接所有顶点所需权值最小的一颗树叫做最小生成树。
原理:利用切分定理,每次找到最小生成树周围权值最小的边,直到连通所有顶点。
切分定理:将一副图以特定的规则切分成两个非空且没有交集的集合。
实现方法分为Prim算法和Kruskal算法。
*/
/*
Prim算法:
利用索引优先队列,每次在已经生成的最小生成树周围的边中选择最小权值的边进行连接,
直到所有连接所有顶点。
*/
//根据一副加权无向图,创建最小生成树计算对象
public PrimMST(EdgeWeightedGraph G) {
edgeTo = new Edge[G.V()];
distTo = new double[G.V()];
marked = new boolean[G.V()];
//存储所有边到最小子节点的边权重,默认为最大数值
for (int i = 0; i < G.V(); i++) {
distTo[i] = Double.POSITIVE_INFINITY;
}
pq = new IndexMinPriorityQueue<>(G.V());
//从0顶点为起点开始进行生成最小生成树
distTo[0] = 0;
pq.insert(0, 0.0);
while (!pq.isEmpty()) {
visit(G, pq.delMin());
}
}
//将顶点v添加到最小生成树中,并且更新数据
private void visit(EdgeWeightedGraph G, int v) {
marked[v] = true;
//获取所有相邻的边
for (Edge edge : G.adj(v)) {
int w = edge.other(v);
if (marked[w]) continue;
//如果当前边权重小于已存在最小权重则更新最小生成树
if (edge.weight() < distTo[w]) {
distTo[w] = edge.weight();
edgeTo[w] = edge;
if (pq.contains(w)) {
pq.changeItem(w, edge.weight());
} else {
pq.insert(w, edge.weight());
}
}
}
}
/*
Kruskal算法:
利用并查集和优先队列,存入所有的边到优先队列,然后每次取出剩余队列中最小的边,
如果该边两个顶点还不在一个集合中,则通过并查集连通这两个数,然后保存该边到集合。
*/
//根据一副加权无向图,创建最小生成树计算对象
public KruskalMST(EdgeWeightedGraph G) {
mst = new LinkedList<>();
uf = new UF_Tree(G.V());
pq = new MinPriorityQueue<>(G.E());
//插入所有边到优先队列
for (Edge edge : G.edges()) {
pq.insert(edge);
}
//优先队列不为空则取出最小权重的边
while (!pq.isEmpty()) {
Edge min = pq.delMin();
int v = min.either();
int w = min.other(v);
//如果该边的两个点已经在在树中,则忽略
if (uf.connected(v, w)) {
continue;
}
//如果不在,则添加到最小生成树,再通过并查集连接到一个集合
mst.add(min);
uf.union(v, w);
}
}
有向图
- 不加权有向图
/*
拓扑排序:
先检测是否有环,再基于深度优先的顶点排序,就是完成了拓扑排序。
*/
/*
判断途中是否有环:
利用栈思想,每遍历一层压栈当前元素,遍历完退栈。然后判断当前元素是否在栈中,是则有环。
*/
//基于深度优先搜索,检测图G中是否有环
private void dfs(Digraph G, int v) {
onStack[v] = true;
marked[v] = true;
for (Integer index : G.adj(v)) {
if (!marked[index]) {
dfs(G, index);
}
//同时在marked中又在onStack中就是有环
if (onStack[index]) {
hasCycle = true;
return;
}
}
onStack[v] = false;
}
/*
基于深度优先的顶点排序:
利用栈思想,将由深到浅的元素压入栈中,进行排序。
*/
//基于深度优先搜索,把顶点排序
private void dfs(Digraph G, int v) {
marked[v] = true;
for (Integer index : G.adj(v)) {
if (!marked[index]) {
dfs(G, index);
}
}
reversePost.push(v);
}
- 加权有向图
1、松弛技术
将一个皮筋沿着两个顶点的某个路径展开,如果还存在更短的路径,就把皮筋转移到更短的路径上,皮筋就可以放松了。
2、Dijkstra算法
与prim算法类似,都是利用索引优先队列,每次在已经生成的最小生成树周围的边中选择最小权值的边进行连接,直到所有连接所有顶点。不同的是,prim算法中distTo保存的是最小生成树与当前顶点的权重,而该算法保存的则是当前起点到该顶点的最小路劲的累加权重,如果当前路径的权重比已存在的累加权重更小,则替代已存在的最短路径
//松弛图G中的顶点v
private void relax(EdgeWeightedDigraph G, int v) {
//找到传入的值所有的边,找到权重最小的
for (DirectedEdge directedEdge : G.adj(v)) {
int w = directedEdge.to();
//当前边的权重 加上 起点到上一个顶点的最短路径权重 就是当前线路的权重
double weight = directedEdge.weight() + distTo[v];
//如果当前线路权重小于之前的线路权重就更新, distTo[w]默认为无限大
if (weight < distTo[w]) {
//更新线路
distTo[w] = weight;
edgeTo[w] = directedEdge;
//判断之前是否存入过W的权重
if (pq.contains(w)) {
//更新权重
pq.changeItem(w, distTo[w]);
} else {
pq.insert(w, distTo[w]);
}
}
}
}