版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jt102605/article/details/84614835
一. 图的认识
1. 图的分类:
按照方向性:
- 无向图(Undirected Graph):边是没有方向的
- 有向图((Directed Graph):边具有方向性,有向图的不对称性会催生出一些比较复杂的算法
按照权重性:
- 无权图(Unweighted Graph)
- 有权图(Weighted Graph)
2.图的连通性:
在一张图中,并不一定所有的节点都是连通的。
3.简单图
简单图是指没有自环边和平行边的图。
二. 图的表示
图的表示主要有两种方法:邻接矩阵和邻接表
- 邻接矩阵适合表示稠密图(Dense Graph),且可以很容易的去掉平行边 ( 判断一条边是否存在的时间复杂度为O(1) )
- 邻接表适合表示稀疏图(Sparse Graph),去除平行边需要额外的处理 ( 判断一条边是否存在的时间复杂度为O(n) )
1.邻接矩阵(Adjacency Matrix): 用一个 n*n 的二维矩阵表示(n为节点个数)
- 对于无向图,关于右斜对角线对称
- 对于有向图,关于右斜对角线不一定对称
//稠密图 邻接矩阵的表示
public class DenseGraph implements Graph {
private int n; //节点个数 用 0~n-1 表示n个顶点
private int m; //边的条数
private boolean isDirected; //是否为有向图
private boolean[][] g; //图的具体数据 g[v][w]=true 表示边 v——>w 存在
public DenseGraph(int n, boolean isDirected){
this.n = n;
this.m = 0; //初始化为没有边
this.isDirected = isDirected;
// false为boolean型变量的默认值
// g初始化为n*n的布尔矩阵, 每一个g[i][j]均为false, 表示没有任何边
this.g = new boolean[n][n];
}
//返回节点个数
@Override
public int V(){
return this.n;
}
//返回边的个数
@Override
public int E(){
return this.m;
}
//检查边 v——>w 是否存在
//可以用于去除平行边的判断
//在临接矩阵的表示中,判断一条边是否存在的时间复杂度为O(1),而在邻接表中为O(n)
@Override
public boolean hasEdge(int v, int w){
assert v>=0 && v<n;
assert w>=0 && w<n;
return g[v][w];
}
//向图中两个节点v,w之间添加边
@Override
public void addEdge(int v, int w){
assert v>=0 && v<n;
assert w>=0 && w<n;
//去除平行边
if(hasEdge(v, w)){
return;
}
g[v][w] = true;
//若为无向图,则双向加边
if(!this.isDirected){
g[w][v] = true;
}
m++;
}
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销
@Override
public Iterable<Integer> adjIterator(int v){
assert v>=0 && v<n;
Vector<Integer> vector = new Vector<>();
for(int i=0; i<n; i++){
if(g[v][i]){
vector.add(i);
}
}
return vector;
}
@Override
public void show(){
for(int i=0; i<n; i++){
for(int j=0; j<n; j++){
if(g[i][j]){
System.out.print(1+"\t");
}else{
System.out.print(0+"\t");
}
}
System.out.println();
}
}
}
2.邻接表(Adjacency Lists): 用一个 n 个链表表示(n为节点个数)
//稀疏图 邻接表表示
public class SparseGraph implements Graph {
private int n; //点的个数
private int m; //边的个数
private boolean isDericted; //是否为有向图
private Vector<Integer>[] g; //通过向量的数组来表示图 0~n-1表示n个节点,g[i]是一个vector,其内部的元素表示与节点i有连接的节点
public SparseGraph(int n, boolean isDericted){
this.n = n;
this.m = 0; //初始化没有边
this.isDericted = isDericted;
//装有泛型类的容器的数组的创建方法,需要强制转换
// g初始化为n个空的vector, 表示每一个g[i]都为空, 即没有任和边
g = (Vector<Integer>[]) new Vector[n];
for(int i = 0; i<n; i++){
g[i] = new Vector<Integer>();
}
}
@Override
public int V(){
return this.n;
}
@Override
public int E(){
return this.m;
}
//检查边 v——>w 是否存在, 即判断g[v]中是否存在w
//可以用于去除平行边的判断
//在临接矩阵的表示中,判断一条边是否存在的时间复杂度为O(1),而在邻接表中为O(n)
@Override
public boolean hasEdge(int v, int w){
assert v>=0 && v<n;
assert w>=0 && w<n;
for(int i=0; i<g[v].size(); i++){
if(g[v].elementAt(i) == w){
return true;
}
}
return false;
}
//向图中两个节点v,w之间添加边,即向g[v]中添加w
@Override
public void addEdge(int v, int w){
assert v>=0 && v<n;
assert w>=0 && w<n;
g[v].add(w);
//去除自环边,若为无向图,则双向加边
if(v!=w && !isDericted){
g[w].add(v);
}
m++;
}
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销
@Override
public Vector<Integer> adjIterator(int v){
assert v>=0 && v<n;
return g[v];
}
@Override
public void show(){
for(int i=0; i<n; i++){
System.out.print("Vertex "+i+":\t");
for(int j=0; j<g[i].size(); j++){
System.out.print(g[i].elementAt(j)+"\t");
}
System.out.println();
}
}
}
三. 图的基本操作
1.遍历某个节点的邻边(图算法中最常见的操作)
遍历邻边操作邻接表更加高效
邻接表表示的稀疏图遍历某个节点操作:
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销
public Vector<Integer> adjIterator(int v){
assert v>=0 && v<n;
return g[v];
}
邻接矩阵表示的稀疏图遍历某个节点操作:
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销
public Iterable<Integer> adjIterator(int v){
assert v>=0 && v<n;
Vector<Integer> vector = new Vector<>();
for(int i=0; i<n; i++){
if(g[v][i]){
vector.add(i);
}
}
return vector;
}
2. 图的深度优先遍历(DFS)
- 图的深度优先遍历对有向图同样适用
- 图的深度优先遍历复杂度
//递归实现 图的深度优先遍历
private void dfs(int v){
//对节点v进行遍历,说明v还没有被遍历过,首先将其置为true
isVisited[v] = true;
//对节点v的相邻节点递归进行dfs
Iterable<Integer> iterable = graph.adjIterator(v);
for(int vertex : iterable){
if(!isVisited[vertex]){
dfs(vertex);
}
}
}
- 深度优先遍历的一个典型应用就是求图的连通分量
//计算一个图的连通分量
public class Component {
private Graph graph; //要计算连通分量的图
private boolean[] isVisited; //记录图中节点是否被遍历过
private int count; //记录连通分量的个数
private int[] group; //记录每个节点所属的连通组别
public Component(Graph graph){
this.graph = graph;
this.count = 0;
this.group = new int[graph.V()];
for(int i = 0; i<group.length; i++){
group[i] = -1;
}
this.isVisited = new boolean[graph.V()];
for(int i=0; i<isVisited.length; i++){
isVisited[i] = false;
}
//求连通分量
for(int i=0; i<graph.V(); i++){
if(!isVisited[i]){
dfs(i);
count++;
}
}
}
//递归实现 图的深度优先遍历
private void dfs(int v){
//对节点v进行遍历,说明v还没有被遍历过,首先将其置为true
isVisited[v] = true;
//遍历该节点时,记录该节点所属的连通组别
group[v] = count;
//对节点v的相邻节点递归进行dfs
Iterable<Integer> iterable = graph.adjIterator(v);
for(int vertex : iterable){
if(!isVisited[vertex]){
dfs(vertex);
}
}
}
//返回连通分量的个数
public int count(){
return this.count;
}
//判断两个节点v,w是否是连通的
public boolean isConnected(int v, int w){
assert v>=0 && v<graph.V();
assert w>=0 && w<graph.V();
return group[v] == group[w];
}
}
- 利用图的深度优先遍历可以获得两个节点间的一条路径,但此路径不一定是最短路径
- 具体思路是维护一个from数组,用来记录待遍历节点的前一个节点,从而形成一条路径
//寻找图中从原点s出发的路径
public class Path {
private Graph graph; //要寻找路径的图
private int s; //寻路的起始节点
private boolean[] isVisited; //记录dfs过程中节点是否被遍历过
private int[] from; //记录遍历过程中当前节点的前一个节点, from[i] = v 表示i节点的前一个节点是v节点
public Path(Graph graph, int s){
this.graph = graph;
assert s>=0 && s<graph.V();
this.s = s;
this.isVisited = new boolean[graph.V()];
this.from = new int[graph.V()];
for(int i=0; i<from.length; i++){
isVisited[i] = false;
from[i] = -1;
}
//通过dfs寻找以s为源点的 “所有的路径”
dfs(s);
}
//递归实现 dfs
private void dfs(int v){
//对节点v进行遍历,说明v还没有被遍历过,首先将其置为true
isVisited[v] = true;
for(int vertex: graph.adjIterator(v)){
if(!isVisited[vertex]){
//记录待遍历节点的前一个节点
from[vertex] = v;
dfs(vertex);
}
}
}
//判断从源点s出发是否能经过节点v
public boolean hasPath(int v){
assert v>=0 && v<graph.V();
return isVisited[v];
}
//查询从源点s到节点v的路径,返回一个vector
public Vector<Integer> path(int w){
assert w>=0 && w<graph.V();
assert hasPath(w);
Stack<Integer> stack = new Stack<>();
// 通过from数组逆向查找到从s到w的路径, 存放到栈中
int p = w;
while (p != -1){ //form[s]=-1
stack.push(p);
p = from[p];
}
// 从栈中依次取出元素, 获得顺序的从s到w的路径
Vector<Integer> vector = new Vector<>();
while (!stack.isEmpty()){
vector.add(stack.pop());
}
return vector;
}
//打印出从s点到w点的路径
public void showPath(int v){
assert hasPath(v);
Vector<Integer> res = path(v);
for (int i=0; i<res.size(); i++){
System.out.print(res.elementAt(i));
if(i!=res.size()-1){
System.out.print(" -> ");
}else{
System.out.println();
}
}
}
}
- 利用图的深度优先遍历还可以查看图中的环,查看有向图中的环是有意义的
3. 图的广度优先遍历(BFS)(求无权图最短路径的标准思路)
- 图的广度优先遍历复杂度
- 广度优先遍历思路:类似于树的广度优先遍历,借助一个队列实现;首先将源节点入队, 出队的同时时,将出队节点的未入过队的邻节点入队,直至队列为空。
//bfs 类似于树的广度优先遍历,借助一个队列实现
//首先将源节点入队, 出队的同时时,将出队节点的未入过队的邻节点入队,直至队列为空
private void bfs(int v){
Queue<Integer> q = new LinkedList<>();
q.add(v);
//对入队的节点作维护
isVisited[v] = true;
dist[v] = 0;
while(!q.isEmpty()){
int w = q.remove();
//对出队节点的邻节点作入队判断,未如过队的邻节点入队,并维护数据
for(int vertex : graph.adjIterator(w)){
if(!isVisited[vertex]){
q.add(vertex); //未入过队的节点,入队
isVisited[vertex] = true; //入队后,置为true
from[vertex] = w; //记录前一个节点
dist[vertex] = dist[w] + 1; //距源节点的距离为前一个节点的距离+1
}
}
}
}
- 利用广度优先遍历求出的路径一定是“无权图的最短路径”
- 思路:维护一个from数组,用来记录待遍历节点的前一个节点,从而形成一条路径;
//利用广度优先遍历 寻找图中两点间最短路径
public class ShortestPath {
private Graph graph; //寻找最短路径的图
private int s; //路径的源节点
private boolean[] isVisited; //bfs中记录节点是否被遍历过,bfs中,入过队列的节点即表示被遍历过,入队后置为true,此后不可再入队
private int[] from; //记录当前待遍历节点的前一个节点,from[i]=v 表示节点i的前一个节点为v
private int[] dist; //记录当前待遍历节点到源节点的距离
public ShortestPath(Graph graph, int s){
this.graph = graph;
assert s>=0 && s<graph.V();
this.s = s;
this.isVisited = new boolean[graph.V()];
this.from = new int[graph.V()];
this.dist = new int[graph.V()];
for(int i=0; i<graph.V(); i++){
isVisited[i] = false;
from[i] = -1;
dist[i] = -1;
}
//bfs, 寻找以s为源节点 到各个节点的最短路径
bfs(s);
}
//bfs 类似于树的广度优先遍历,借助一个队列实现
//首先将源节点入队, 出队的同时时,将出队节点的未入过队的邻节点入队,直至队列为空
private void bfs(int v){
Queue<Integer> q = new LinkedList<>();
q.add(v);
//对入队的节点作维护
isVisited[v] = true;
dist[v] = 0;
while(!q.isEmpty()){
int w = q.remove();
//对出队节点的邻节点作入队判断,未如过队的邻节点入队,并维护数据
for(int vertex : graph.adjIterator(w)){
if(!isVisited[vertex]){
q.add(vertex); //未入过队的节点,入队
isVisited[vertex] = true; //入队后,置为true
from[vertex] = w; //记录前一个节点
dist[vertex] = dist[w] + 1; //距源节点的距离为前一个节点的距离+1
}
}
}
}
//判断从源点s出发是否能经过节点v
public boolean hasPath(int v){
assert v>=0 && v<graph.V();
return isVisited[v];
}
//查询从源点s到节点v的路径,返回一个vector
public Vector<Integer> path(int v){
assert hasPath(v);
Stack<Integer> stack = new Stack<>();
// 通过from数组逆向查找到从s到w的路径, 存放到栈中
int p = v;
while (p!=-1){
stack.push(p);
p = from[p];
}
// 从栈中依次取出元素, 获得顺序的从s到w的路径
Vector<Integer> res = new Vector<>();
while(!stack.isEmpty()){
res.add(stack.pop());
}
return res;
}
//打印出从s点到w点的路径
public void showPath(int v){
assert hasPath(v);
Vector<Integer> vector = path(v);
for(int i=0; i<vector.size(); i++){
System.out.print(vector.elementAt(i));
if(i!=vector.size()-1){
System.out.print(" -> ");
}else {
System.out.println();
}
}
}
// 查看从s点到w点的最短路径长度
// 若从s到w不可达,返回-1
public int length(int v){
assert v>=0 && v<graph.V();
return dist[v];
}
}