【数据结构】手撕图的相关数据结构与算法

一、定义Graph抽象类

/**
 * Graph抽象类
 */
abstract public class Graph {
    // 枚举图的种类
    public enum Kind {
        DG,  //有向图
        DN,  //有向网
        UDG, //无向图
        UDN  //无向网
    }

    // 用65535表示无穷大
    public static int INFINITY = 65535;

    protected Kind kind;       // 图的种类
    protected int numVertexes; // 图的顶点数
    protected int numEdges;    // 图的边数
}

二、图的常见算法

2.1 邻接矩阵的构建

// 邻接矩阵
private int[][] matrix;

// 构造方法:实现有向图、有向网、无向图和无向网的创建
public MGraph(Graph.Kind kind) {
    this.kind = kind;
    Scanner in = new Scanner(System.in);
    System.out.println("请输入图的顶点数和边数:");
    numVertexes = in.nextInt();
    numEdges = in.nextInt();

    // 初始化邻接矩阵
    matrix = new int[numVertexes + 1][numVertexes + 1];
    for (int i = 0; i <= numVertexes; i++)
        for (int j = 0; j <= numVertexes; j++) {
            matrix[i][j] = INFINITY;
        }

    switch (kind) {
        // 创建有向图的邻接矩阵
        case DG:
            System.out.println("请输入有向图的边<vi,vj>");
            for (int i = 0; i < numEdges; i++) {
                int vi = in.nextInt();
                int vj = in.nextInt();
                matrix[vi][vj] = 1;
            }
            return;
        // 创建有向网的邻接矩阵
        case DN:
            System.out.println("请输入有向网的边<vi,vj>和权重w:");
            for (int i = 0; i < numEdges; i++) {
                int vi = in.nextInt();
                int vj = in.nextInt();
                matrix[vi][vj] = in.nextInt();
            }
            return;
        // 创建无向图的邻接矩阵
        case UDG:
            System.out.println("请输入无向图的边(vi,vj):");
            for (int i = 0; i < numEdges; i++) {
                int vi = in.nextInt();
                int vj = in.nextInt();
                matrix[vi][vj] = matrix[vj][vi] = 1;
            }
            return;
        // 创建无向网的邻接矩阵
        case UDN:
            System.out.println("请输入无向网的边(vi,vj)和权重w:");
            for (int i = 0; i < numEdges; i++) {
                int vi = in.nextInt();
                int vj = in.nextInt();
                matrix[vi][vj] = matrix[vj][vi] = in.nextInt();
            }
    }
}

2.2 图的深度优先遍历算法

// 深度优先遍历外部API
public void DFS() {
    System.out.println("深度优先遍历的结果为:");
    boolean[] visited = new boolean[numVertexes + 1]; // 设置访问数组
    visited[0] = true;

    for (int i = 1; i <= numVertexes; i++) {
        if (!visited[i])
            DFSTraverse(visited, i);
    }
    System.out.println();
}

// 递归实现部分
private void DFSTraverse(boolean[] visited, int v) {
    if (visited[v]) return; // 基线条件:该结点已访问则直接return

    visited[v] = true;
    System.out.print(v + " ");
    for (int i = 1; i <= numVertexes; i++) {
        if (!visited[i] || matrix[v][i] != INFINITY) // 递归条件
            DFSTraverse(visited, i);
    }
}

2.3 图的广度优先遍历算法

// 广度优先遍历
public void BFS() {
    System.out.println("广度优先遍历的结果为:");
    boolean[] visited = new boolean[numVertexes + 1];
    visited[0] = true;

    // 使用队列暂存结点的访问顺序
    Queue<Integer> queue = new LinkedList<>();

    for (int i = 1; i <= numVertexes; i++)
        if (!visited[i]) {                     // 找到尚未未访问的结点
            visited[i] = true;
            queue.add(i);                      // 将该结点入队列
            while (!queue.isEmpty()) {
                int next = queue.remove();     // 取出队首结点
                System.out.print(next + " ");

                // 将该结点的所有相邻且未访问的结点入队列,并更改结点的访问属性为true
                for (int j = 1; j <= numVertexes; j++) {
                    if (!visited[j] && matrix[next][j] != INFINITY) {
                        visited[j] = true;
                        queue.add(j);
                    }
                }
            }
        }
    System.out.println();
}

三、图的应用

3.1 求最小生成树(Prim算法和Kruskal算法)

// Prim最小生成树算法
public void Prim() {
    // 有向图最小生成树不一定存在,因此碰到有向图或有向网就不用求最小生成树
    if (kind == Kind.DG || kind == Kind.DN) return;

    int[] adjvex = new int[numVertexes + 1];//保存相关顶点
    int[] weight = new int[numVertexes + 1];//保存相关顶点间边的权值

    System.out.println("Prim算法计算出的最小生成树为:");
    for (int i = 2; i <= numVertexes; i++) {
        weight[i] = matrix[1][i];
    }
    adjvex[2] = 1;

    for (int i = 2; i <= numVertexes; i++) {
        int min = INFINITY;
        int j, k = j = 1;
        while (j <= numVertexes) {
            if (weight[j] != 0 && weight[j] < min) {
                min = weight[j];
                k = j;
            }
            j++;
        }
        System.out.print("(" + adjvex[k] + ", " + k + "), ");//打印当前顶点边中权值最小的边
        weight[k] = 0;// 将当前顶点权值设为零,表示此顶点已完成任务
        for (j = 2; j <= numVertexes; j++) {
            if (weight[j] != 0 && matrix[k][j] < weight[j]) { //若下标为k顶点各边权值小于此前这些顶点未被加入生成树的权值
                weight[j] = matrix[k][j]; //将较小权值存入lowcost
                adjvex[j] = k;//将下标为k的顶点存入adjvex
            }
        }
    }
    System.out.println("\b\b");
}
/**
 * Kruskal算法使用边集数组的数据结构来描述图
 */
public class ERGraph extends Graph {
    // Edge内部类用于构建边集数组
    private static class Edge {
        public int begin;      // 边的起点
        public int end;        // 边的终点
        public int weight;     // 边的权值
    }

    // 定义边集数组
    private Edge[] edges = null;

    public ERGraph(Kind kind) {
        Scanner in = new Scanner(System.in);
        System.out.println("请输入图的顶点数和边数:");
        numVertexes = in.nextInt();
        numEdges = in.nextInt();

        int vi, vj;
        switch (kind) {
            // 创建有向图的边集数组
            case DG:
                edges = new Edge[numEdges];
                System.out.println("请输入有向图的边<vi,vj>");
                for (int i = 0; i < numEdges; i++) {
                    vi = in.nextInt();
                    vj = in.nextInt();
                    edges[i] = new Edge();
                    edges[i].begin = vi;
                    edges[i].end = vj;
                    edges[i].weight = 1;                      //图的所有边权重都为1
                }
                return;
            // 创建有向网的边集数组
            case DN:
                edges = new Edge[numEdges];
                System.out.println("请输入有向网的边<vi,vj>和权重w:");
                for (int i = 0; i < numEdges; i++) {
                    vi = in.nextInt();
                    vj = in.nextInt();
                    edges[i] = new Edge();
                    edges[i].begin = vi;
                    edges[i].end = vj;
                    edges[i].weight = in.nextInt();
                }
                return;
            // 创建无向图的边集数组
            case UDG:
                edges = new Edge[numEdges * 2];
                for (int i = 0; i < numEdges * 2; i++)
                    edges[i] = new Edge();
                System.out.println("请输入无向图的边(vi,vj):");
                for (int i = 0; i < numEdges; i++) {
                    vi = in.nextInt();
                    vj = in.nextInt();
                    edges[i].begin = edges[i + numEdges].end = vi;
                    edges[i].end = edges[i + numEdges].begin = vj;
                    edges[i].weight = edges[i+numEdges].weight = 1;
                }
                return;
            // 创建无向网的边集数组
            case UDN:
                edges = new Edge[numEdges * 2];
                for (int i = 0; i < numEdges * 2; i++)
                    edges[i] = new Edge();
                System.out.println("请输入无向网的边(vi,vj)和权重w:");
                for (int i = 0; i < numEdges; i++) {
                    vi = in.nextInt();
                    vj = in.nextInt();
                    edges[i].begin = edges[i + numEdges].end = vi;
                    edges[i].end = edges[i + numEdges].begin = vj;
                    edges[i].weight = edges[i+numEdges].weight = in.nextInt();
                }
        }
    }

    // Kruskal最小生成树算法
    public void Kruskal() {
        int[] parent = new int[numVertexes + 1];//定义数组用来判断与边是否形成环路

        Arrays.sort(edges, (a, b) -> a.weight - b.weight);// 对边集数组按照权重排序

        System.out.println("Kruskal算法计算出的最小生成树为:");
        // 遍历边集数组
        for (int i = 0; i < numEdges; i++) {
            int n = find(parent, edges[i].begin);
            int m = find(parent, edges[i].end);
            if (n != m) { // 如果n与m不等,说明此边没有与现有生成树形成环路
                parent[n] = m;
                System.out.print("(" + edges[i].begin + ", " + edges[i].end + "), ");
            }
        }
        System.out.println("\b\b");
    }

    // 用于查找连线顶点的尾部下标
    private int find(int[] parent, int f) {
        while (parent[f] > 0)
            f = parent[f];
        return f;
    }
}

3.2 求最短路径(Dijkstra算法和Floyd算法)

// Dijkstra最短路径算法
public void Dijkstra(int v) {
    // 创建三个辅助数组
    int[] weight = new int[numVertexes + 1];                //用来存放v到其他顶点的权值
    int[] previous = new int[numVertexes + 1];              //用来存放其他顶点相对v的最短前驱顶点
    boolean[] visited = new boolean[numVertexes + 1];       //访问标记数组,避免顶点的重复访问

    // 初始化工作
    for (int i = 1; i <= numVertexes; i++) {
        weight[i] = matrix[v][i];                           // 初始化v与其他顶点之间的权值
        if (weight[i] == INFINITY) previous[i] = INFINITY;  // i与v不相邻,前驱设为无穷大
        else previous[i] = v;                               // 说明i与v相邻,因此设i的前驱为v
    }
    weight[v] = 0;                                          // v到自身的距离为0
    visited[v] = true;                                      // weight初始化的时候访问过v

    // 算法开始
    for (int i = 1; i <= numVertexes; ++i) {
        int minWeight = INFINITY;                           // 暂存相邻顶点中权值最小的顶点的距离
        int minVex = v;                                     // 暂存相邻顶点中权值最小的顶点

        // 找到距离v权值最小的顶点
        for (int j = 1; j <= numVertexes; j++)
            if (!visited[j] && weight[j] < minWeight) {
                minVex = j;
                minWeight = weight[j];
            }

        visited[minVex] = true;                             // 访问完毕,已读取到该顶点的权值信息

        // 调整weight数组当前顶点的权值,并修改当前顶点的前驱顶点
        for (int j = 1; j <= numVertexes; j++)
            if (!visited[j] && matrix[minVex][j] < INFINITY)
                if (weight[j] > weight[minVex] + matrix[minVex][j]) {
                    weight[j] = weight[minVex] + matrix[minVex][j];
                    previous[j] = minVex;
                }
    }

    // 输出算法运行结果
    System.out.println("Dijkstra算法计算最短路径的结果为:");
    int temp;
    Stack<Integer> stack = new Stack<>();
    for (int i = 1; i <= numVertexes; i++) {
        temp = previous[i];
        while (temp != INFINITY) {
            stack.push(temp);
            temp = previous[temp];
        }
        System.out.print("顶点 " + v + " 到顶点 " + i + " 的最短距离为: " + weight[i] + " , 最短路径为: ");
        while (!stack.empty()) System.out.print(stack.pop() + "->");
        System.out.println(i);
    }
}
// Floyd最短路径算法
public void Floyd() {
    int[][] path = new int[numVertexes + 1][numVertexes + 1];    // 存储最短路径
    int[][] weights = new int[numVertexes + 1][numVertexes + 1]; // 存储权重和

    // 初始化操作
    for (int i = 1; i <= numVertexes; i++)
        for (int j = 1; j <= numVertexes; j++) {
            weights[i][j] = matrix[i][j];
            path[i][j] = j;
        }

    // 算法开始
    for (int k = 1; k <= numVertexes; k++)
        for (int i = 1; i <= numVertexes; i++)
            for (int j = 1; j <= numVertexes; j++) {
                // 如果经过下标为k顶点路径比原来两顶点间路径更短,则将当前两点间的权值设为更小的一个
                if (weights[i][j] > weights[i][k] + weights[k][j]) {
                    weights[i][j] = weights[i][k] + weights[k][j];
                    path[i][j] = path[i][k];
                }
            }

    // 输出算法执行结果
    System.out.println("Floyd算法计算最短路径的结果为:");
    for (int i = 1; i <= numVertexes; i++)
        for (int j = 1; j <= numVertexes; j++) {
            if (i == j) {
                // 在邻接矩阵初始化时没有让对角线为0,所以当i==j时往往无法显示正确结果
                System.out.println("顶点 " + i + " 到顶点 " + j + " 的最短距离为: " + 0 + " , 最短路径为: " + i + "->" + j);
                continue;
            }

            System.out.print("顶点 " + i + " 到顶点 " + j + " 的最短距离为: " + weights[i][j] + " , 最短路径为: ");
            int k = path[i][j];
            System.out.print(i);
            while (k != j) {
                System.out.print("->" + k);
                k = path[k][j];
            }
            System.out.println("->" + j);
        }
    System.out.println();
}

3.3 拓扑排序

由于拓扑排序和关键路径两个算法使用邻接表实现,因此先给出邻接表创建的代码:

/**
 * 图的邻接表形式,主要用于拓扑排序和关键路径的计算
 */
public class ALGraph {
    private int numVertex;
    private int numEdge;

    // 边表结构
    private static class Edge {
        int adjVertex;
        int weight;
        Edge next;

        public Edge(int vertex, int weight, Edge next) {
            this.adjVertex = vertex;
            this.weight = weight;
            this.next = next;
        }
    }

    // 顶点表结构
    private static class Vertex {
        int in; // 入度
        Edge firstEdge;
    }

    // 枚举AOV网和AOE网
    public enum Type {
        AOV,
        AOE
    }

    private Vertex[] adjList = null; // 邻接表

    public ALGraph(Type type) {
        Scanner input = new Scanner(System.in);
        numVertex = input.nextInt();
        numEdge = input.nextInt();

        // 初始化邻接表
        adjList = new Vertex[numVertex];
        for (int i = 0; i < numVertex; i++)
            adjList[i] = new Vertex();

        int u, v, w;
        if (type == Type.AOV) { // AOV网(有向无环图)
            for (int i = 0; i < numEdge; i++) {
                u = input.nextInt() - 1; // 存的是角标,要减1,防止数组越界
                v = input.nextInt() - 1;
                w = 1;

                if (adjList[u].firstEdge == null)
                    adjList[u].firstEdge = new Edge(v, w, null);
                else {
                    Edge temp = adjList[u].firstEdge;
                    while (temp.next != null) temp = temp.next;
                    temp.next = new Edge(v, w, null);
                }
                // 入度自增
                adjList[v].in++;
            }
        } else { // AOE网(有向无环网)
            for (int i = 0; i < numEdge; i++) {
                u = input.nextInt() - 1;
                v = input.nextInt() - 1;
                w = input.nextInt();

                if (adjList[u].firstEdge == null)
                    adjList[u].firstEdge = new Edge(v, w, null);
                else {
                    Edge temp = adjList[u].firstEdge;
                    while (temp.next != null) temp = temp.next;
                    temp.next = new Edge(v, w, null);
                }
                // 入度自增
                adjList[v].in++;
            }
        }
    }
}

AOV网的拓扑排序算法:

/**
 * 拓扑排序算法
 * 
 * 所谓拓扑排序,就是对一个有向图构造拓扑序列的过程。构造时会有两个结果:
 * 1.若此网的全部顶点都被输出,则说明它是AOV网(不存在环)
 * 2.若输出顶点数少于全部顶点数,则说明存在环,不是AOV网
 * 
 * 拓扑排序主要是为了解决一个工程能否顺序执行的问题
 * 
 * 对AOV网进行拓扑排序算法的基本思路是:从AOV网中选择一个入度为0的顶点输出,然后删去此顶点,
 * 并删去以此顶点为尾的弧,重复此步骤,直到输出全部顶点或AOV网中不存在入度为0的顶点为止
 */
public void topologicalSort() {
    int count = 0;
    Stack<Integer> stack = new Stack<>();

    // 将所有入度为0的顶点入栈
    for (int i = 0; i < numVertex; i++)
        if (adjList[i].in == 0) stack.push(i);

    while (!stack.empty()) {
        int temp = stack.pop(); // 出栈
        System.out.print((temp + 1) + "->");
        count++;

        // 遍历此顶点的边表
        for (Edge edge = adjList[temp].firstEdge; edge != null; edge = edge.next) {
            int k = edge.adjVertex;
            adjList[k].in--;

            if (adjList[k].in == 0) stack.push(k);
        }
    }

    System.out.println("\b\b");
    if (count < numVertex)
        System.out.println("该AOV网存在环,拓扑排序失败!");
    else
        System.out.println("拓扑排序完毕");
}

3.4 关键路径

/**
 * 关键路径算法解决的是工程完成需要的最短时间问题
 * 
 * 路径长度:路径上各个活动持续时间之和
 * 关键路径:从源点到汇点具有最大长度的路径
 * 关键活动:关键路径上的活动
 * 
 * 参数:
 * - etv:事件的最早发生时间
 * - ltv:事件的最晚发生时间
 * - ete:活动的最早开始时间
 * - lte:活动的最晚开始时间,即不推迟工期的最晚开工时间
 * 根据ete与lte是否相等判断弧是否是关键活动
 */
public void criticalPath() {
   int ete, lte;
   Stack<Integer> sorted = new Stack<>();
   int[] etv = new int[numVertex];
   int[] ltv = new int[numVertex];

   // 求etv的拓扑序列
   topologicalSort(sorted, etv);

   // 初始化ltv
   Arrays.fill(ltv, etv[numVertex - 1]);

   // 计算ltv
   while (!sorted.empty()) {
       int temp = sorted.pop();
       for (Edge edge = adjList[temp].firstEdge; edge != null; edge = edge.next) {
           int k = edge.adjVertex;
           if (ltv[k] - edge.weight < ltv[temp])
               ltv[temp] = ltv[k] - edge.weight;
       }
   }

   // 计算ete、lte,并求出关键路径
   int sum = 0;
   System.out.println("输出关键路径为:");
   for (int i = 0; i < numVertex; i++)
       for (Edge edge = adjList[i].firstEdge; edge != null; edge = edge.next) {
           int k = edge.adjVertex;
           ete = etv[i];
           lte = ltv[k] - edge.weight;
           if (ete == lte) {
               sum += edge.weight;
               System.out.println("顶点" + i + "到顶点" + k + ", 路径长度为:" + edge.weight);
           }
       }
   System.out.println("关键路径总长为:" + sum);
}

// 用于计算AOE网中所有事件的最早发生时间
private void topologicalSort(Stack<Integer> sorted, int[] etv) {
   int count = 0;
   Stack<Integer> stack = new Stack<>();

   // 将所有入度为0的顶点入栈
   for (int i = 0; i < numVertex; i++)
       if (adjList[i].in == 0) stack.push(i);

   while (!stack.empty()) {
       int temp = stack.pop();
       sorted.push(temp);
       count++;

       // 遍历此顶点的边表
       for (Edge edge = adjList[temp].firstEdge; edge != null; edge = edge.next) {
           int k = edge.adjVertex;
           adjList[k].in--;

           if (adjList[k].in == 0)
               stack.push(k);
           if (etv[temp] + edge.weight > etv[k])
               etv[k] = etv[temp] + edge.weight;
       }
   }
   if (count < numVertex)
       System.out.println("该AOE网存在环,拓扑排序失败!");
   else
       System.out.println("拓扑排序完毕");
}

四、代码测试数据(测试前需要指定图的类型)

# 无向网,测试最短路径、BFS、DFS
9 16
1 2 1
1 3 5
2 3 3
2 4 7
2 5 5
3 5 1
3 6 7
4 7 3
4 5 2
5 7 6
5 8 9
5 6 3
6 8 5
7 9 7
7 8 2
8 9 4

# 有向网,测试最短路径、BFS、DFS
5 11
1 5 12
5 1 8
1 2 16
2 1 29
5 2 32
2 4 13
4 2 27
1 3 15
3 1 21
3 4 7
4 3 19

# AOV网,测试拓扑排序
14 20
1 12
1 6
1 5
2 9
2 5
2 3
3 10
3 7
3 6
4 14
4 3
5 8
6 13
6 9
7 6
9 8
10 12
10 11
11 14
13 10

# AOE网,测试关键路径
10 13
1 3 4
1 2 3
2 5 6
2 4 5
3 6 7
3 4 8
4 5 3
5 8 4
5 7 9
6 8 6
7 10 2
8 9 5
9 10 3

参考资料:清华大学《数据结构(C语言版)》、《大话数据结构》

2021/02/03更新完毕,已完结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值