1.图的定义
图是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为G(V,E),其中G表示一个图,V是图G的顶点的集合,E 是图G中边的集合;线性表中我们把数据元素叫做元素,树中的元素叫做结点,在图中数据元素我们称为顶点(Vertex)。在图中,若V是顶点的集合,则V非空且有穷;如下图就是一个有穷非空图:
1.1.各种图定义
1.1.1.
若顶点Vi到Vj之间的边没有方向,则称这条边为无向边;如果图中的任意两个顶点之间的边都是无向边,那么这个图就是无向图;反之则成为有向图;如图2中连接顶点A到D的有向边就是弧,A是弧尾,D是弧头,<A,D>表示弧;
1.1.2.
在图中,若存在顶点到其自身的边,且同一条边不重复出现,则这样的图叫做简单图;在无向图中,如果任意两个顶点之间都存在边,那么称该图为完全无向图;含有n个顶点的完全无向图含有
(n*(n-1))/2条边;
在有向图中,如果任意两个顶点之间都存在方向相反的两条边,则称该图为完全有向图;
1.1.3.
有很少条边或弧的图称为稀疏图,反之称为稠密图;与图的边或弧相关的数字叫做权,这些权可以表示从一个顶点到另一个顶点的距离或者耗费。这种带权的图通常称为网;如图就一张带权图:
假设有两个图G1=(V1,{E1})和G2={V2,{E2}},如果V2∈V1,且E2∈E1,则称G2是G1的子图;下图为有向图和无向图及其子图;
1.2.连通图的相关术语
在无向图G中,如果顶点V1到V2有路径,则称V1和V2是连同的。如果对于图中的任意两个顶点V和V’都是连同的,则称图G为连通图;
无向图中的极大连同子图称为连同分量;通俗点讲就是有几个子图连图分量就是几;
所谓一个连通图的生成树是一个极小连通子图,它包含图中全部的n个顶点,但只有足以构成一棵树的n-1条边,如果他有大于n-1条边,那么他一定是一个环;
如果一个有向图恰有一个顶点的入度为0,其余顶点的入度均为1,则是一颗有向树;一个有向图的生成森林由若干个有向树组成,含有图中的全部顶点,但只有足以构成若干棵不相交的有向树的弧;
1.3.图的基本操作
public interface Graph {
public int V();//获取结点的个数
public int E();//获取边的个数
public void addEdge(int v,int w);//向图中添加一条边 v-w 或者 v->w
public boolean hasEdge(int v,int w);//判断图中两个结点v-w之间是否有边(判断有没有边,不是判断连通性)
public void show();//打印图的内容
public Iterable<Integer> adj(int v);//返回图中一个结点V的所有邻边
}
1.3.1.邻接矩阵
图的邻接矩阵存储方式是用两个数组来表示图。一个一维数组存储图中的顶点信息,一个二维数组存储图的边的信息;根据邻接矩阵,可以很明确的知道图的一些信息:
1.可以判断两点之间是否有边;
2.要知道某个顶点的度,其实就是这个顶点Vi在邻接矩阵中第i行的元素之和。
3.求顶点Vi的所有邻接点就是将矩阵第i行的元素遍历一遍;
4.若矩阵对称则为有向图;
5.邻接矩阵适用于稠密图;
//邻接矩阵-用于稠密图
public class DensGraph implements Graph {
private int [][]g;//图的具体数据
private int m;//边的个数
private int n;//结点的个数
private boolean isDir;//判断是否为有向图 true为有向 false为无向
//表示图中有多少个结点 isDir表明此图是否有效
//n=5 表示结点有5个(0,1,2,3,4)
public DensGraph() {
this(0,false);
}
public DensGraph(int n,boolean isDir) {
if(n<0) {
throw new IllegalArgumentException("图中的节点个数必须大于等于0");
}
this.n=n;
this.m=0;
this.isDir=isDir;
this.g=new int[n][n];
}
@Override
public int V() {
return n;
}
@Override
public int E() {
return m;
}
@Override
public void addEdge(int v, int w) {
if(v<0||v>=n) {
throw new IllegalArgumentException(v+"结点不存在");
}
if(w<0||w>=n) {
throw new IllegalArgumentException(w+"结点不存在");
}
//不考虑平行边和自环边
if(v==w) {
return ;
}
//考虑有向或者无向
if(hasEdge(v, w)) {//如果已存在边则不加入,没有自环边
return ;
}
if(isDir) {
g[v][w]=1;
}else {
g[w][v]=1;
g[v][w]=1;
}
m++;
}
@Override
public boolean hasEdge(int v, int w) {
if(v<0||v>=n) {
throw new IllegalArgumentException(v+"结点不存在");
}
if(w<0||w>=n) {
throw new IllegalArgumentException(w+"结点不存在");
}
return g[v][w]==1;
}
@Override
public void show() {
for(int i=0;i<n;i++) {
for(int j=0;j<n;j++) {
System.out.print(g[i][j]+" ");
}
System.out.println();
}
}
@Override
public Iterable<Integer> adj(int v) {
if(v<0||v>=n) {
throw new IllegalArgumentException(v+"结点不存在");
}
Vector<Integer> adjV=new Vector<>();
for(int i=0;i<n;i++) {
if(g[v][i]==1) {
adjV.add(i);
}
}
return adjV;
}
}
1.3.2.邻接表
由于邻接矩阵在存储变数相对顶点较少的图时会造成空间浪费,所以使用邻接表–数组和链表结合的方式;
//邻接表- 稀疏图
public class SparesGraph implements Graph {
private int n;//结点个数
private int m;//边的个数
private boolean isDir;
private Vector<Integer> [] g;
public SparesGraph(int n,boolean isDir) {
if(n<0) {
throw new IllegalArgumentException("节点个数必须大于0");
}
this.n=n;
this.m=0;
this.isDir=isDir;
g=new Vector[n];
for(int i=0;i<g.length;i++) {
g[i]=new Vector<Integer>();
}
}
@Override
public int V() {
return n;
}
@Override
public int E() {
return m;
}
@Override
public void addEdge(int v, int w) {
if(v<0||v>=n) {
throw new IllegalArgumentException(v+"结点不存在");
}
if(w<0||w>=n) {
throw new IllegalArgumentException(w+"结点不存在");
}
if(v==w) {//自环边
return;
}
if(hasEdge(v, w)) {//避免平行边
return;
}
g[v].add(w);//有向图的单向边
if(!isDir) {
g[w].add(v);//无向图双向边
}
m++;
}
@Override
public boolean hasEdge(int v, int w) {
if(v<0||v>=n) {
throw new IllegalArgumentException(v+"结点不存在");
}
if(w<0||w>=n) {
throw new IllegalArgumentException(w+"结点不存在");
}
return g[v].contains(w);
}
@Override
public void show() {
for(int i=0;i<n;i++) {
System.out.print("ver "+i+" : ");
for(int j=0;j<g[i].size();j++) {
System.out.print(g[i].get(j)+" ");
//System.out.println(g[i].elementAt(j)+" ");
}
System.out.println();
}
}
@Override
public Iterable<Integer> adj(int v) {
if(v<0||v>=n) {
throw new IllegalArgumentException(v+"结点不存在");
}
return g[v];
}
}
1.4.图的遍历
1.4.1.深度优先遍历(DFS)
深度优先遍历就是一个递归的过程,它从图中的某个顶点V出发,访问此顶点,然后从V的未被访问的邻接点出发深度优先遍历,直至图中所有和V有路劲连同的顶点都被访问到,这里是对于连通图而言;对于非连通图,只需要对他的连通分量分别进行深度优先遍历,即在先前一个顶点进行一次深度优先遍历后,若图中尚有顶点未被访问,则另选图中的一个未被访问的顶点作为起始点,重复上述过程,直至所有顶点都被访问完;
1.4.1.1.邻接矩阵的深度优先
public class ComPath {
private Graph g; //创建一个图
private boolean visited[]; //状态数组 true表示已经被访问过
private int []from; //表示该结点的上一个顶点
private int s; //起点
public ComPath(Graph g,int s) {
this.g=g;
if(s<0||s>=g.V()) {
throw new IllegalArgumentException("no this node");
}
visited=new boolean[g.V()];
from=new int[g.V()];
this.s=s;
for(int i=0;i<g.V();i++) {
visited[i]=false;
from[i]=-1;
}
dfs(s);
}
private void dfs(int v) {
visited[v]=true;
for(int i : g.adj(v)) {
if(i==1&&!visited[i]) {
from[i]=v;
dfs(i);
}
}
}
}
1.4.1.2.邻接表的深度优先
public class SparePath{
private Graph g;
private boolean [] visited;
private int [] from;
private int s;
public SparePath(Graph g,int s){
this.g=g;
this.s=s;
visited=new boolean[g.V()];
from=new int[g.V()];
for(int i=0;i<g.V();i++){
from[i]=-1;
visited[i]=false;
}
dfs(s);
}
private void dfs(int v){
visited[v]=true;
for(int i: g.adj(v)){
if(!visited[i]){
from[i]=v;
dfs(i);
}
}
}
}
1.4.2.广度优先遍历(BFS)
图的广度优先遍历类似于树的层序遍历;
1.4.2.1.邻接矩阵的广度优先遍历
public class DensBFS{
private Graph g;
private int s;
private boolean [] visited;
private int [] from;
private int [] order;
public DensBFS(Graph g,int s){
this.g=g;
this.s=s;
visited=new boolean[g.V()];
from=new int[g.V()];
order=new int[g.V()];
for(int i=0;i<g.V();i++){
visited[i]=false;
from[i]=-1;
order[i]=-1;
}
Queue<Integer> queue=new LinkedList<>();
bfs(s,queue);
}
private void bfs(int v,Queue queue){
queue.add(v);
visited[v]=true;
order[v]=0;
while(!queue.isEmpty()){
int v=queue.remove();
for(int i : g.adj(v)){
if(i==1&&!visited[i]){
queue.add(i);
visited[i]=true;
from[i]=v;
order[i]=order[v]+1;
}
}
}
}
}
1.4.2.2.邻接表的广度优先遍历
public class ShortestPath {
private Graph g;
private int s;
private boolean[] visit;
private int []from;
private int [] ord;
private StringBuffer sb;
public ShortestPath(Graph g,int s) {
sb=new StringBuffer();
this.g=g;
if(s<0||s>=g.V()) {
throw new IllegalArgumentException("节点不存在");
}
visit=new boolean[g.V()];
from=new int[g.V()];
ord=new int[g.V()];
for(int i=0;i<g.V();i++) {
visit[i]=false;
from[i]=-1;
ord[i]=-1;
}
this.s=s;
Queue<Integer>queue=new LinkedList<>();
queue.add(s);
visit[s]=true;
ord[s]=0;
while(!queue.isEmpty()) {
int v=queue.remove();
sb.append(v+"->");
for(int i:g.adj(v)) {
if(!visit[i]) {
queue.add(i);
visit[i]=true;
from[i]=v;
ord[i]=ord[v]+1;
}
}
}
}