数据结构与算法(六):图结构

news2024/11/14 20:25:33

图是一种比线性表和树更复杂的数据结构,在图中,结点之间的关系是任意的,任意两个数据元素之间都可能相关。图是一种多对多的数据结构。

一、基本概念

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

注意:线性表中可以没有元素,称为空表。树中可以没有结点,叫做空树。但是在图中不允许没有顶点,可以没有边。

基本术语:

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

  • 无向图(Undirected graphs):图中任意两个顶点的边都是无向边。

  • 有向边:若从顶点Vi到Vj的边有方向,称这条边为有向边,也称为弧(Arc),用<Vi, Vj>来表示,其中Vi称为弧尾(Tail),Vj称为弧头(Head)。

  • 有向图(Directed graphs):图中任意两个顶点的边都是有向边。

  • 简单图:不存在自环(顶点到其自身的边)和重边(完全相同的边)的图

在这里插入图片描述

  • 无向完全图:无向图中,任意两个顶点之间都存在边。

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

  • 稀疏图;有很少条边或弧的图称为稀疏图,反之称为稠密图。

  • 权(Weight):表示从图中一个顶点到另一个顶点的距离或耗费。

  • 网:带有权重的图

  • 度:与特定顶点相连接的边数;

  • 出度、入度:有向图中的概念,出度表示以此顶点为起点的边的数目,入度表示以此顶点为终点的边的数目;

  • 环:第一个顶点和最后一个顶点相同的路径;

  • 简单环:除去第一个顶点和最后一个顶点后没有重复顶点的环;

  • 连通图:任意两个顶点都相互连通的图;

  • 极大连通子图:包含竟可能多的顶点(必须是连通的),即找不到另外一个顶点,使得此顶点能够连接到此极大连通子图的任意一个顶点;

  • 连通分量:极大连通子图的数量;

  • 强连通图:此为有向图的概念,表示任意两个顶点a,b,使得a能够连接到b,b也能连接到a 的图;

  • 生成树:n个顶点,n-1条边,并且保证n个顶点相互连通(不存在环);

  • 最小生成树:此生成树的边的权重之和是所有生成树中最小的;

  • AOV网(Activity On Vertex Network ):在有向图中若以顶点表示活动,有向边表示活动之间的先后关系

  • AOE网(Activity On Edge Network):在带权有向图中若以顶点表示事件,有向边表示活动,边上的权值表示该活动持续的时间

二、图的存储结构

由于图的结构比较复杂,任意两个顶点之间都可能存在关系,因此用简单的顺序存储来表示图是不可能,而若使用多重链表的方式(即一个数据域多个指针域的结点来表示),这将会出现严重的空间浪费或操作不便。这里总结一下常用的表示图的方法:

1、邻接矩阵

图的邻接矩阵(Adjacency Matrix)存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称邻接矩阵)存储图中的边或弧的信息。

在这里插入图片描述

无向图由于边不区分方向,所以其邻接矩阵是一个对称矩阵。邻接矩阵中的0表示边不存在,主对角线全为0表示图中不存在自环。

带权有向图的邻接矩阵:

在这里插入图片描述

在带权有向图的邻接矩阵中,数字表示权值weight,「无穷」表示弧不存在。由于权值可能为0,所以不能像在无向图的邻接矩阵中那样使用0来表示弧不存在。

代码:

/**
 * 有向图的邻接矩阵实现
 */
public class Digraph {
    private int vertexsNum;
    private int edgesNum;
    private int[][] arc;

    public Digraph(int[][] data, int vertexsNum) {
        this.vertexsNum = vertexsNum;
        this.edgesNum = data.length;
        arc = new int[vertexsNum][vertexsNum];
        for (int i = 0; i < vertexsNum; i++) {
            for (int j = 0; j < vertexsNum; j++) {
                arc[i][j] = Integer.MAX_VALUE;
            }
        }
        
        for (int i = 0; i < data.length; i++) {
            int tail = data[i][0];
            int head = data[i][1];
            arc[tail][head] = 1;
        }
    }
    
    //用于测试,返回一个顶点的邻接点
    public Iterable<Integer> adj(int vertex) {
        Set<Integer> set = new HashSet<>();
        for (int i = 0; i < vertexsNum; i++) {
            if (arc[vertex][i] != Integer.MAX_VALUE)
                set.add(i);
        }
        return set;
    }
    
    public static void main(String[] args) {
        int[][] data = {
                {0,3},
                {1,0},
                {1,2},
                {2,0},
                {2,1},
        };
        Digraph wd = new Digraph(data,4);
        for(int i :wd.adj(1)) {
            System.out.println(i);
        }   
    }
}

优缺点:

  • 优点:结构简单,操作方便
  • 缺点:对于稀疏图,这种实现方式将浪费大量的空间。

2、邻接表

邻接表是一种将数组与链表相结合的存储方法。其具体实现为:将图中顶点用一个一维数组存储,每个顶点Vi的所有邻接点用一个单链表来存储。这种方式和树结构中孩子表示法一样。

对于有向图其邻接表结构如下:

在这里插入图片描述

有向图的邻接表是以顶点为弧尾来存储边表的,这样很容易求一个顶点的出度(顶点对应单链表的长度),但若求一个顶点的入度,则需遍历整个图才行。这时可以建立一个有向图的逆邻接表即对每个顶点v都建立一个弧头尾v的单链表。如上图所示。

代码:

/**
 * 有向图的邻接表实现
 *
 */
public class AdjListDigraph {
    
    private class EdgeNode {
        int index;
        EdgeNode next;
        EdgeNode(int index, EdgeNode next){
            this.index = index;
            this.next = next;
        }
    }
    
    private class VertexNode {
        int id;
        EdgeNode headNode;
    }
    
    private VertexNode[] vertexs;
    private int vertexsNum;
    private int edgesNum;
    
    public AdjListDigraph(int[][] data, int vertexsNum) {
        this.vertexsNum = vertexsNum;
        this.edgesNum = data.length;
        vertexs = new VertexNode[vertexsNum];
        for (int i = 0; i < vertexs.length; i++) {
            vertexs[i] = new VertexNode();
            vertexs[i].id = i;        //
        }
        
        for (int i = 0; i < data.length; i++) {
            int index = data[i][1];
            EdgeNode next = vertexs[data[i][0]].headNode;
            EdgeNode eNode = new EdgeNode(index,next);
            vertexs[data[i][0]].headNode = eNode; //头插法
        }
        
    }
    
    //用于测试,返回一个顶点的邻接点
    public Iterable<Integer> adj(int index) {
        Set<Integer> set = new HashSet<>();
        EdgeNode current = vertexs[index].headNode;
        while(current != null) {
            VertexNode node = vertexs[current.index];
            set.add(node.id);
            current = current.next;
        }
        return set;
    }
    
    public static void main(String[] args) {
        int[][] data = {
                {0,3},
                {1,0},
                {1,2},
                {2,0},
                {2,1},
        };
        AdjListDigraph ald = new AdjListDigraph(data,4);
        for(int i :ald.adj(1)) {
            System.out.println(i);
        }   
    }
}

本算法的时间复杂度为 O(N + E),其中N、E分别为顶点数和边数,邻接表实现比较适合表示稀疏图。

3、十字链表

十字链表(Orthogonal List)是将邻接表和逆邻接表相结合的存储方法,它解决了邻接表(或逆邻接表)的缺陷,即求入度(或出度)时必须遍历整个图。

十字链表的结构如下:

在这里插入图片描述

图中:

  • firstIn表示入边表(即是逆邻接表中的单链表)头指针,firstOut表示出边表(即是邻接表中的单链表)头指针,data表示顶点数据。
  • tailVex表示边的起点在顶点数组中的下标,tailNext值出边表指针域,指向起点相同的下一条边。
  • headVex表示边的终点在顶点数组中的下标,headNext指入边表指针域,指向终点相同的下一条边。

代码实现:

/**
 * 有向图的十字链表实现
 *
 */
public class OrthogonalList {
    
    private class EdgeNode {
        int tailVex;
        int headVex;
        EdgeNode headNext;
        EdgeNode tailNext;
        
        public EdgeNode(int tailVex, int headVex, EdgeNode headNext, EdgeNode tailNext) {
            super();
            this.tailVex = tailVex;
            this.headVex = headVex;
            this.headNext = headNext;
            this.tailNext = tailNext;
        }
        
    }
    
    private class VertexNode {
        int data;
        EdgeNode firstIn;
        EdgeNode firstOut;
    }
    
    private VertexNode[] vertexs;
    private int vertexsNum;
    private int edgesNum;
    
    public OrthogonalList(int[][] data, int vertexsNum) {
        this.vertexsNum = vertexsNum;
        this.edgesNum = data.length;
        vertexs = new VertexNode[vertexsNum];
        for (int i = 0; i < vertexs.length; i++) {
            vertexs[i] = new VertexNode();
            vertexs[i].data = i;        //
        }
        
        //关键
        for (int i = 0; i < data.length; i++) {
            int tail = data[i][0];
            int head = data[i][1];
            EdgeNode out = vertexs[tail].firstOut;
            EdgeNode in = vertexs[head].firstIn;
            EdgeNode eNode = new EdgeNode(tail,head,in,out);
            vertexs[tail].firstOut = eNode;
            vertexs[head].firstIn = eNode;
        }
        
    }
    
    //返回一个顶点的出度
    public int outDegree(int index) {
        int result = 0;
        EdgeNode current = vertexs[index].firstOut;
        while(current != null) {
            current = current.tailNext;
            result++;
        }
        return result;
    }
    
    //返回一个顶点的入度
    public int inDegree(int index) {
        int result = 0;
        EdgeNode current = vertexs[index].firstIn;
        while(current != null) {
            current = current.headNext;
            result++;
        }
        return result;
    }
    
    public static void main(String[] args) {
        int[][] data = {
                {0,3},
                {1,0},
                {1,2},
                {2,0},
                {2,1},
        };
        OrthogonalList orth = new OrthogonalList(data,4);
        System.out.println("顶点1的出度为" + orth.outDegree(1));
        System.out.println("顶点1的入度为" + orth.inDegree(1));
            
    }
}

十字链表创建图算法的时间复杂度和邻接表相同都为O(N + E)。在有图的应用中推荐使用。

三、图的遍历

从图的某个顶点出发,遍历图中其余顶点,且使每个顶点仅被访问一次,这个过程叫做图的遍历(Traversing Graph)。对于图的遍历通常有两种方法:深度优先遍历和广度优先遍历。

1、深度优先遍历

深度优先遍历(Depth First Search,简称DFS),也成为深度优先搜索。

遍历思想:基本思想:首先从图中某个顶点v0出发,访问此顶点,然后依次从v相邻的顶点出发深度优先遍历,直至图中所有与v路径相通的顶点都被访问了;若此时尚有顶点未被访问,则从中选一个顶点作为起始点,重复上述过程,直到所有的顶点都被访问。

深度优先遍历用递归实现比较简单,只需用一个递归方法来遍历所有顶点,在访问某一个顶点时:

  • 将它标为已访问
  • 递归的访问它的所有未被标记过的邻接点

深度优先遍历的过程:

在这里插入图片描述

代码如下:

public class DFSTraverse {
    
    private boolean[] visited;
    
    //从顶点index开始遍历
    public DFSTraverse(Digraph graph, int index) {
        visited = new boolean[graph.getVertexsNum()];
        dfs(graph,index);
    }

    private void dfs(Digraph graph, int index) {
        visited[index] = true;
        for(int i : graph.adj(index)) {
            if(!visited[i])
                dfs(graph,i);   
        }
    }
}

2、广度优先遍历

广度优先遍历(Breadth First Search,简称BFS),又称为广度优先搜索

遍历思想:首先,从图的某个顶点v0出发,访问了v0之后,依次访问与v0相邻的未被访问的顶点,然后分别从这些顶点出发,广度优先遍历,直至所有的顶点都被访问完。

广度优先遍历的过程:

代码:

在这里插入图片描述

public class BFSTraverse {
    
    private boolean[] visited;
    
    public BFSTraverse(AdjListDigraph graph, int index) {
        visited = new boolean[graph.getVertexsNum()];
        bfs(graph,index);
    }

    private void bfs(AdjListDigraph graph, int index) {
        //在JSE中LinkedList实现了Queue接口
        Queue<Integer> queue = new LinkedList<>();
        visited[index] = true;
        queue.add(index);
        while(!queue.isEmpty()) {
            int vertex = queue.poll();
            for(int i : graph.adj(vertex)) {
                if(!visited[i]) {
                    visited[i] = true;
                    queue.offer(i);
                }
            }
        }
    }
}

四、最小生成树

图的生成树是它的一棵含有所有顶点的无环连通子图。一棵加权图的最小生成树(MST)是它的一棵权值(所有边的权值之和)最小的生成树。

计算最小生成树可能遇到的情况:

  • 非连通的无向图,不存在最小生成树
  • 权重不一定和距离成正比
  • 权重可能是0或负数
  • 若存在相等的权重,那么最小生成树可能不唯一

图的切分是将图的所有顶点分为两个非空且不重叠的两个集合。横切边是一条连接两个属于不同集合的顶点的边。

切分定理:在一幅加权图中,给定任意的切分,它的横切边中的权重最小者必然属于图的最小生成树。

切分定理是解决最小生成树问题的所有算法的基础。这些算法都是贪心算法。

在这里插入图片描述

首先先构造一个带权的无向图,其代码如下:

//定义边
public class Edge implements Comparable<Edge>{
    private final int ver1;
    private final int ver2;
    private final Integer weight;
    public Edge(int ver1, int ver2, int weight) {
        super();
        this.ver1 = ver1;
        this.ver2 = ver2;
        this.weight = weight;
    }
    //返回一个顶点
    public int either() {
        return ver1;
    }
    //返回另一个顶点
    public int other(int vertex) {
        if (vertex == ver1)
            return ver2;
        else if(vertex == ver2)
            return ver1;
        else 
            throw new RuntimeException("边不一致");
    }
    @Override
    public int compareTo(Edge e) {
        return this.weight.compareTo(e.weight);
    }
    
    public Integer getWeight() {
        return weight;
    }
    @Override
    public String toString() {
        return "Edge [" + ver1 + "," + ver2 +"]";
    }
}

/**
 * 带权无向图的实现
 */
public class WeightedGraph {
    private final int vertexsNum;
    private final int edgesNum;
    private List<Edge>[] adj;
    
    public WeightedGraph(int[][] data, int vertexsNum) {
        this.vertexsNum = vertexsNum;
        this.edgesNum = data.length;
        adj  = (List<Edge>[]) new ArrayList[vertexsNum];
        for(int i=0; i<vertexsNum; i++) {
            adj[i] = new ArrayList<>();
        }

        for (int i = 0; i < data.length; i++) {
            Edge edge = new Edge(data[i][0],data[i][1],data[i][2]);
            int v = edge.either();
            adj[v].add(edge);
            adj[edge.other(v)].add(edge);
        }
    }
    
    public Iterable<Edge> adj(int vertex) {
        return adj[vertex];
    }

    public int getVertexsNum() {
        return vertexsNum;
    }

    public int getEdgesNum() {
        return edgesNum;
    }
    
    public Iterable<Edge> getEdges() {
        List<Edge> edges = new ArrayList<>();
        for(int i=0; i<vertexsNum; i++) {
            for(Edge e : adj[i]) {
                if(i > e.other(i)) { //无向图,防止将一条边加入两次
                    edges.add(e);
                }
            }
        }
        return edges;
    }
}

1、Prim算法

每次将权值最小的横切边加入生成树中

1)、Prim算法的延迟实现

实现过程如下图:

在这里插入图片描述

从顶点0开始,首先将顶点0加入到树中(标记),顶点0和其它点的横切边(这里即为顶点0的邻接边)加入优先队列,将权值最小的横切边出队,加入生成树中。此时相当于也向树中添加了一个顶点2,接着将集合(顶点1,2组成)和另一个集合(除1,2的顶点组成)间的横切边加入到优先队列中,如此这般,直到队列为空。

注意:若横切边中另一个顶点在树中,则此边失效。

代码如下:

public class LazyPrimMST {
    private boolean[] visited; //标记顶点
    private LinkedQueue<Edge> mst; //存储最小生成树的边
    private MinPQ<Edge> pq; //优先队列,权值越最小优先级越高
    
    public LazyPrimMST(WeightedGraph wg) {
        visited = new boolean[wg.getVertexsNum()];
        mst = new LinkedQueue<Edge>();
        pq = new MinPQ<>(wg.getVertexsNum());
        
        visit(wg, 0); //从0点开始
        while(!pq.isEmpty()) {
            Edge e = pq.deQueue();
            int ver1 = e.either();
            int ver2 = e.other(ver1);
            if(visited[ver1] && visited[ver2]) {
                continue; //边失效
            }
            mst.enQueue(e);
            if(!visited[ver1])
                visit(wg, ver1);
            if(!visited[ver2])
                visit(wg, ver2);
        }
    }

    private void visit(WeightedGraph wg, int ver) {
        visited[ver] = true; //标记顶点
        for(Edge e : wg.adj(ver)) {
            if(!visited[e.other(ver)])
                pq.enQueue(e);
        }
    }
    
    public Iterable<Edge> getMST() {
        return mst;
    }
    
    public static void main(String[] args) {
        int[][] data = {
                {0, 2, 2},
                {0, 1, 4},
                {0, 5, 5},
                {1, 2, 3},
                {1, 5, 11},
                {1, 3, 7},
                {2, 3, 8},
                {2, 4, 10},
                {3, 5, 6},
                {3, 4, 1},
                {4, 5, 9}
        };
        WeightedGraph wg = new WeightedGraph(data,6);
        LazyPrimMST lpm = new LazyPrimMST(wg);
        for(Edge e : lpm.getMST()) {
            System.out.println(e);
        }
    }
}

其中,LinkedQueue类的代码在《数据结构与算法(三),栈与队列》中;而MinPQ类的代码与《数据结构与算法(五),优先队列》中MaxPQ类的代码几乎一样,只需将方法less中的小于号改为大于号即可。这里就不在给出代码了

此方法的时间复杂度为 O(ElogE),空间复杂度为 O(E)。其中,V为顶点个数,E为边数

2)、Prim算法即时实现

基于Prim算法的延迟实现,我们可以在优先队列中只保存每个非树顶点V的一条边(即它与树中的顶点连接起来的权重最小的那条边),因为其他权重较大的边迟早都会失效。

实现过程如下图:

在这里插入图片描述

代码实现:

/**
 * prim的即时实现
 */
public class PrimMST {
    private Edge[] edgeTo; //点离生成树最近的边
    private int[] distTo; //点到生成树的距离
    private boolean[] visited;
    private IndexMinPQ<Integer> pq; //索引优先队列,关联顶点与distTo
    
    public PrimMST(WeightedGraph wg) {
        //初始化
        edgeTo = new Edge[wg.getVertexsNum()];
        distTo = new int[wg.getVertexsNum()];
        visited = new boolean[wg.getVertexsNum()];
        for(int i=0; i<wg.getVertexsNum(); i++) {
            distTo[i] = Integer.MAX_VALUE;
        }
        pq = new IndexMinPQ<>(wg.getVertexsNum());
        distTo[0] = 0;
        pq.insert(0, 0);
        
        while(!pq.isEmpty()) {
            visit(wg, pq.delMin());
        }
    }

    private void visit(WeightedGraph wg, int ver) {
        visited[ver] = true;
        for(Edge e : wg.adj(ver)) {
            int vertex = e.other(ver); //边的另一个点
            if(visited[vertex])
                continue;
            if(e.getWeight() < distTo[vertex]) {
                edgeTo[vertex] = e; //被覆盖的边失效
                distTo[vertex] = e.getWeight();
                if(pq.contains(vertex)) {
                    pq.change(vertex, distTo[vertex]); 
                }else {
                    pq.insert(vertex, distTo[vertex]);
                }
            }
        }
    }
    
    public Iterable<Edge> getMST() {
        return Arrays.asList(edgeTo);
    }
}

此方法的时间复杂度为 O(ElogV),空间复杂度为 O(V)。其中,V为顶点个数,E为边数。

可以看出Prim算法的即时实现比延迟实现明显要快,特别是对于稠密矩阵(E>>>V)的情况。

2、Kruskal算法

Kruskal算法的思想是按照边的权重顺序来生成最小生成树,首先将图中所有边加入优先队列,将权重最小的边出队加入最小生成树,保证加入的边不与已经加入的边形成环,直到树中有V-1到边为止。

实现过程如下图:

在这里插入图片描述

/**
 * Kruskal算法的实现
 */
public class KruskalMST {
    private List<Edge> mst; //存储最小生成树的边
    private MinPQ<Edge> pq; //优先队列
    private int[] parent; //用来判断边与边是否形成回路
    
    public KruskalMST(WeightedGraph wg) {
        mst = new ArrayList<Edge>();
        pq = new MinPQ<>(wg.getEdgesNum());
        parent = new int[wg.getVertexsNum()];
        for(Edge e : wg.getEdges()) {
            pq.enQueue(e);
        }
        //最小生成树的边最多为V-1个
        while(!pq.isEmpty() && mst.size() < wg.getVertexsNum() - 1) {
            Edge e = pq.deQueue();
            int v = e.either();
            int n = find(parent, v);
            int m = find(parent, e.other(v));
            if(n != m) { //表示此边没有与生成树形成环路
                parent[n] = m;
                mst.add(e);
            }
        }
    }
    
    //查找连接树的尾部下标
    private int find(int[] data, int v) {
        while(parent[v] > 0) {
            v = parent[v];
        }
        return v;
    }
    
    public Iterable<Edge> getMST() {
        return mst;
    } 
}

Kruskal算法的时间复杂度最坏情况下为O(ElogE)。空间复杂度为O(E)。

对比Prim算法和Kruskal算法,Kruskal算法主要根据边来生成树,边数少时效率比较高,适合稀疏图;而Prim算法对边数多的稠密图效果更好一些。

五、最短路径

最短路径指两顶点之间经过的边上权值之和最少的路径,并且称路径上的第一个顶点为源点,最后一个顶点为终点。

为了操作方便,首先使用面向对象的方法,来实现一个加权的有向图,其代码如下:

/**
 * 有向边
 */
public class Edge{
    private final int from;
    private final int to;
    private final int weight;
    public Edge(int from, int to, int weight) {
        super();
        this.from = from;
        this.to = to;
        this.weight = weight;
    }
    
    public int getFrom() {
        return from;
    }
    
    public int getTo() {
        return to;
    }
    
    public int getWeight() {
        return weight;
    }
}

//带权有向图的实现
public class WeightedDigraph {
    private final int vertexsNum;
    private final int edgesNum;
    private List<Edge>[] adj; //邻接表
    
    public WeightedDigraph(int[][] data, int vertexsNum) {
        this.vertexsNum = vertexsNum;
        this.edgesNum = data.length;
        adj  = (List<Edge>[]) new ArrayList[vertexsNum];
        for(int i=0; i<vertexsNum; i++) {
            adj[i] = new ArrayList<>();
        }

        for (int i = 0; i < data.length; i++) {
            Edge edge = new Edge(data[i][0],data[i][1],data[i][2]);
            int v = edge.getFrom();
            adj[v].add(edge);
        }
    }
    
    public Iterable<Edge> adj(int vertex) {
        return adj[vertex];
    }

    public int getVertexsNum() {
        return vertexsNum;
    }

    public int getEdgesNum() {
        return edgesNum;
    }
    
    //有向图中所有的边
    public Iterable<Edge> getEdges() {
        List<Edge> edges = new ArrayList<>();
        for(List<Edge> list : adj) {
            for(Edge e : list) {
                edges.add(e);
            }
        }
        return edges;
    }
}

顶点到源点s的最短路径,我们使用一个用顶点索引的Edge数组(edgeTo[])来存储,使用数组distTo[]来存储最短路径树(包含了源点S到所有可达顶点的最短路径)。

边的松弛操作:

边的松弛过程如下图:

在这里插入图片描述

松弛边【1,4】就是检查顶点0到4的最短路径是否是先从顶点0到1,然后在由顶点1到4。如果是则【0,4】边失效,将【1,4】加入最短路径树。

代码:

private void relax(WeightedDigraph wd,Edge e) {
    int v = e.getFrom();
    int w = e.getTo();
    if(distTo[w] > distTo[v] + e.getWeight()) {
        distTo[w] = distTo[v] + e.getWeight();
        edgeTo[w] = e;
    }
}

顶点的松弛操作:

顶点的松弛就是松弛顶点的所有邻接边,这里就不给出过程了,实现代码在Dijkstra实现中。

1、Dijkstra算法

算的的实现过程:

在这里插入图片描述

Dijkstra算法的代码实现:

//Dijkstra算法的实现
public class Dijkstra {
    private Edge[] edgeTo; //最短路径树
    private int[] distTo; //存储每个顶点到源点的距离
    //索引优先队列,建立distTo和顶点索引,distTo越小,优先级越高
    private IndexMinPQ<Integer> pq; 
    
    public Dijkstra(WeightedDigraph wd, int s) {
        edgeTo = new Edge[wd.getVertexsNum()];
        distTo = new int[wd.getVertexsNum()];
        pq = new IndexMinPQ<>(wd.getVertexsNum());
        for(int i=0; i<wd.getVertexsNum(); i++) {
            distTo[i] = Integer.MAX_VALUE;
        }
        distTo[s] = 0; //源点s的distTo为0
        pq.insert(s, 0);
        while(pq.isEmpty()) {
            relax(wd, pq.delMin());
        }
    }
    
    //顶点的松弛
    private void relax(WeightedDigraph wd, int ver) {
        for(Edge e : wd.adj(ver)) {
            int v = e.getTo();
            if(distTo[v] > distTo[ver] + e.getWeight()) {
                distTo[v] = distTo[ver] + e.getWeight();
                edgeTo[v] = e;
                if(pq.contains(v)) {
                    pq.change(v, distTo[v]);
                }else {
                    pq.insert(v, distTo[v]);
                }
            }
        }
    }
}

Dijkstra算法的局限性:图中边的权重必须为正,但可以是有环图。时间复杂度为O(ElogV),空间复杂度O(V)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/377175.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

嵌入式小白的进阶之路

学习嵌入式采用的是2021年华清远见嵌入式课程 安装虚拟机 &#xff1a; 懒人版 目标&#xff1a;快速上手 问题一&#xff1a; 在Linux虚拟环境中运行一个c语言程序步骤 1、激活虚拟环境&#xff1a;source venv/bin/activate 2、进入Hello文件夹&#xff1a;cd Hello 3、新…

【Python学习笔记】第二十节 Python 网络编程

Python 网络编程简单来说&#xff0c;网络是用物理链路将各个孤立的工作站或主机相连在一起&#xff0c;组成数据链路&#xff0c;从而达到资源共享和通信的目的。所谓的网络编程就是&#xff0c;让在不同的电脑上的软件能够进行数据传递&#xff0c;即进程之间的通信Python 提…

超级品牌符号怎么设计?大咖有方法

怎么设计超级LOGO图标&#xff1f;有方法&#xff01; LOGO设计大趋势&#xff1a;卡通化、拟人化 抽象符号已经泛滥 但卡通形象也已经泛滥 趣讲大白话&#xff1a;设计容易出名难 【安志强趣讲信息科技89期】 ******************************* 别以为设计一个卡通就牛X闪闪 比…

解析iptables原里及设置规则

文章目录一、前言二、iptables简介三、iptables原理四、iptables设置规则五、 Linux 常用实例六、注意事项1、命令书写规则2、谨慎使用的动作 DROP3、永久生效一、前言 其实在IT行业里了&#xff0c;不仅是专职的运维人员需要了解 iptables&#xff0c;开发、测试等人员在了解…

IIS .Net Core 413错误和Request body too large解决办法

错误描述图片比较大时&#xff0c;在前端上传就报413错误。根本到不了后端。在网上看到这个文章比较有用。https://blog.csdn.net/wstever/article/details/1288707421、修改网站Web.config配置文件加入下面这段配置<?xmlversion"1.0" encoding"utf-8"…

Python 中 openpyxl 模块封装,读写 Excel 文件中自动化测试用例数据

只有测试数据和错误提示信息不同&#xff0c;其他代码都是一样的&#xff0c;不这样不易修改数据和维护&#xff0c;会有两点痛点 1.代码冗余极其严重, 程序可读性不佳 2.程序拓展性很差 往往我们在自动化测试汇总&#xff0c;会将数据放在 Excel 文件、CSV文件、数据库 Py…

JVM 堆内存模型

方法区和永久代的关系 方法区与 Java 堆一样&#xff0c;是各个线程共享的内存区域&#xff0c;它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分&#xff0c;但是它却有一个别名叫做 N…

如何整理自己的前端面试题库

compose 题目描述:实现一个 compose 函数 // 用法如下: function fn1(x) {return x 1; } function fn2(x) {return x 2; } function fn3(x) {return x 3; } function fn4(x) {return x 4; } const a compose(fn1, fn2, fn3, fn4); console.log(a(1)); // 1432111实现代码…

基于Java+SpringBoot+Vue前后端分离书店购书系统设计与实现

博主介绍&#xff1a;✌全网粉丝3W&#xff0c;全栈开发工程师&#xff0c;从事多年软件开发&#xff0c;在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战✌ 博主作品&#xff1a;《微服务实战》专栏是本人的实战经验总结&#xff0c;《Spring家族及…

Tomcat部署及多实例

Tomcat部署及多实例一、Tomcat简介1、Tomcat核心组件2、什么是JSP二、Tomcat数据流向1、Tomcat数据流向2、Tomcat-Nginx数据流向三、Tomcat服务部署安装1、安装jdk包2、解压Tomcat所需的安装包3、在/etc/profile添加环境变量4、启动服务并查看5、在浏览器网页验证6、创建用户&a…

flink常用算子介绍

flink任务中【Transformation 数据转换】是对数据进行操作&#xff0c;有 Map、FlatMap、Filter、KeyBy 、Reduce 、Fold 、Aggregations、Window 、WindowAll 、Union 、Window join 、Split 、Select 、Project 等&#xff0c;通过对数据的操作&#xff0c;转换成想要的数据&…

【设计模式】4.建造者模式

概述 将一个复杂对象的构建与表示分离&#xff0c;使得同样的构建过程可以创建不同的表示。 分离了部件的构造(由Builder来负责)和装配(由Director负责)。 从而可以构造出复杂的对象。这个模式适用于&#xff1a;某个对象的构建过程复杂的情况。由于实现了构建和装配的解耦。不…

【沐风老师】3DMAX一键玻璃门生成器使用教程

3DMAX一键玻璃门生成器使用方法详解 3dMax一键玻璃门生成器是在3dMax中自动创建三维玻璃门模型的高效脚本。有6种风格的玻璃门&#xff0c;它可以在Archviz项目中灵活应用&#xff0c;同时为3D艺术家节省大量时间。 【适用版本】 3dMax 2018.2及更高版本 【安装方法】…

day37|完全背包、518. 零钱兑换 II、377. 组合总和 Ⅳ 复健运动

完全背包 有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i]&#xff0c;得到的价值是value[i] 。每件物品都有无限个&#xff08;也就是可以放入背包多次&#xff09;&#xff0c;求解将哪些物品装入背包里物品价值总和最大。 完全背包和01背包问题唯一不同…

什么是MES系统?本文解释得很清楚了

MES&#xff08;英文Manufacturing Execution System的缩写&#xff09;&#xff0c;即生产执行系统&#xff0c;是近几年发展起来的企业信息化系统&#xff0c;目前在发达国家已经普遍推广。 MES软件是介于ERP&#xff08;企业资源计划系统&#xff09;和自控系统&#xff08…

百度前端训练营

视频&#xff1a;前端训练营开营仪式20220627_哔哩哔哩_bilibili http://bit.baidu.com/productsBuy?id248 一、Git安装与使用【因为重装系统&#xff0c;所以重新装一次环境】 查看电脑32位还是64&#xff0c;安装git TortoiseGit 及TortoiseGit 汉化包 下载64位对应的版本…

Java集合概述(Collection集合)

目录集合一、集合与数组二、集合类体系结构三、泛型(约定集合存储数据类型)四、Collection集合常用API五、Collection集合的遍历方式5.1 迭代器遍历5.2 增强for循环(for each):5.3 lambda表达式六、Collection集合存储自定义类型的对象七、常见数据结构集合 一、集合与数组 数…

【设计模式】9.桥接模式

概述 现在有一个需求&#xff0c;需要创建不同的图形&#xff0c;并且每个图形都有可能会有不同的颜色。我们可以利用继承的方式来设计类的关系&#xff1a; 我们可以发现有很多的类&#xff0c;假如我们再增加一个形状或再增加一种颜色&#xff0c;就需要创建更多的类。 试…

redis 05 篇——主从复制

redis 05 篇——主从复制1. 前言1.1 什么是复制&#xff1f;1.1.1 复制概述1.1.2 主从复制的架构1.2 为什么要使用主从复制&#xff1f;1.3 主从复制主要的命令配置2. 准备工作3. 核心配置3.1 主服务器3.2 从服务器4. 实例演示4.1 简单实例——两台服务器4.1.1 同一服务多个red…

小程序:使用分包异步化解决一个分包引入另一个分包的组件/函数的问题

背景 我们一般使用小程序插件的时候&#xff0c;喜欢将其放在分包中&#xff0c;因为插件体积会打包进主包内&#xff0c;很容易造成主包体积超过 2M 从而无法发布&#xff0c;我们暂且叫这个有插件的分包叫分包P&#xff0c;这时候另外两个业务分包XY&#xff0c;想引入这个分…