4 图
定义:由一组顶点和一组可连接两个顶点的边组成
4.1 无向图
定义:边只和两个顶点连接
4.1.1 术语表
- 自环:一条连接一个顶点和自身的边
- 平行边:连接同一对顶点的两条边
- 多重图:含有平行边的图
- 简单图:无平行边和自环的图
- 子图:由一幅图的所有边和所依附顶点组成的图(边诱导子图)
路径:由边顺序连接的一系列点
简单路径(基本道路):无重复顶点的路径
- 简单环(圈):起点终点相同的闭简单路径
- 连通图:任意一个顶点都存一条路径到任意一个顶点
树:无环连通图
- 森林:互不相连的树
生成树:含有连通图所有顶点且为树的连通图子图
- 生成树森林:所有连通子图生成树的集合
树的六个等价命题:对于G=(V,E)
- G不含简单环(圈)且G连通
- G有V-1条边且连通
- G有V-1条边且无简单环
- G连通,但删除任意一条边不再连通
- G无环,但加上任意一条边产生环
- G中任意一对顶点间仅存在一条简单路径
密度:已经连接的顶点对占所有可能连接的顶点对比例
- 稀疏/稠密图:图中不同边的数量在/不在顶点总数的一个小常数倍内
- 二分图:可以将所有结点分为两部分的图,每条边连接两点分属不同部分
4.1.2 无向图API:public class Graph
返回类型 | 方法 | 描述 |
---|---|---|
Graph(int V) | 创建V个顶点的无边图 | |
Graph(In in) | 从标准输入in读入一幅图 | |
int | V() | 顶点数 |
int | E() | 边数 |
void | addEdge(int v, int w) | 添加边v-w |
Iterable | adj(int v) | 和v相邻的所有顶点 |
String | toString() | 对象的字符串表示 |
- 本节所有算法基于adj()抽象
- 第二个构造函数接收2E+2个整数:V,E,E对0~V-1的整数(边)
常用图处理代码
- 计算v的度数
public static int degree(Graph G, int v){
int degree = 0;
for(int w: G.adj(v))
degree++;
return degree;
}
- 计算所有顶点的最大度数
public static int maxDegree(Graph G){
int max = 0;
for(int v = 0; v < G.V(); v++){
if(max < degree(G, v)) max = degree(G, v);
return max;
}
}
- 计算所有结点平均度数
public static double avgDegree(Graph G){
return 2.0*G.E()/G.V();
}
- 计算自环个数
public static int loopNum(Graph G){
int num = 0;
for(int v = 0; v < G.V(); v++){
for(int w: G.adj(v)){
if(w == v) num++;
}
}
return num;
}
4.1.2.1 图的表示
需求:
- 必须为可能的各种类型图留出足够空间
- Graph的实例方法实现要快
对比:
邻接矩阵:使用V*V的boolean矩阵,true表示两点之间有连接,否则为false
- 第一个条件不满足:V²空间在数组极大时不实际
边类数组:使用Edge类,含有int v, int w表示两端点
- 第二个条件不满足:adj()的实现需要遍历所有元素
- 邻接表数组:使用一个以顶点为索引的列表数组(如拉链法散列表),每个元素都是和该顶点相邻的顶点列表
4.1.2.2 邻接表数据结构
- 实现:将每个顶点的所有相邻顶点保存在顶点对应元素指向的链表中,链表有Bag(类似Stack),addEdge(int v, int w)时,既要添加v-w也要添加w-v,所以邻接表中每条边出现两次
性能特点:
- 空间:和V(数组大小)+E(链表大小)成正比
- 添加一条边的时间为常数:链表插入
- 遍历顶点v相邻结点的时间和v的度数成正比:遍历链表
- 顺序问题:有上述说明可知,邻接表中每个元素对应链表的元素顺序取决于输入顺序和图本身,顺序不同的邻接表可以表示相同的图
Graph数据类型
public class Graph {
private final int V;//顶点数
private int E;//边数
private Bag<Integer>[] adj;//邻接表
public Graph(int V){
this.V = V;
this.E = 0;//构造无边图
adj = (Bag<Integer>[]) new Bag[V];
for (int v = 0; v < V; v++){
adj[v] = new Bag<Integer>();//对象数组单个元素需要实例化
}
}
public Graph(In in){
this(in.readInt());//读取V并执行第一个构造函数
int E = in.readInt();//读取E,边数(非实例变量E)
for(int i = 0; i < E; i++){
int v = in.readInt();
int w = in.readInt();
addEdge(v,w);
}
}
//加入边
public void addEdge(int v, int w){
adj[v].add(w);
adj[w].add(v);
E++;
}
public int V(){ return V; }
public int E(){ return E; }
//返回与v相邻所有点
public Iterable<Integer> adj(int v){
return adj[v];
}
//字符串显示邻接表
public String toString(){
String s = V + " vertices, " + E + " edges\n";
for(int v = 0; v < V; v++){
s += v + ": ";
for (int w: this.adj(v))
s += w + " ";
s += "\n";
}
return s;
}
}
4.1.2.3 图处理算法设计模式
- 图的实现和图的处理应该分离:构造一副图,将图作为参数传递给某个算法的类