安卓数据结构09-图论之最小生成树与最短路径

数据结构09-图

一、图的基本概念

1.什么是图

图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。

2.图的基本性质

  • 线性表中我们把数据元素叫元素,树中将数据元素叫节点,在图中数据元素,我们称之为顶点(Vertex)。
  • 线性表中可以没有元素,称为空表;树中可以没有节点。称为空树;图中不能没有顶点,可以没有边。
  • 线性表中,相邻的数据元素之间具有线性关系;树中,相邻两层的节点具有层次关系;而图中,任意两点之间都可能有关系,顶点之间的逻辑关系用边表示,边集可以是空的。

3.无向图

无向边:若顶点Vi到Vj之间的边没有方向,则称这条边为无向边(Edge),用无序偶(ViVj)来表示。

无向图(Undirected graphs)是任意两个顶点之间的边都是无向边的图。

无向完全图是任意两个顶点之间都存在边的无向图。

4.有向图

有向边:若顶点Vi到Vj之间的边有方向,则称这条边为有向边,也称为弧(Arc)。用有序偶<Vi, Vj>来表示,Vi称为弧尾(Tail),。Vi称为弧头(Head)。

有向图(Directed graphs)是任意两个顶点之间的边都是有向边的图。

有向完全图是任意两个顶点之间都存在方向相反的两条弧的有向图。

5.图的权

有些图的边或弧具有与它相关的数字,这些数字叫做权。

6.连通图

在一个无向图 G 中,若从顶点i到顶点j有路径相连(当然从j到i也一定有路径),则称i和j是连通的。如果 G 是有向图,那么连接i和j的路径中所有的边都必须同向。

连通图:图中任意两点都是连通的图。

连通分量:无向图 G的一个极大连通子图称为 G的一个连通分量。连通图只有一个连通分量,即其自身;非连通的无向图有多个连通分量。

强连通图:有向图 G(V,E) 中,若对于V中任意两个不同的顶点 xy,都存在从xy以及从 yx的路径,则称 G是强连通图。

强连通分量:强连通图只有一个强连通分量,即是其自身;非强连通的有向图有多个强连分量。

7.度

无向图顶点的边数叫度,有向图顶点的边数叫出度和入度。

二、图的数据存储结构

由于图的结构比较复杂,任意两个顶点之间都可能存在联系,因此无法以数据元素在内存中的物理位置来表示元素之间的关系。也就是说,图不可能用简单的顺序存储结构来表示。

图有两种存储结构:邻接矩阵和邻接表。

1.邻接矩阵

考虑到图是由顶点和边组成的,用一个结构表示比较困难,所以用两个结构来分别存储。顶点不分大小、主次,所以用一个一维数组来存储。而边是顶点与顶点之间的关系,一位数组搞不定,所有用二维数组。于是我们的邻接矩阵方案就诞生了。

图的邻接矩阵(Adjacency Matrix)用两个数组来表示图。一个一位数组存储顶点,一个二维数组(称为邻接矩阵)存储边。这个二维数组是一个对称矩阵。

带权邻接矩阵是图的边带有权重的邻接矩阵。

优点:实现简单,可以直接查询任意两节点间是否存在边,和边的权值

缺点:遍历效率低;对于边数相对顶点较少的图,这种结构存在对存储空间的极大浪费。

2.邻接表

邻接表用数组和链表表示,数组存放顶点,每个顶点又是一个链表,用于存放顶点的所有邻接顶点。在有向图中,这个链表存放的是相邻并且有方向的顶点;在无向图中,这个链表存放的是所有相邻的顶点。

出边表:链表中存放正向相邻顶点的邻接表。

称逆邻接表:链表中存放逆向相邻顶点的邻接表。。

带权邻接表:链表中存放相邻顶点和相邻顶点之间的边的权值的邻接表。

优点:复杂度低
缺点:无法直接判断两点间是否存在边

三、图的遍历

图的遍历和树的遍历类似,我们希望从图中某一顶点出发访遍图中其余顶点,且每一个顶点仅访问一次,这一过程就叫做图的遍历(Traversing Graph)。

1.深度优先遍历

基本思想

假设给定图G的初态是所有顶点均未曾访问过。在G中任选一顶点v为初始出发点(源点),则深度优先遍历可定义如下:

  1. 首先访问出发点v,并将其标记为已访问过;
  2. 然后依次从v出发搜索v的每个邻接点w。若w未曾访问过,则以w为新的出发点继续进行深度优先遍历,直至图中所有和源点v有路径相通的顶点均已被访问为止。
  3. 若此时图中仍有未访问的顶点,则另选一个尚未访问的顶点作为新的源点重复上述过程,直至图中所有顶点均已被访问为止。

图的深度优先遍历也叫深度优先搜索(Depth First Search),类似于树的前序遍历。采用的搜索方法的特点是尽可能先对纵深方向进行搜索。

应用:最大路径。

代码实现

//深度优先遍历
public void dfsErgodic() {
    if (Tool.isEmpty(vertices)) {
        return;
    }
    boolean[] visit = new boolean[vertices.length];
    for (int i = 0; i < vertices.length; i++) {
        dfs(visit, i);
    }
}

/**
 * @param visit 表示已经访问过的顶点
 * @param index 对应顶点的下标
 */
public void dfs(boolean[] visit, int index) {
    if (visit[index] || Tool.isEmpty(matrix) || Tool.isEmpty(matrix[index])) {
        return;
    }

    visit[index] = true;
    ToolShow.log(vertices[index].toString());
    //邻接点
    int[] mat = matrix[index];
    for (int i = 0; i < mat.length; i++) {
        //权值为0代表自己,权值为M代表不可达
        if (mat[i] > 0 && mat[i] < M) {
            //优先访问第一个邻接点
            dfs(visit, i);
        }
    }
}

2.广度优先遍历

基本思想

广度优先遍历是连通图的一种遍历策略。因为它的思想是从一个顶点V0开始,辐射状地优先遍历其周围较广的区域,故得名。

遍历过程:

  1. 从图中某个顶点V0出发,并访问此顶点;
  2. 从V0出发,访问V0的各个未曾访问的邻接点W1,W2,…,Wk;然后,依次从W1,W2,…,Wk出发访问各自未被访问的邻接点;
  3. 重复步骤2,直到全部顶点都被访问为止。

应用:广度优先生成树、最短路径。

代码实现

// 广度优先遍历
public void bfsErgodic() {
    if (Tool.isEmpty(vertices)) {
        return;
    }
    boolean[] visit = new boolean[vertices.length];
    for (int i = 0; i < vertices.length; i++) {
        bfs(visit, i);
    }
}

/**
 * @param visit 表示已经访问过的顶点
 * @param index :对应顶点的下标
 */
public void bfs(boolean[] visit, int index) {
    if (visit[index] || Tool.isEmpty(matrix) || Tool.isEmpty(matrix[index])) {
        return;
    }

    visit[index] = true;
    ToolShow.log(vertices[index].toString());
    //邻接点
    int[] mat = matrix[index];
    //已访问过的邻接点
    List<Integer> visitedE = new ArrayList<>();
    //先访问所有的邻接点
    for (int i = 0; i < mat.length; i++) {
        //不能重复访问
        if (!visit[i] && mat[i] > 0 && mat[i] < M) {
            visit[i] = true;
            ToolShow.log(vertices[i].toString());
            visitedE.add(i);
        }
    }
    //再以已经访问过的邻接点为起点,开始访问
    for (Integer m : visitedE) {
        dfs(visit, m);
    }
}

四、最小生成树

1.基本概念

树(Tree):不存在回路的无向连通图。

生成树(Spanning Tree):无向连通图G的一个子图如果是一颗包含G所有顶点的树,则该子图称为G的生成树。

生成树的权:无向连通图的生成树的各边的权值总和。

最小生成树(Minimum Spanning Tree ,MST):权最小的生成树。最小生成树也叫最小代价树(Minimum-cost Spanning Tree)。

最小生成树算法:Prim、Kruskal。

应用:城市光钎路径等。

2.Prim算法

基本思想

Prim算法(普里姆算法)是一种最小生成树算法。Prim算法从任意一个顶点开始,每次选择一个与当前顶点集最近的一个顶点,并将两顶点之间的边加入到树中。

Prim算法在找当前最近顶点时使用到了贪婪算法。

算法描述:

  1. 在一个加权连通图中,顶点集合V,边集合为E;
  2. 任意选出一个点作为初始顶点,标记为visit,计算所有与之相连接的点的距离,选择距离最短的,标记visit;
  3. 在剩下的点中,计算与已标记visit点距离最小的点,标记visit,证明加入了最小生成树;
  4. 重复3,直到所有点都被标记为visit。

代码实现

public List<Edge<E>> prim(E v) {
    int size = vertices == null ? 0 : vertices.length;
    if (size < 1) {
        return null;
    }
    List<Edge<E>> result = new ArrayList<>();
    //已标记的点
    List<Integer> visit = new ArrayList<>();
    visit.add(getIndex(v));
    for (int m = 0; m < size; m++) {
        int start = -1;
        int end = -1;
        int weight = M;
        //找到未访问的点中,距离当前最小生成树距离最小的点
        for (Integer n : visit) {
            //邻接点
            int[] mat = matrix[n];
            //找到最小邻接点的下标
            int min = getMin(mat, visit);
            if (min != -1 && mat[min] > 0 && mat[min] < weight) {
                weight = mat[min];
                start = n;
                end = min;
            }
        }
        if (start > -1 && end > -1) {
            Edge<E> e = new Edge<>(vertices[start], vertices[end], weight);
            result.add(e);
            visit.add(end);
        }
    }
    return result;
}

//获取数组的最小权值的下标
public int getMin(int[] arr, List<Integer> visit) {
    int index = -1;
    if (Tool.isEmpty(arr)) {
        return index;
    }
    int weight = M;
    for (int i = 0; i < arr.length; i++) {
        if (visit.contains(i)) {
            continue;
        }
        int w = arr[i];
        if (w > 0 && w < weight) {
            index = i;
            weight = w;
        }
    }
    return index;
}

3.Kruskal算法

基本思想

Kruskal算法(克鲁斯卡尔算法)是另一个计算最小生成树的算法,其算法原理如下:

  • 首先,将所有的边排序,并创建一个空的顶点集合;
  • 然后,按照权值的升序来选择边。如果起点和终点被相同的集合包含,就跳过。否则,将这条边插入最小生成树中。
  • 然后更新顶点集合:如果所有的集合都不包含这条边的顶点,就新建一个集合,并将他的顶点添加进去;如果起点只被一个集合包含,那就把终点添加到这个集合;如果终点只被一个集合包含,那就把起点添加到这个集合;如果起点和终点分别被不同的集合包含,那就把这两个集合合并;
  • 重复这个过程直到所有的边都探查过。

代码实现

public List<Edge<E>> kruskal() {
    if (Tool.isEmpty(edges)) {
        return null;
    }
    int size = vertices.length;
    //已取出的线的顶点的集合
    List<List<E>> visitList = new ArrayList<>();
    List<Edge<E>> result = new ArrayList<>();
    for (int i = 0; i < edges.length; i++) {
        Edge<E> e = edges[i];
        //包含e的起点的集合
        List<E> start = null;
        //包含e的终点的集合
        List<E> end = null;
        for (List<E> eList : visitList) {
            if (Tool.isEmpty(eList)) {
                break;
            }
            if (eList.contains(e.start) && !eList.contains(e.end)) {
                start = eList;
            } else if (!eList.contains(e.start) && eList.contains(e.end)) {
                end = eList;
            } else if (eList.contains(e.start) && eList.contains(e.end)) {
                start = eList;
                end = eList;
            }
        }
        //如果所有的集合都不包含这条边的顶点,就新建一个集合,并将他的顶点添加进去
        if (start == null && end == null) {
            List<E> list = new ArrayList<>();
            list.add(e.start);
            list.add(e.end);
            visitList.add(list);
            //如果起点只被一个集合包含,那就把终点添加到这个集合
        } else if (start != null && end == null) {
            start.add(e.end);
            //如果终点只被一个集合包含,那就把起点添加到这个集合
        } else if (start == null && end != null) {
            end.add(e.start);
            //如果起点和终点分别被不同的集合包含,那就把这两个集合合并
        } else if (start != end) {
            start.addAll(end);
            visitList.remove(end);
            //如果起点和终点被相同的集合包含,就跳过
        } else {
            break;
        }
        result.add(e);
    }
    return result;
}

五、最短路径算法

从某顶点出发,沿图的边到达另一顶点所经过的路径中,各边上权值之和最小的一条路径叫做最短路径。

解决最短路的问题有以下算法,Dijkstra算法,Bellman-Ford算法,Floyd算法和SPFA算法等

这里我们只说明Dijkstra算法(迪杰斯特拉算法)。

1.Dijkstra算法的基本思想

迪杰斯特拉(Dijkstra)算法是典型最短路径算法,用于计算一个节点到其他节点的最短路径。
它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止。

操作步骤:

  1. 引进两个集合S和U,S的作用是记录已求出最短路径的顶点,而U中元素的下标表示顶点,U中元素的值表示该顶点到起点s的距离;
  2. 初始时,S只包含起点s;
  3. 从U中选出距离最短的顶点k,并将顶点k加入到S中;
  4. 利用k更新U的值。如果k与o直连,则取(s,o)的距离与(s,k)+(k,o)的距离的最小值作为s与o的距离;
  5. 重复步骤3和4,直到遍历完所有顶点。

2.Dijkstra算法的证明

Dijkstra算法每次从更新后的U中,挑选权值最小的顶点k,然后把k的值当作起点s与顶点k的最短距离。然后用k更新其他未确定最短距离的顶点的值。下面证明它的正确性:

我们把所有与s直接相连顶点叫s的直连点,所有与s不直接相连顶点叫s的非直连点。

最初时,U中的值为s与直接点的权值,可以确定权值最小的顶点k的最短路径就是这个最小权值D,因为通过任意非直接点来连接s,都必须经过至少一个直连点。而s与非直连点的路径=s与直连点的路径+直连点与非直连点的路径,这个值肯定大于D,即D是s与其他点的距离的最小值,那么s与k的距离肯定不会小于D,即s与k的最短路径为D。

然后,把k加入S,再利用k更新U的值。如果k与o直连,则取(s,o)的距离与(s,k)+(k,o)的距离的最小值作为s与o的距离。

然后,在找U中权值最小的顶点k1,这个时候U中与s的所有可达点就相当于s的直连点。按照上面的推论,s与k1的最短路径即为U中的最小权值。

重复以上步骤,即可找出所有顶点与s的最短距离。

3.Dijkstra算法的实现

public int[] dijkstra(E e) {
    if (Tool.isEmpty(vertices)) {
        return null;
    }
    int size = vertices.length;
    //e的下标
    int index = getIndex(e);
    //已求出最短路径的顶点
    List<Integer> S = new ArrayList<>();
    S.add(index);
    //e与其他顶点的最短距离
    int[] U = Arrays.copyOf(matrix[index], size);
    for (int k = 0; k < size; k++) {
        //找出最短路径的顶点
        int update = getMin(U, S);
        if (update == -1) {
            break;
        }
        S.add(update);
        //更新U
        for (int i = 0; i < U.length; i++) {
            int weight = U[i];
            int newWeight = U[update] + matrix[update][i];
            if (weight > 0 && !S.contains(i) && newWeight < weight) {
                U[i] = newWeight;
            }
        }
    }
    return U;
}

最后

代码地址:https://gitee.com/yanhuo2008/Common/blob/master/Tool/src/main/java/gsw/tool/datastructure/graph/Graph.java

数据结构与算法专题:https://www.jianshu.com/nb/25128590

喜欢请点赞,谢谢!

    原文作者:最爱的火
    原文地址: https://www.jianshu.com/p/27cbc1fe14fd
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞