06-图

图的表示方式:邻接矩阵、邻接链表

1. 邻接矩阵表示图

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

// 图的邻接矩阵存储结构
public class Graph {
    int maxVertexNum; // 最大顶点数
    List<String> vertex; // 顶点表
    int[][] edge; // 邻接矩阵,边表
    int edgeNum; // 图中当前的边数
    boolean[] visited; // 顶点是否被访问

    public Graph(int maxVertexNum) {
        this.maxVertexNum = maxVertexNum;
        vertex = new ArrayList<>(maxVertexNum);
        edge = new int[maxVertexNum][maxVertexNum];
        visited = new boolean[maxVertexNum];
    }

    // 显示邻接矩阵
    public void display() {
        for (int i = 0; i < edge.length; i++) {
            System.out.println(Arrays.toString(edge[i]));
        }
    }

    // 在图中插入一个顶点
    public void insertVertex(String vertex) {
        this.vertex.add(vertex);
    }

    // 向图中添加一条无向边<vs, ve>,其权值为weight
    public void addEdge(int vs, int ve, int weight) {
        edge[vs][ve] = weight;
        edge[ve][vs] = weight;
        edgeNum++;
    }

    // 获取边<vs, ve>的权值
    public int getEdgeValue(int vs, int ve) {
        return edge[vs][ve];
    }

    // 获取顶点vertex的第一个邻接点*
    public int firstNeighbor(int vertex) {
        for (int c = 0; c < this.vertex.size(); c++) {
            if (edge[vertex][c] > 0)
                return c;
        }
        return -1;
    }

    // 获取vertex的除了exclude顶点的下一个邻接点*
    public int nextNeighbor(int vertex, int exclude) {
        for (int c = exclude + 1; c < this.vertex.size(); c++) {
            if (edge[vertex][c] > 0)
                return c;
        }
        return -1;
    }

    // 对图进行深度优先遍历
    public void dfsTraverse() {
        int vertexNum = vertex.size();
        // 初始化visited
        for (int v = 0; v < vertexNum; v++)
            visited[v] = false;
        // 从0开始逐个访问
        for (int v = 0; v < vertexNum; v++)
            if (!visited[v])
                dfs(v);
    }

    // Depth First Search,深度优先搜索,递归
    private void dfs(int v) {
        System.out.print(vertex.get(v) + " "); // 访问顶点v
        visited[v] = true; // 标记为已被访问
        // adjacent为v的尚未被访问的邻接顶点
        for (int adjacent = firstNeighbor(v); adjacent >= 0; adjacent = nextNeighbor(v, adjacent)) {
            // v的所有邻接结点是从左到右按顺序访问
            if (!visited[adjacent])
                dfs(adjacent);
        }
    }

    // 借助栈,非递归,从顶点v开始深度优先*
    public void dfsTraverse(int v) {
        LinkedList<Integer> stack = new LinkedList<>();
        // 初始化visited
        for (int i = 0; i < vertex.size(); i++)
            visited[i] = false;
        // 将v入栈,入栈一个就将其置为已被访问
        stack.push(v);
        visited[v] = true;
        while (!stack.isEmpty()) {
            // 出栈一个顶点并访问
            v = stack.pop();
            System.out.print(vertex.get(v) + " ");
            // 将v的所有邻接结点adjacent入栈
            for (int adjacent = firstNeighbor(v); adjacent >= 0; adjacent = nextNeighbor(v, adjacent))
                // 由于是栈,因此v的所有邻接结点是从右到左逆序访问
                if (!visited[adjacent]) { // 未进过栈的顶点入栈
                    stack.push(adjacent);
                    visited[adjacent] = true;
                }
        }
    }

    // 对图进行广度优先遍历
    public void bfsTraverse() {
        // 初始化visited
        for (int i = 0; i < vertex.size(); i++)
            visited[i] = false;
        // 从0开始逐个访问
        for (int i = 0; i < vertex.size(); i++)
            if (!visited[i])
                bfs(i);
    }

    // Breadth First Search,广度优先搜索,借助队列,非递归*
    private void bfs(int v) {
        LinkedList<Integer> queue = new LinkedList<>();
        // 访问v,并将其入队
        System.out.print(vertex.get(v) + " ");
        visited[v] = true;
        queue.addLast(v);
        while (!queue.isEmpty()) {
            // 顶点v出队
            v = queue.removeFirst();
            // 访问v的所有邻接点
            for (int adjacent = firstNeighbor(v); adjacent >= 0; adjacent = nextNeighbor(v, adjacent))
                if (!visited[adjacent]) {
                    // 访问adjacent,并将其入队
                    System.out.print(vertex.get(adjacent) + " ");
                    visited[adjacent] = true;
                    queue.addLast(adjacent);
                }
        }
    }
}

// 测试
class GraphClient {
    public static void main(String[] args) {
        String[] vertex = {"1", "2", "3", "4", "5", "6", "7", "8"};
        Graph graph = new Graph(vertex.length);
        for (String s : vertex)
            graph.insertVertex(s);
        graph.addEdge(0, 1, 1); // 1-2
        graph.addEdge(0, 2, 1); // 1-3
        graph.addEdge(1, 3, 1); // 2-4
        graph.addEdge(1, 4, 1); // 2-5
        graph.addEdge(2, 5, 1); // 3-6
        graph.addEdge(2, 6, 1); // 3-7
        graph.addEdge(3, 7, 1); // 4-8
        graph.addEdge(4, 7, 1); // 5-8
        graph.addEdge(5, 6, 1); // 6-7
        graph.display();
        System.out.println("递归深度优先:");
        // 1 2 4 8 5 3 6 7
        graph.dfsTraverse();
        System.out.println("\n非递归深度优先:");
        // 1 3 7 6 2 5 8 4
        graph.dfsTraverse(0);
        System.out.println("\n非递归广度优先:");
        // 1 2 3 4 5 6 7 8
        graph.bfsTraverse();
    }
}

2. 最小生成树

最小生成树可以用Prim(普里姆)算法或Kruskal(克鲁斯卡尔)算法求出。

Prim算法简述:

  1. 输入:一个加权连通图,其中顶点集合为V,边集合为E
  2. 初始化:V_{new}=\{ x \},其中x为集合V中的任一节点,E_{new}=\{ \},为空
  3. 重复下列操作,直到V_{new}=V
    1. 在集合E中选取权值最小的边<u,v>,其中u为集合V_{new}中的元素,而v不在V_{new}集合当中,并且v \in V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一)
    2. 将v加入集合V_{new}中,将<u,v>边加入集合E_{new}
  4. 输出:使用集合V_{new}E_{new}来描述所得到的最小生成树

Kruskal算法简述:

  1. 构造一个只含n个顶点,而边集为空的子图,若将该子图中各个顶点看成是各棵树上的根结点,则它是一个含有n棵树的一个森林。
  2. 从边集E中选取一条权值最小的边,若该条边的两个顶点分属不同的树,则将其加入子图;反之,若该条边的两个顶点已落在同一棵树上,则不可取,而应该取下一条权值最小的边再试之。直至森林中只有一棵树,也即子图中含有n-1条边为止。
import java.util.ArrayList;
import java.util.List;

class Edge {
    String start;
    String end;
    int weight;

    public Edge(String start, String end, int weight) {
        this.start = start;
        this.end = end;
        this.weight = weight;
    }
}

public class MinimumSpanningTree {
    // prim求解最小生成树,v表示从第v个顶点开始生成
    public static void prim(Graph graph, int v) {
        // visited标记结点是否被访问
        boolean[] visited = new boolean[graph.maxVertexNum];
        // 初始化visited
        for (int i = 0; i < visited.length; i++)
            visited[i] = false;
        // 顶点v已被访问
        visited[v] = true;
        // vs和ve记录两个顶点的下标
        int vs = -1;
        int ve = -1;
        int minWeight = Integer.MAX_VALUE;
        for (int k = 1; k < graph.maxVertexNum; k++) {
            // 确定每一次生成的子图和哪个结点的距离最近
            for (int i = 0; i < graph.maxVertexNum; i++) { // 遍历已经访问过的结点
                for (int j = 0; j < graph.maxVertexNum; j++) { // 遍历所有没有访问过的结点
                    // 如果是一个访问过的结点和一个没有访问过的结点,并且当前边的权值小于最小权值
                    if (visited[i] == true && visited[j] == false && graph.edge[i][j] < minWeight) {
                        minWeight = graph.edge[i][j];
                        vs = i;
                        ve = j;
                    }
                }
            }
            System.out.println(graph.vertex.get(vs) + "->" + graph.vertex.get(ve) + ": " + minWeight);
            visited[ve] = true;
            minWeight = Integer.MAX_VALUE;
        }
    }

    // kruskal求解最小生成树
    public static void kruskal(Graph graph, List<Edge> edges) {
        // 保存已有最小生成树中,每个顶点所在树的根的索引
        int[] ends = new int[edges.size()];
        // 对边进行排序,按照权值从小到大
        edges.sort(((o1, o2) -> o1.weight - o2.weight));
        // 遍历边集合,将边添加到最小生成树中
        // 判断准备加入的边是否形成回路,即是同一棵树的两个顶点
        for (int i = 0; i < edges.size(); i++) {
            // 获取第i条边的起点索引
            int start = graph.vertex.indexOf(edges.get(i).start);
            // 获取第i条边的终点索引
            int end = graph.vertex.indexOf(edges.get(i).end);
            // 获取起点所在树的根索引
            int startRoot = getRoot(ends, start);
            // 获取终点所在树的根索引
            int endRoot = getRoot(ends, end);
            if (startRoot != endRoot) { // 不在同一棵树
                // 设置startRoot这棵树在已有最小生成树中的终点
                ends[startRoot] = endRoot;
                // 输出这一条边
                System.out.println(edges.get(i).start + "--" + edges.get(i).end + ": " + edges.get(i).weight);
            }
        }
    }

    // (kruskal的难点)返回顶点i所在树的根的索引,ends数组记录各个顶点所在树的根的索引
    private static int getRoot(int[] ends, int i) {
        while (ends[i] != 0)
            i = ends[i];
        return i;
    }

    // 测试
    public static void main(String[] args) {
        String[] vertex = {"V1", "V2", "V3", "V4", "V5", "V6"};
        // 将顶点加入到集合
        List<String> list = new ArrayList<>();
        for (String v : vertex)
            list.add(v);
        // max表示不连通
        int max = Integer.MAX_VALUE;
        // 初始化邻接矩阵
        int[][] edge = new int[][]{
                {max, 6, 1, 5, max, max},
                {6, max, 5, max, 3, max},
                {1, 5, max, 5, 6, 4},
                {5, max, 5, max, max, 2},
                {max, 3, 6, max, max, 6},
                {max, max, 4, 2, 6, max}
        };
        // 将边加入到集合
        List<Edge> edges = new ArrayList<>();
        for (int r = 0; r < edge.length; r++)
            for (int c = r + 1; c < edge[r].length; c++)
                if (edge[r][c] != max)
                    edges.add(new Edge(vertex[r], vertex[c], edge[r][c]));
        // 创建一个图,图是自己定义的
        Graph graph = new Graph(vertex.length);
        graph.vertex = list;
        graph.edge = edge;
        graph.edgeNum = edges.size();
        prim(graph, 0);
        System.out.println("---------");
        kruskal(graph, edges);
    }
}

3. 最短路径

最短路径可以用Dijkstra(迪杰斯特拉)算法或Floyd(弗洛伊德)算法求出。

  1. Dijkstra算法可以求出指定顶点到其他各个顶点的最短路径
  2. Floyd算法可以求出每一个顶点到其他各个顶点的最短路径

Dijkstra算法过程:Dijkstra算法不适用于含有负权边的图

  1. 初始化:集合S初始为{0},dist[]的初始值dist[i]=arcs[0][i],i=1,2,...,n-1
  2. 从顶点集合V-S中选出v_j,满足dist[j]=Min \{ dist[i] | v_{i} \in V-S \}v_j就是当前求得的一条从v_0出发的最短路径的终点,令S=S \cup \{j\}
  3. 修改从v_0出发到集合V-S上任一顶点v_k可达的最短路径长度:若dist[j]+arcs[j][k]<dist[k],则令dist[k]=dist[j]+arcs[j][k]
  4. 重复2~3操作共n-1次,直到所有的顶点都包含在S中。
  • dist[]:记录从源点v_0到其他各顶点当前的最短路径长度,dist[i]的初值为arcs[v_0][i]
  • path[]:path[i]表示从源点到顶点i之间的最短路径的前驱结点,在算法结束时,可根据其值追溯得到源点v_0到顶点v_i的最短路径。

Floyd算法过程:Floyd算法又称为插点法,利用了动态规划的思想

  1. 从任意一条单边路径开始。所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大。
  2. 对于每一对顶点u和v,看看是否存在一个顶点w使得从u到w再到v比已知的路径更短,如果存在则更新它。
  • 把图用邻接矩阵G表示出来,如果从v_iv_j有路可达,则G[i][j]=d,d表示该路的长度;否则G[i][j]=+\infty。定义一个矩阵D用来记录所插入点的信息,D[i][j]表示从v_iv_j需要经过的点,初始化D[i][j]=j。把各个顶点插入图中,比较插点后的距离与原来的距离,G[i][j]=min(G[i][j],G[i][k]+G[k][j]),如果G[i][j]的值变小,则D[i][j]=k。在G中包含有两点之间最短道路的信息,而在D中则包含了最短通路径的信息。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

// Dijkstra算法的辅助数组类
class AuxiliaryArray {
    // 记录各个顶点是否访问过:1访问过,0未访问
    int[] visited;
    // 记录从源点到其他各顶点当前的最短路径长度
    int[] dist;
    // path[i]表示从源点到顶点i之间的最短路径的前驱结点
    int[] path;
    // 顶点数
    int vertexNum;
    // 源点的索引
    int source;

    public AuxiliaryArray(int vertexNum, int source) {
        this.vertexNum = vertexNum;
        this.source = source;
        visited = new int[vertexNum];
        dist = new int[vertexNum];
        path = new int[vertexNum];
        Arrays.fill(dist, Integer.MAX_VALUE / 2); // 初始化dist数组
        visited[source] = 1; // 标记源点已被访问
        dist[source] = 0; // 设置源点到源点的距离为0
    }

    // 判断顶点v是否被访问过
    public boolean isVisited(int v) {
        return visited[v] == 1;
    }

    // 返回从源点到顶点v当前的最短路径长度
    public int getDistance(int v) {
        return dist[v];
    }

    // 更新源点到顶点v的距离为distance
    public void updateDist(int v, int distance) {
        dist[v] = distance;
    }

    // 更新顶点v的前驱结点为顶点prev
    public void updatePath(int v, int prev) {
        path[v] = prev;
    }

    // 选出vj,dist[j]为剩余结点i中dist[i]最小的
    public int next() {
        int minValue = Integer.MAX_VALUE / 2;
        int minIndex = 0;
        for (int i = 0; i < visited.length; i++) {
            if (visited[i] == 0 && dist[i] < minValue) {
                minValue = dist[i];
                minIndex = i;
            }
        }
        // 更新minIndex顶点被访问过
        visited[minIndex] = 1;
        return minIndex;
    }

    // 便于展示
    public void display(List<String> vertex) {
        System.out.println("visited: ");
        for (int i = 0; i < visited.length; i++)
            System.out.print(visited[i] + " ");
        System.out.println("\npath: ");
        for (int i = 0; i < path.length; i++)
            System.out.print(path[i] + " ");
        System.out.println("\ndist: ");
        for (int i = 0; i < dist.length; i++)
            System.out.print(dist[i] + " ");
        System.out.println("\n源点" + vertex.get(source) + "到各顶点的最短路径:");
        for (int i = 0; i < dist.length; i++) {
            if (dist[i] != Integer.MAX_VALUE / 2)
                System.out.print(vertex.get(source) + "-" + dist[i] + "->" + vertex.get(i) + "\t");
            else
                System.out.print("MAX ");
        }
    }
}

public class ShortestPath {
    // dijkstra更新辅助数组Dist和Path
    private static void updateDistAndPath(Graph graph, AuxiliaryArray auxiliary, int j) {
        // 遍历邻接矩阵的第j行
        for (int k = 0; k < graph.edge[j].length; k++) {
            // distance表示源点到顶点v的距离+源点到顶点j的距离
            int distance = auxiliary.getDistance(j) + graph.edge[j][k];
            // 如果顶点j没有被访问过,并且distance小于源点到顶点j的距离,就更新
            if (!auxiliary.isVisited(k) && distance < auxiliary.getDistance(k)) {
                // 更新顶点j的前驱结点为顶点v
                auxiliary.updatePath(k, j);
                // 更新源点到顶点j的距离为distance
                auxiliary.updateDist(k, distance);
            }
        }
    }

    // Dijkstra算法求最短路径
    public static void dijkstra(Graph graph, AuxiliaryArray auxiliary, int source) {
        updateDistAndPath(graph, auxiliary, source);
        for (int j = 1; j < graph.vertex.size(); j++) {
            // 获取下一个访问顶点next
            int next = auxiliary.next();
            // 设置到next顶点的最短路径和前驱
            updateDistAndPath(graph, auxiliary, next);
        }
    }

    // Floyd算法求最短路径
    public static void floyd(Graph graph, int[][] dist, int[][] path) {
        // 初始化dist为邻接矩阵
        for (int r = 0; r < graph.maxVertexNum; r++)
            for (int c = 0; c < graph.maxVertexNum; c++)
                dist[r][c] = graph.edge[r][c];
        // 初始化path
        for (int i = 0; i < graph.maxVertexNum; i++)
            // 初始时,各个顶点到源点i的前驱默认为i
            Arrays.fill(path[i], i);
        // Floyd求最短路径过程
        for (int k = 0; k < dist.length; k++) { // 中间顶点k
            for (int i = 0; i < dist.length; i++) { // 从顶点i出发
                for (int j = 0; j < dist.length; j++) { // 到达顶点j
                    // distance为路径i-->k-->j的距离
                    int distance = dist[i][k] + dist[k][j];
                    // 如果i-->k-->j的距离小于i-->j的距离
                    if (distance < dist[i][j]) {
                        // 更新最短路径
                        dist[i][j] = distance;
                        // 更新前驱
                        path[i][j] = path[k][j];
                    }
                }
            }
        }
    }

    // 测试
    public static void main(String[] args) {
        String[] vertex = {"A", "B", "C", "D", "E", "F", "G"};
        // 将顶点加入到集合
        List<String> list = new ArrayList<>();
        for (String v : vertex)
            list.add(v);
        // max表示不连通
        int max = Integer.MAX_VALUE / 2;
        // 初始化邻接矩阵
        int[][] edge = new int[][]{
                {0, 5, 7, max, max, max, 2},
                {5, 0, max, 9, max, max, 3},
                {7, max, 0, max, 8, max, max},
                {max, 9, max, 0, max, 4, max},
                {max, max, 8, max, 0, 5, 4},
                {max, max, max, 4, 5, 0, 6},
                {2, 3, max, max, 4, 6, 0}
        };
        // 创建一个图,图是自己定义的
        Graph graph = new Graph(vertex.length);
        graph.vertex = list;
        graph.edge = edge;

        System.out.println("-----Dijkstra-----");
        AuxiliaryArray auxiliary = new AuxiliaryArray(graph.maxVertexNum, 2);
        dijkstra(graph, auxiliary, auxiliary.source);
        auxiliary.display(graph.vertex);

        System.out.println("\n-----Floyd-----");
        int[][] dist = new int[graph.maxVertexNum][graph.maxVertexNum];
        int[][] path = new int[graph.maxVertexNum][graph.maxVertexNum];
        floyd(graph, dist, path);
        // 遍历dist和path
        for (int r = 0; r < dist.length; r++) {
            System.out.print("各个顶点到源点" + graph.vertex.get(r) + "的前驱:");
            for (int c = 0; c < dist.length; c++)
                System.out.print(vertex[path[r][c]] + " ");
            System.out.println();
            for (int c = 0; c < dist.length; c++)
                System.out.print(vertex[r] + "-" + dist[r][c] + "->" + vertex[c] + "\t");
            System.out.println();
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,825评论 4 377
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,887评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,425评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,801评论 0 224
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,252评论 3 299
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,089评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,216评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 31,005评论 0 215
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,747评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,883评论 2 255
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,354评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,694评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,406评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,222评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,242评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,017评论 2 281

推荐阅读更多精彩内容