文章目录
1. 基本术语
图( Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
线性表中我们把数据元素叫元素,树中将数据元素叫结点,在图中数据元素,我们则称之为顶点(Vertex)。
无向边:若顶点
v
i
v_i
vi到
v
j
v_j
vj之间的边没有方向,则称这条边为无向边(Edge),用无序偶对
(
v
i
,
v
j
)
(v_i,v_j)
(vi,vj)来表示。如果图中任意两个顶点之间的边都是无向边,则称该图为无向图。
有向边:若从顶点
v
i
v_i
vi到
v
j
v_j
vj的边有方向,则称这条边为有向边,也称为弧(Arc ),用有序偶对
<
v
i
,
v
j
>
<v_i,v_j>
<vi,vj>来表示。
v
i
v_i
vi称为弧尾,
v
j
v_j
vj称为弧头。如果图中任意两个顶点之间的边都是有向边,则称该图为有向图。
在图中,若不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图。我们一般讨论的都是简单图。
在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。
在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。
有很少条边或弧的图称为稀疏图,反之称为稠密图。
有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的数叫做权(Weight)。这些权可以表示从一个顶点到另一个顶点的距离或耗费。这种带权的图通常称为网(Network)。
无向图中,顶点v的度(Degree)是和v相关联的边的数目。对于有向图中,又细分为入度和出度。
第一个顶点到最后一个顶点相同的路径称为回路或环(Cycle)。序列中顶点不重复出现的路径称为简单路径。除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称为简单回路或简单环。
在无向图G中,如果从顶点
v
i
v_i
vi到顶点
v
j
v_j
vj有路径,则称
v
i
v_i
vi和
v
j
v_j
vj是连通的。如果对于图中任意两个顶点
v
i
v_i
vi、
v
j
v_j
vj∈E,
v
i
v_i
vi和
v
j
v_j
vj都是连通的,则称G是连通图(ConnectedGraph)。
无向图中的极大连通子图称为连通分量。注意连通分量的概念,它强调:
要是子图;
子图要是连通的;
连通子图含有极大顶点数;
具有极大顶点数的连通子图包含依附于这些顶点的所有边。
在有向图G中,如果对于图中任意两个顶点
v
i
v_i
vi、
v
j
v_j
vj∈E、
v
i
v_i
vi不等于
v
j
v_j
vj,从
v
i
v_i
vi到
v
j
v_j
vj和从
v
j
v_j
vj到
v
i
v_i
vi都存在路径,则称G是强连通图。有向图中的极大强连通子图称做有向图的强连通分量。
一个连通图的生成树是一个极小的连通子图,它含有图中全部的n个顶点,但只有足以构成一棵树的n-1条边。
2. 图的存储结构
图不可能用简单的顺序存储结构表示。
2.1 邻接矩阵
图的邻接矩阵(Adjacency Matrix)存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。
假设有这样一个图:
邻接矩阵的实现如下:
package dataStructure.graph.adjMatrix;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.Scanner;
public class GraphAdjMatrix<Type1,Type2 extends Comparable<? super Type2>> {
private static final int MAXVEX=15; // 最大顶点数
private static final int INF=65535; // 无穷大
private Type1[] vexs; // 顶点表
private Type2[][] adjM; // 邻接矩阵
private int numVertexs,numEdges; // 图中当前的顶点数和边数
// 构造函数
GraphAdjMatrix() throws IOException {
vexs= (Type1[]) new Object[ MAXVEX ];
adjM=(Type2[][]) new Comparable[ MAXVEX ][ MAXVEX ];
createAdjMatrix();
}
// 创建一个图(空图是非法的,图的顶点数一定是有穷非空的)
public void createAdjMatrix() throws IOException {
String path="E:\\project_file\\IntelliJ_IDEA\\in.txt";
// 读取输入流
Scanner in =new Scanner(Paths.get(path),"UTF-8");
numVertexs=in.nextInt();
numEdges=in.nextInt();
int sym=in.nextInt();
for(int i=0;i<numVertexs;i++){
vexs[i]=(Type1) in.next();
}
for(int i=0;i<numVertexs;i++){
for(int j=0;j<numVertexs;j++){
if(i!=j) adjM[i][j]=(Type2)(Integer) INF;
else adjM[i][j]=(Type2)(Integer) 0;
}
}
int i,j;
for(int k=0;k<numEdges;k++){
i=in.nextInt();
j=in.nextInt();
adjM[i][j]=(Type2) in.next();
if(sym==0){
adjM[j][i]=adjM[i][j];
}
}
}
public static void main(String[] args)throws IOException{
GraphAdjMatrix<String,Integer> g=new GraphAdjMatrix<>();
System.out.println("yes");
}
}
其中输入流是在一个文件中读取,文件中内容如下:
5 6 0
v0 v1 v2 v3 v4
0 4 6
1 0 9
1 2 3
2 0 2
2 3 5
3 4 1
2.2 邻接表
邻接表用顶点表和边表结点来存储所有顶点和边。顶点表存储每个顶点的数据和这个顶点的边表表头指针(这个边表中存放着该顶点的所有邻接顶点和权值)。边表结点存储当前顶点的下标、权值和下一个邻接点指针。
邻接表的实现如下:
package dataStructure.graph.adjList;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.Scanner;
class EdgeNode<Type2 extends Comparable<? super Type2>>{ // 边表节点
private int adjVex; // 邻接点域,存储该顶点对应的下标
private Type2 weight; // 用于存储权值,对于非网图可以不需要
private EdgeNode<Type2> next; // 下一个邻接点
EdgeNode(int adjVex,Type2 weight,EdgeNode<Type2> next){
this.adjVex=adjVex;
this.weight=weight;
this.next=next;
}
public int getAdjVex(){return adjVex; }
public Type2 getWeight(){return weight;}
public EdgeNode<Type2> getNext(){return next;}
}
class VertexNode<Type1,Type2 extends Comparable<? super Type2>>{ // 顶点表节点
private Type1 data; // 当前顶点的数据域
private EdgeNode<Type2> firstEdge; // 边表头指针
private int indegree; // 存储该顶点的入度
VertexNode(Type1 data,EdgeNode<Type2> firstEdge){
this.data=data;
this.firstEdge=firstEdge;
this.indegree=0; // 入度初始化为0
}
public Type1 getData(){ return data; }
public EdgeNode<Type2> getFirstEdge(){ return firstEdge; }
public int getIndegree(){return indegree;}
public void changeIndegree(int change){indegree+=change;}
public void setFirstEdge(EdgeNode<Type2> firstEdge){ this.firstEdge=firstEdge; }
}
public class GraphAdjList<Type1,Type2 extends Comparable<? super Type2>> {
private static final int MAXVEX=15; // 最大顶点数
private VertexNode<Type1,Type2>[] adjList; // 顶点表
private int numVertexs,numEdges; // 图中当前的顶点数和边数
// 构造函数
GraphAdjList() throws IOException {
adjList=new VertexNode[MAXVEX];
createAdjList();
loadIndegree();
}
public int getNumVertexs(){
return numVertexs;
}
public VertexNode<Type1,Type2>[] getAdjList(){return adjList;}
// 创建一个图(空图是非法的,图的顶点数一定是有穷非空的)
public void createAdjList() throws IOException {
String path="E:\\project_file\\IntelliJ_IDEA\\Hello\\src\\dataStructure\\in.txt";
// 读取输入流
Scanner in =new Scanner(Paths.get(path),"UTF-8");
numVertexs=in.nextInt();
numEdges=in.nextInt();
int sym=in.nextInt();
for(int i=0;i<numVertexs;i++){
adjList[i]=new VertexNode((Type1) in.next(),null);
}
int i,j;
Type2 wei;
for(int k=0;k<numEdges;k++){
i=in.nextInt();
j=in.nextInt();
// 这句并没有将权值转化为integer类型,weight中存储的是String,也就是说强制类型转换并没有起作用;
// 但奇怪的是,该句也没有报错,可以正常运行,并且weight可以正常存储String,即便泛型Type2传进来的是Integer
wei=(Type2) in.next();
EdgeNode<Type2> e=new EdgeNode<>(j,wei,adjList[i].getFirstEdge());
adjList[i].setFirstEdge(e);
if(sym==0){
e=new EdgeNode<>(i,wei,adjList[j].getFirstEdge());
adjList[j].setFirstEdge(e);
}
}
}
// 根据图 加载入度
private void loadIndegree(){
for(int i=0;i<numVertexs;i++){
EdgeNode e=adjList[i].getFirstEdge();
while (e!=null){
adjList[e.getAdjVex()].changeIndegree(1);
e=e.getNext();
}
}
}
public static void main(String[] args)throws IOException{
GraphAdjList<String,Integer> g=new GraphAdjList<>();
System.out.println("yes");
}
}
2.3 边集数组
边集数组是由两个一维数组构成。一个是存储顶点的信息;另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标(begin)、终点下标(end)和权(weight)组成。显然边集数组关注的是边的集合,在边集数组中要查找一个顶点的度需要扫描整个边数组,效率并不高。因此它更适合对边依次进行处理的操作,而不适合对顶点相关的操作。
package dataStructure.graph.edgeSet;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.Scanner;
class Edge<Type2 extends Comparable<? super Type2>>{
private int begin;
private int end;
private Type2 weight;
Edge(int begin,int end,Type2 weight){
this.begin=begin;
this.end=end;
this.weight=weight;
}
public int getBegin(){return begin;}
public int getEnd(){return end;}
public Type2 getWeight(){return weight;}
}
// 边集数组表示法
public class GraphEdgeSet<Type1,Type2 extends Comparable<? super Type2>> {
private static final int MAXVEX=15; // 最大顶点数
private static final int MAXEDGE=50; // 最大边数
private static final int INF=65535; // 无穷大
private Type1[] vexs; // 顶点表
private Edge<Type2>[] edgeSet; // 边集数组
private int numVertexs,numEdges; // 图中当前的顶点数和边数
private int sym; // 标志位,0为无向图,否则为有向图
GraphEdgeSet()throws IOException{
vexs= (Type1[]) new Object[ MAXVEX ];
edgeSet=new Edge[MAXEDGE];
createEdgeSet();
}
public int getNumVertexs(){return numVertexs;}
public int getNumEdges(){
if(sym==0)
return 2*numEdges; // 如果是无向图,每条边交换起点和终点后再读入,共读入两次
return numEdges;
}
public Edge<Type2>[] getEdgeSet(){return edgeSet;}
// 创建一个图(空图是非法的,图的顶点数一定是有穷非空的)
public void createEdgeSet() throws IOException {
String path="E:\\project_file\\IntelliJ_IDEA\\in.txt";
// 读取输入流
Scanner in =new Scanner(Paths.get(path),"UTF-8");
numVertexs=in.nextInt();
numEdges=in.nextInt();
sym=in.nextInt();
for(int i=0;i<numVertexs;i++){
vexs[i]=(Type1) in.next();
}
int be,en;
Type2 wei;
for(int i=0;i<getNumEdges();){
be=in.nextInt();
en=in.nextInt();
wei=(Type2) in.next();
edgeSet[i++]=new Edge<>(be,en,wei);
if(sym==0){
edgeSet[i++]=new Edge<>(en,be,wei);
}
}
}
public void printEdges(){
for(int i=0;i<getNumEdges();i++){
System.out.println(edgeSet[i].getBegin()+"--"+edgeSet[i].getEnd()+",weight="+edgeSet[i].getWeight());
}
}
// 排序统一接口
public void sort(){
// sortWeight(0,getNumEdges()-1);
insertSort(0,getNumEdges()-1);
}
// 插入排序
private void insertSort(int left,int right){
int tmp;
int i;
for(int p=left+1;p<=right;p++){
tmp=p;
for(i=p;i>left && Integer.parseInt((String)edgeSet[i-1].getWeight())>
Integer.parseInt((String)edgeSet[tmp].getWeight());i--){
edgeSet[i]=edgeSet[i-1];
}
edgeSet[i]=edgeSet[tmp];
}
}
// 快速排序
private void sortWeight(int left,int right){
if (left>=right) return;
int low,high;
Type2 pivot; // 主元
pivot=median(left,right);
if(right-left<3) return;
low=left;
high=right-1;
while (true){
while(Integer.parseInt((String)edgeSet[++low].getWeight())<Integer.parseInt((String)pivot))
if(low>=right-2)break;
while(Integer.parseInt((String)edgeSet[--high].getWeight())>Integer.parseInt((String)pivot))
if(high<=left+1)break;
if(low<high)
swap(low,high);
else
break;
}
swap(low,right-1);
sortWeight(left,low-1);
sortWeight(low+1,right);
}
// 快排选主元
private Type2 median(int left,int right){
int middle=(left+right)/2;
if(Integer.parseInt((String) edgeSet[left].getWeight())>Integer.parseInt((String) edgeSet[middle].getWeight()))
swap(left,middle);
if(Integer.parseInt((String) edgeSet[left].getWeight())>Integer.parseInt((String) edgeSet[right].getWeight()))
swap(left,right);
if(Integer.parseInt((String) edgeSet[middle].getWeight())>Integer.parseInt((String) edgeSet[right].getWeight()))
swap(middle,right);
swap(middle,right-1);
return edgeSet[right-1].getWeight();
}
private void swap(int i,int j){
if(i!=j) {
Edge<Type2> temp;
temp = edgeSet[i];
edgeSet[i] = edgeSet[j];
edgeSet[j] = temp;
}
}
public static void main(String[] args)throws IOException{
GraphEdgeSet<String,Integer> g=new GraphEdgeSet<>();
g.printEdges();
g.sort();
System.out.println("------------------------------");
g.printEdges();
System.out.println("yes:");
}
}
3. 图的遍历
图的遍历是和树的遍历类似,我们希望从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫做图的遍历(Traversing Graph)。
3.1 深度优先搜索
深度优先遍历(Depth_First_Search),也有称为深度优先搜索,简称为 DFS。
对于邻接表方式构造的图,进行 深度优先搜索:
package dataStructure.graph.adjList;
import java.io.IOException;
public class DfsAdjList<Type1,Type2 extends Comparable<? super Type2>> {
private GraphAdjList<Type1,Type2> g;
boolean[] visited; // 用以存储每个顶点是否被访问过
DfsAdjList(GraphAdjList<Type1,Type2> g){
this.g=g;
visited=new boolean[g.getNumVertexs()];
initVisited();
}
// 初始化访问状态数组,都置为false,即没有被访问过
private void initVisited(){
for(int i=0;i<visited.length;i++){
visited[i]=false;
}
}
// 对整个图进行深度优先搜索
public void DFSTraverse(){
initVisited();
for(int i=0;i<g.getNumVertexs();i++){
if(!visited[i]) DFS(i);
}
}
// 从顶点i开始,进行深度优先搜索,只能搜索到所有连通的顶点
private void DFS(int i){
visited[i]=true;
System.out.println(g.getAdjList()[i].getData());
EdgeNode<Type2> p=g.getAdjList()[i].getFirstEdge();
while(p!=null){
if(!visited[p.getAdjVex()]) DFS(p.getAdjVex());
p=p.getNext();
}
}
public static void main(String [] args) throws IOException {
GraphAdjList<String,Integer> g=new GraphAdjList<>();
DfsAdjList<String,Integer> dfs=new DfsAdjList<>(g);
dfs.DFSTraverse();
}
}
3.2 广度优先搜索
广度优先遍历(Breadth_First_Search),又称为广度优先搜索,简称 BFS。
对于邻接表方式构造的图,进行广度优先搜索:
package dataStructure.graph.adjList;
import dataStructure.list.MyArrayQueue;
import java.io.IOException;
public class BfsAdjList<Type1,Type2 extends Comparable<? super Type2>> {
private GraphAdjList<Type1,Type2> g;
boolean[] visited; // 用以存储每个顶点是否被访问过
BfsAdjList(GraphAdjList<Type1,Type2> g){
this.g=g;
visited=new boolean[g.getNumVertexs()];
initVisited();
}
// 初始化访问状态数组,都置为false,即没有被访问过
private void initVisited(){
for(int i=0;i<visited.length;i++){
visited[i]=false;
}
}
// 对整个图进行广度优先搜索
public void BFSTraverse(){
initVisited();
for(int i=0;i<g.getNumVertexs();i++){
if(!visited[i]){
BFS(i);
}
}
}
// 从顶点i开始,进行广度优先搜索
private void BFS(int i){
MyArrayQueue<Integer> Q=new MyArrayQueue<>();
EdgeNode<Type2> p;
visited[i]=true;
System.out.println(g.getAdjList()[i].getData());
Q.enQueue(i); // 对当前顶点入队
while(!Q.isEmpty()){
// 取出队头顶点,指向该顶点的边表表头
p=g.getAdjList()[Q.deQueue()].getFirstEdge();
// 循环遍历该顶点的整个边表
while(p!=null){
if(!visited[p.getAdjVex()]){
visited[p.getAdjVex()]=true;
System.out.println(g.getAdjList()[p.getAdjVex()].getData());
// 对该顶点执行完输出操作后,将该顶点入队
Q.enQueue(p.getAdjVex());
}
p=p.getNext();
}
}
}
public static void main(String [] args) throws IOException {
GraphAdjList<String,Integer> g=new GraphAdjList<>();
BfsAdjList<String,Integer> bfs=new BfsAdjList<>(g);
bfs.BFSTraverse();
}
}
上述代码中,我使用的是自己定义的队列MyArrayQueue,代码在这里。
注:如果图顶点和边非常多,不能在短时间内遍历完成,遍历的目的是为了寻找合适的顶点,那么选择哪种遍历就要仔细斟酌了。深度优先更适合目标比较明确,以找到目标为主要目的的情况,而广度优先更适合在不断扩大遍历范围时找到相对最优解的情况。
4. 最小生成树
把构造连通网的最小代价生成树称为最小生成树。找连通网的最小生成树,经典的有两种算法,普里姆算法和克鲁斯卡尔算法。
最小生成树的算法用一个更复杂的图来测试。
in.txt中的输入内容修改如下:
9 15 0
v0 v1 v2 v3 v4 v5 v6 v7 v8
4 7 7
2 8 8
0 1 10
0 5 11
1 8 12
3 7 16
1 6 16
5 6 17
1 2 18
6 7 19
3 4 20
3 8 21
2 3 22
3 6 24
4 5 26
4.1普里姆(Prim)算法
普里姆(Prim)算法就是选择一个顶点开始,每一步都选择与现有顶点相关联的最小权值的一个边,如此不断地将新顶点加入最小生成树中。
package dataStructure.graph.adjList;
import java.io.IOException;
public class MiniSpanTree_Prim<Type1,Type2 extends Comparable<? super Type2>> {
private static final int INF=65535; // 无穷大
private GraphAdjList<Type1,Type2> g;
private boolean[] inTree; // 存储已经加入最小生成树的顶点
MiniSpanTree_Prim(GraphAdjList<Type1,Type2> g){
this.g=g;
inTree=new boolean[g.getNumVertexs()];
initInTree();
}
private void initInTree(){
for(int i=0;i<inTree.length;i++){
inTree[i]=false;
}
}
// 最小生成树之Prim算法
public void spanTreePrim(int index){
inTree[index]=true;
int min;
int minBegin=0,minEnd=0;
EdgeNode<Type2> p;
for(int i=1;i<g.getNumVertexs();i++){
min=INF;
// 找到最小权值边
for(int j=0;j<inTree.length;j++){
if(inTree[j]){
p=g.getAdjList()[j].getFirstEdge();
while(p!=null){
// 字符串转整型
int temp=Integer.parseInt((String) p.getWeight());
if(!inTree[p.getAdjVex()]&&temp<min){
min=temp;
minBegin=j;
minEnd=p.getAdjVex();
}
p=p.getNext();
}
}
}
inTree[minEnd]=true;
System.out.println(minBegin+"--"+minEnd+",weight="+min);
}
}
public static void main(String[] args)throws IOException {
GraphAdjList<String,Integer> g=new GraphAdjList<>();
MiniSpanTree_Prim<String,Integer> prim=new MiniSpanTree_Prim<>(g);
prim.spanTreePrim(0);
}
}
输出如下:
0--1,weight=10
0--5,weight=11
1--8,weight=12
8--2,weight=8
1--6,weight=16
6--7,weight=19
7--4,weight=7
7--3,weight=16
4.2克鲁斯卡尔(Kruskal)算法
克鲁斯卡尔(Kruskal)算法就是每一步都选择最小权值的边,所以图用边集数据存储最为合适。如果下一个最小权值边会使当前的最小生成树出现环路,则舍弃掉该边,选择下一个最小权值边。
package dataStructure.graph.edgeSet;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
public class MiniSpanTree_Kruskal<Type1,Type2 extends Comparable<? super Type2>> {
private static final int INF=65535; // 无穷大
private GraphEdgeSet<Type1,Type2> g;
private boolean[] inTree; // 存储已经加入最小生成树的顶点,true为已经加入
private ArrayList<HashSet<Integer>> subGraph; // 存储已经放入最小生成树的连通子图的顶点
MiniSpanTree_Kruskal(GraphEdgeSet<Type1,Type2> g){
this.g=g;
inTree=new boolean[g.getNumVertexs()];
subGraph=new ArrayList<>();
initInTree();
}
private void initInTree(){
for(int i=0;i<inTree.length;i++){
inTree[i]=false;
}
}
// 判断新加入的边是否成环
private boolean isLoop(Edge e){
int begin=e.getBegin();
int end=e.getEnd();
for(int i=0;i<subGraph.size();i++){
HashSet<Integer> s=subGraph.get(i);
if(s.contains(begin) && s.contains(end)) // 成环
return true;
}
return false;
}
// 最小生成树之Kruskal算法
public void spanTreeKruskal(){
g.sort();// 先按权值进行排序
for(int i=0;i<g.getNumEdges();i++){
Edge e=g.getEdgeSet()[i];
// 如果新边没有形成环
if(!isLoop(e)){
putEdge(e);
}
}
}
// 将一条边放入最小生成树
private void putEdge(Edge e){
int begin=e.getBegin();
int end=e.getEnd();
Type2 weight= (Type2) e.getWeight();
// 两个顶点都还没有加入生成树,这时会形成新的连通子图
if(!inTree[begin] && !inTree[end]){
inTree[begin]=true;
inTree[end]=true;
// 构建一个新的连通子图的顶点集
HashSet<Integer> set=new HashSet<>();
set.add(begin);
set.add(end);
subGraph.add(set);
}
// 两个顶点都在生成树中,这时会发生连通子图的合并。是否成环已在上层函数中判断过
else if(inTree[begin] && inTree[end]){
int bi=0,ei=0;
for (int i=0;i<subGraph.size();i++){
if(subGraph.get(i).contains(begin)) bi=i;
if(subGraph.get(i).contains(end)) ei=i;
}
HashSet<Integer> s=new HashSet<>();
s.addAll(subGraph.get(bi));
s.addAll(subGraph.get(ei));
subGraph.set(bi,s);
subGraph.remove(ei);
}
// 只有一个顶点都在生成树中;连通子图的个数不变,但需加入新顶点
else {
int i;
// end未在生成树中
if(inTree[begin]){
inTree[end] = true;
for (i=0;i<subGraph.size();i++){
if(subGraph.get(i).contains(begin)) break;
}
HashSet<Integer> s=new HashSet<>();
s.add(end); // 将end加入生成树
s.addAll(subGraph.get(i));
subGraph.set(i,s);
}
// begin未在生成树中
else{
inTree[begin] = true;
for (i=0;i<subGraph.size();i++){
if(subGraph.get(i).contains(end)) break;
}
HashSet<Integer> s=new HashSet<>();
s.add(begin); // 将end加入生成树
s.addAll(subGraph.get(i));
subGraph.set(i,s);
}
}
System.out.println(begin+"--"+end+",weight="+weight);
}
public static void main(String [] args)throws IOException {
GraphEdgeSet<String,Integer> g=new GraphEdgeSet<>();
MiniSpanTree_Kruskal<String,Integer> kruskal=new MiniSpanTree_Kruskal<>(g);
kruskal.spanTreeKruskal();
}
}
输出如下:
4--7,weight=7
2--8,weight=8
0--1,weight=10
0--5,weight=11
1--8,weight=12
3--7,weight=16
1--6,weight=16
6--7,weight=19
5. 最短路径
5.1迪杰斯特拉(Dijkstra)算法:
1)先选取一个顶点作为起点,将该顶点的最短路径dis置为0。(起始状态下所有顶点未知最短路径(unknown),最短路径dis为INF)
2)将所有顶点(实例域包含每个顶点的当前最短路径)加入优先队列
3)从优先队列中取最小元,并将该最小元标记为known(已知最短路径)。遍历该最小元的所有邻接顶点,更新所有邻接顶点的dis(如果dis变小的话)。
4)根据新的dis重置优先队列。回到3),直到优先队列为空,退出循环。
package dataStructure.graph.adjList;
import dataStructure.heap.BinaryHeap;
import java.io.IOException;
// 保存每个顶点的索引和当前距离,封装为一个类
class Dis implements Comparable {
private int i;
private Float d;
Dis(int i,float d){
this.i=i;
this.d=d;
}
public int getI(){return i;}
public Float getD(){return d;}
public void setD(float d){this.d=d;}
@Override
public int compareTo(Object o) {
Dis di=(Dis) o;
Float cha=this.d-di.d;
if(Math.abs(cha)<1e-6)
return 0;
else if(cha<0)
return -1;
else
return 1;
}
}
public class Dijkstra<type> {
private static final int INF=65535; // 无穷大
private GraphAdjList<type> g;
private boolean[] known; // 若已经知道最短路径,则置为true
private Dis[] dis; // 存储每个顶点的最短路径
private int[] path; // 存储最短路径中该顶点的上一个顶点
Dijkstra(GraphAdjList<type> g){
this.g=g;
initKnown();
initDis();
initPath();
}
private void initDis(){
// dis默认初始化为无穷大
dis=new Dis[g.getNumVertexs()];
for(int i=0;i<dis.length;i++){
dis[i]=new Dis(i,INF);
}
}
private void initPath(){
path=new int[g.getNumVertexs()];
}
private void initKnown(){
// known默认初始化为false
known=new boolean[g.getNumVertexs()];
}
public void dijkstra(int k){
dis[k].setD(0); // 将起点的dis置为0
path[k]=-1; // 起点无前趋顶点,索引path置为-1,意为终止
var heap=new BinaryHeap<Dis>(dis); // 根据dis初始化优先队列
var heap0=new BinaryHeap<Dis>(); // 建一个空的优先队列
// 遍历优先队列中所有元素
while (!heap.isEmpty()){
// 根据优先级取元素,每次取出dis最小的顶点
Dis d=heap.deleteMin();
known[d.getI()]=true; // 该顶点的最短路径设为已知
EdgeNode e=g.getAdjList()[d.getI()].getFirstEdge(); // 当前顶点的边表
// 遍历当前顶点的边表
while (e!=null){
// 如果该邻接顶点还没有确定最短路径
if(!known[e.getAdjVex()]){
float temp=e.getWeight(); // 该邻接点与头顶点之间的权值
// 如果发现了更短路径
if(d.getD()+temp<dis[e.getAdjVex()].getD()){
// 对该邻接顶点找到一个更短路径,此时需要更新优先队列
// 将优先队列heap的每个元素取出,插入heap0
while (!heap.isEmpty()) {
Dis d0 = heap.deleteMin();
// 如果当前元素是要修改的元素,重置元素的dis
if (d0.getI() == e.getAdjVex()) {
d0.setD(d.getD() + temp);
}
heap0.insert(d0);
}
var h=heap;
heap=heap0;
heap0=h;
path[e.getAdjVex()]=d.getI();// 将所更改最短路径的顶点 的前趋顶点 重新设置
}
}
e=e.getNext();
}
}
}
public void printAll(){
for(int i=0;i<g.getNumVertexs();i++){
System.out.print(g.getAdjList()[i].getData()+": ");
printPath(i);
System.out.print("\n");
}
}
public void printPath(int k){
if(path[k]!=-1){
printPath(path[k]);
System.out.print(" to ");
}
System.out.print(g.getAdjList()[k].getData());
}
public static void main(String[] args)throws IOException {
GraphAdjList<String> g=new GraphAdjList<>();
Dijkstra<String> dij=new Dijkstra<>(g);
dij.dijkstra(0);
dij.printAll();
}
}
输出结果:
v0: v0
v1: v0 to v1
v2: v0 to v1 to v2
v3: v0 to v1 to v8 to v3
v4: v0 to v5 to v4
v5: v0 to v5
v6: v0 to v1 to v6
v7: v0 to v5 to v4 to v7
v8: v0 to v1 to v8
6. 拓扑排序
有向无圈图的拓扑排序:
package dataStructure.graph.adjList;
import dataStructure.list.MyArrayQueue;
import java.io.IOException;
public class Topological<Type1,Type2 extends Comparable<? super Type2>> {
private GraphAdjList<Type1,Type2> g;
private int[] order; // 拓扑排序
private int[] indegree; // 存储每个顶点的入度
Topological(GraphAdjList<Type1,Type2> g){
this.g=g;
order=new int[g.getNumVertexs()];
initIndegree();
sortTopological(); // 执行拓扑排序,结果存储在order中
}
// 初始化每个顶点的入度
private void initIndegree(){
indegree=new int[g.getNumVertexs()];
for(int i=0;i<indegree.length;i++){
indegree[i]=g.getAdjList()[i].getIndegree();
}
}
// 拓扑排序
public void sortTopological(){
int k=0;
MyArrayQueue<Integer> q=new MyArrayQueue<>();
// 遍历每一个顶点,如果该顶点入度为0,则入队
for(int i=0;i<g.getNumVertexs();i++){
if(indegree[i]==0) q.enQueue(i);
}
// 当队列非空时
while (!q.isEmpty()){
int v=q.deQueue();
order[k++]=v;
EdgeNode e=g.getAdjList()[v].getFirstEdge();
// 对于刚加入拓扑排序的顶点,遍历其每个邻接顶点
while (e!=null){
// 如果邻接顶点入度为0,则入队
if(--indegree[e.getAdjVex()]==0) q.enQueue(e.getAdjVex());
e=e.getNext();
}
}
// 图中存在环
if(k!=g.getNumVertexs()){
System.out.println("there is a cycle!");
}
}
// 打印拓扑排序
public void outOrder(){
for(int i:order){
System.out.println(g.getAdjList()[i].getData());
}
}
public static void main(String[] args)throws IOException {
GraphAdjList<String,Integer> g=new GraphAdjList<>();
Topological<String,Integer> top=new Topological<>(g);
top.outOrder();
}
}
用下面的图进行测试
输出如下
v1
v2
v3
v0
v4
7.关键路径
(未完待续~)