图的分类:
我们的数据结构大体可以分为三类 :线性结构 、 树结构 、图结构 这三种结构都是既可以用 数组表示 也可以用链表表示。
图的表示需要表示的就是图的顶点和图的边。
顶点:vertex 边: edge 无向图:undirected graph 有向图 : directed graph
有些地方建模需要用有向图 ,有些需要无向图 : 比如交通网络需要无向图 。 比如微信的好友关系 这是双向的, 没有方向的。 但是在微博中的关注关系,需要使用有向图进行表示。
除了描述两个点之间有没有边之外,还有描述这个边有没有权值。 按照图是否有方向和是否有权值可以分为四类。 之所以分类,这是因为有些算法只适合部分类型的图。
图的基本概念:
无向无权图:没有方向,没有权重
两点相邻: 如果两个点是相邻的。 点的邻边:和当前的点相邻的边。
我们知道一个点相邻的点 就可以知道和一个点相邻的边。
路径path: 这个不用解释了。
环Loop : 从一个顶点出发,存在一条路径 回到这个顶点
自环边: 不经过其他顶点,自己到自己
平行边: 两个顶点之间不止一条边
一般有自环边和平行边需要处理一下,比如求最短路径时,只需要保存较短的那条表即可
没有自环边和平行边的图称为简单图
连通分量: 一张图中相互可达的顶点集合构成一个连通分量 一个图可能有多个连通分量
有环图:
无环图: 树是一种无环图 无环图不一定是一棵树,因为连通分量不是1。联通的无环图 是一棵树
连通图的生成树:连通图的生成树包含所有顶点的树 边数为v-1
生成森林: 一个图一定有生成森林 我们主要关注生产树
顶点的度(degree) : 对于无向无权图来说,就是这个顶点相邻的边数。顶点的度是顶 点的一个属性
图的基本表示-邻接矩阵
从无向无权图开始。用二维数组存储一幅图 对于简单图来说,主对角线为0 , 因为没有自环边。 无向图的邻接矩阵是关于主对角线对称的。
// 第一版邻接矩阵代码
package graphBasic;
import java.io.File;
import java.io.IOException;
import java.util.Scanner;
public class AdjMatrix {
private int V; // 顶点数
private int E; //边的条数
private int[][] adj; // 邻接矩阵
public AdjMatrix(String filename) {
File file = new File(filename); // 这里学会了如何简单处理文件
try (Scanner scanner = new Scanner(file)) { // 必须要处理异常
V = scanner.nextInt();
adj = new int[V][V];
E = scanner.nextInt();
for (int i = 0; i < E; i++) { // 建立邻接矩阵
int a = scanner.nextInt();
int b = scanner.nextInt();
adj[a][b] = 1;
adj[b][a] = 1;
}
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(String.format("V = %d , E = %d\n", V, E));
for (int i = 0; i < V; i++) {
for (int j = 0; j < V; j++) {
stringBuilder.append(String.format("%d ", adj[i][j]));
}
stringBuilder.append("\n");
}
return stringBuilder.toString();
}
public static void main(String[] args) {
// 默认一个工程的性对路径是在这个工程的文件夹下,也就是Graph 这个文件夹下
AdjMatrix adjMatrix = new AdjMatrix("g.txt");
System.out.println(adjMatrix);
}
}
// 打印输出传入的图的邻接矩阵
V = 7 , E = 9
0 1 0 1 0 0 0
1 0 1 0 0 0 1
0 1 0 1 0 1 0
1 0 1 0 1 0 0
0 0 0 1 0 1 0
0 0 1 0 1 0 1
0 1 0 0 0 1 0
加入更多的方法:
package graphBasic;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Scanner;
public class AdjMatrix {
private int V; // 顶点数
private int E; //边的条数
private int[][] adj; // 邻接矩阵
// 构造函数
public AdjMatrix(String filename) {
File file = new File(filename);
try (Scanner scanner = new Scanner(file)) { // 必须要处理异常
V = scanner.nextInt();
if (V < 0) throw new IllegalArgumentException("V must be non-negative");
adj = new int[V][V];
E = scanner.nextInt();
if (E < 0) throw new IllegalArgumentException("E must be non-negative");
for (int i = 0; i < E; i++) { // 建立邻接矩阵
int a = scanner.nextInt();
int b = scanner.nextInt();
validateVertax(a);
validateVertax(b);
if (a == b) // 自环边
throw new IllegalArgumentException("self loop is detected");
if (adj[a][b] == 1) // 平行边
throw new IllegalArgumentException("parallel edge is degteced");
adj[a][b] = 1;
adj[b][a] = 1;
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 判断顶点的合法性
private void validateVertax(int v) {
if (v < 0 || v >= V)
throw new IllegalArgumentException("verrtex" + v + "is invalid");
}
public int V() {
return V;
}
public int E() {
return E;
}
// 是否存在一条边
public boolean hasEdge(int v, int w) {
validateVertax(w);
validateVertax(v);
return adj[v][w] == 1;
}
// 返回和V 相邻的顶点集合
public ArrayList<Integer> adj(int v) {
validateVertax(v);
ArrayList<Integer> res = new ArrayList<>();
for (int i = 0; i < V; i++) {
if (adj[v][i] == 1)
res.add(i);
}
return res;
}
// 求一个顶点的度
public int degree(int v) {
// validateVertax(v); 这里不做验证 adj这里会进行验证
return adj(v).size();
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(String.format("V = %d , E = %d\n", V, E));
for (int i = 0; i < V; i++) {
for (int j = 0; j < V; j++) {
stringBuilder.append(String.format("%d ", adj[i][j]));
}
stringBuilder.append("\n");
}
return stringBuilder.toString();
}
public static void main(String[] args) {
// 默认一个工程的性对路径是在这个工程的文件夹下,也就是Graph 这个文件夹下
AdjMatrix adjMatrix = new AdjMatrix("g.txt");
System.out.println(adjMatrix);
}
}
图的表示-邻接表
图的邻接矩阵 : 空间复杂度 O(V^2)
建立图的时间复杂度:O(E)
查看两个节点是否相邻:O(1)
求一个点的相邻节点: O( V ) 遍历一遍其他的所有顶点
瓶颈在图的空间复杂度和求一个点的相邻节点
稀疏图和稠密图
如果一个图有3000个节点,并且节点的度最大为3 那么边数为 3000*3/2=4500 边
如果是一个3000个顶点的完全图,大约是4498500( 450万条) 两者差1000倍
大多数的情况下,我们处理的都是稀疏图
邻接表表示:
0:1 3
1:0 2 6
2:1 3 5
3:0 2 4
4:3 5
5:2 4 6
6:1 5
编程实现邻接表
把arrayList 全部换成linkedList 对一些方法做出语法修改,提出 在编程过程中,一般来说i、j、k 我们用来表示下标, 而在图论算法中,u 、v 、w 一般用来表示图的顶点
package graphBasic;
import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.Scanner;
public class AdjList {
private int V; // 顶点数
private int E; //边的条数
private LinkedList<Integer>[] adj; // 邻接表
// 构造函数
public AdjList(String filename) {
File file = new File(filename);
try (Scanner scanner = new Scanner(file)) { // 必须要处理异常
V = scanner.nextInt();
if (V < 0) throw new IllegalArgumentException("V must be non-negative");
adj = new LinkedList[V]; // V个链表
for (int i = 0; i < V; i++) {
adj[i] = new LinkedList<>(); // 类型推断 Integer 可以省略
}
E = scanner.nextInt();
if (E < 0) throw new IllegalArgumentException("E must be non-negative");
for (int i = 0; i < E; i++) { // 建立邻接表
int a = scanner.nextInt();
int b = scanner.nextInt();
validateVertax(a);
validateVertax(b);
if (a == b) // 自环边
throw new IllegalArgumentException("self loop is detected");
if (adj[a].contains(b)) // 平行边
throw new IllegalArgumentException("parallel edge is degteced");
adj[a].add(b); // O(v) 级别的操作,链表长度级别的
adj[b].add(a);
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 判断顶点的合法性
private void validateVertax(int v) {
if (v < 0 || v >= V)
throw new IllegalArgumentException("verrtex" + v + "is invalid");
}
public int V() {
return V;
}
public int E() {
return E;
}
// 是否存在一条边
public boolean hasEdge(int v, int w) {
validateVertax(w);
validateVertax(v);
return adj[v].contains(w); //
}
// 返回和V 相邻的顶点集合
public LinkedList<Integer> adj(int v) {
validateVertax(v);
return adj[v];
}
// 求一个顶点的度
public int degree(int v) {
// validateVertax(v); 这里不做验证 adj这里会进行验证
return adj(v).size();
}
@Override
public String toString() { // 这里需要做一些修改
// 我们一般都 i j k 表示index uvw 表示图论中的顶点
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(String.format("V = %d , E = %d\n", V, E));
for (int v = 0; v < V; v++) {
stringBuilder.append(String.format("%d : ", v));
for (int w : adj[v]) { // adj[v].get(j)
stringBuilder.append(String.format("%d ", w));
}
stringBuilder.append("\n");
}
return stringBuilder.toString();
}
public static void main(String[] args) {
// 默认一个工程的性对路径是在这个工程的文件夹下,也就是Graph 这个文件夹下
AdjList adjList = new AdjList("g.txt");
System.out.println(adjList);
}
}
V = 7 , E = 9
0 : 1 3
1 : 0 2 6
2 : 1 3 5
3 : 0 2 4
4 : 3 5
5 : 2 4 6
6 : 1 5
邻接表的问题和改进
V = 7 , E = 9
0 : 1 3
1 : 0 2 6
2 : 1 3 5
3 : 0 2 4
4 : 3 5
5 : 2 4 6
6 : 1 5
空间复杂度: O(V + E) 首先由多少顶点就有多少链表 每一个链表都要存储和这个顶点相邻的顶点,所有的相邻顶点之和,就是图中边的总数 2 (无权图) 在O( ) 下 2倍被省略了。 写成O(E)可以吗? 其实是有道理的,比如一棵树,V = E+1 -> O(2E-1) 但是如果在极端情况下,这个图没有边, 复杂度就变成O(0) 这显然是不对的。
时间复杂度:建立图 O( E * V )我们在判断平行边是需要扫描整个链表 O( V ) 这是一个很大的性能开销。 查看两点是否相邻: O(degree(v)) 对整个链进行线性扫描 求一个点的相邻节点 : O(degree(v)) ( 因为我们展现出来给用户还是要遍历整个链表), 邻接矩阵是O( v )(扫描一整行)
快速查重 快速查看两个点是否相邻:不使用链表 而是使用 hashset(O(1)) threeset O((log V)) 哈希表或者红黑树 看上去红黑树时间复杂度高一些,但是其实两者性能差距非常低。
红黑树的优势是: 红黑树实现的集合是一个有序集 这样我们输出一个顶点的相邻顶点结合,那么输出的顺序一定是从小较大的。相对节省空间
hash表的优势是:快
log 级别到底是一个什么级别:我么都知道 O(1) < O(logn) < O(n) 如果 n =100万
1 < 20 < 1000000 如果n =10亿 logn 是30 ,这更接近1
改进代码:使用红黑树
package graphBasic;
import java.io.File;
import java.io.IOException;
import java.util.TreeSet;
import java.util.Scanner;
public class AdjSet {
private int V; // 顶点数
private int E; //边的条数
private TreeSet<Integer>[] adj; // treeset类型的数组
// 构造函数
public AdjSet(String filename) {
File file = new File(filename);
try (Scanner scanner = new Scanner(file)) { // 必须要处理异常
V = scanner.nextInt();
if (V < 0) throw new IllegalArgumentException("V must be non-negative");
adj = new TreeSet[V]; // V个集合
for (int i = 0; i < V; i++) {
adj[i] = new TreeSet<>(); // 类型推断 Integer 可以省略
}
E = scanner.nextInt();
if (E < 0) throw new IllegalArgumentException("E must be non-negative");
for (int i = 0; i < E; i++) { // 建立邻接表
int a = scanner.nextInt();
int b = scanner.nextInt();
validateVertax(a);
validateVertax(b);
if (a == b) // 自环边
throw new IllegalArgumentException("self loop is detected");
if (adj[a].contains(b)) // 平行边
throw new IllegalArgumentException("parallel edge is degteced");
adj[a].add(b); // O(v) 级别的操作,链表长度级别的
adj[b].add(a);
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 判断顶点的合法性
private void validateVertax(int v) {
if (v < 0 || v >= V)
throw new IllegalArgumentException("verrtex" + v + "is invalid");
}
public int V() {
return V;
}
public int E() {
return E;
}
// 是否存在一条边
public boolean hasEdge(int v, int w) {
validateVertax(w);
validateVertax(v);
return adj[v].contains(w); //
}
// 返回和V 相邻的顶点集合
public Iterable<Integer> adj(int v) { // 返回接口对象 这几种结构都实现了iterable
// 这样可以统一方法 这里就进行一次抽象
validateVertax(v);
return adj[v];
}
// 求一个顶点的度
public int degree(int v) {
validateVertax(v); //这里不做验证 adj这里会进行验证
return adj[v].size();
}
@Override
public String toString() { // 这里需要做一些修改
// 我们一般都 i j k 表示index uvw 表示图论中的顶点
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(String.format("V = %d , E = %d\n", V, E));
for (int v = 0; v < V; v++) {
stringBuilder.append(String.format("%d : ", v));
for (int w : adj[v]) { // adj[v].get(j) 这里用的是迭带遍历
stringBuilder.append(String.format("%d ", w));
}
stringBuilder.append("\n");
}
return stringBuilder.toString();
}
public static void main(String[] args) {
// 默认一个工程的相对路径是在这个工程的文件夹下,也就是Graph 这个文件夹下
AdjSet adjSet = new AdjSet("g.txt");
System.out.println(adjSet);
}
}
本章小结
空间 | 建图时间 | 查看两点是否相邻(后面是最坏情况) | 查找点所有的邻边(后面是最坏情况) | |
---|---|---|---|---|
邻接矩阵 | O(v^2) | O(E) | O(1) | O(V) |
邻接表(链表) | O(v+E) | O(E) 如果查平行边:O(E*V) | O(degree(v)) O(v) | O(degree(v)) O(V) |
邻接表(红黑树) | O(V+E) | O(E*logv) | O(degree(v)) O(v) | O(degree(v) ) O(V) |
括号中所说的最坏情况指的是,v这个顶点和其他所有的顶点都相连,这样查找的长度就是V-1 ,如此,时间复杂度就是 O(V) . 但是一般我们处理的图都是稀疏图,O(V)是远远大于O(logV)
是不是可以做一个接口进行统一?其实是可以的
如果是稠密图,邻接矩阵也没有什么太大的优势。