【数据结构】图

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.关键路径

(未完待续~)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冷冰殇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值