数据结构与算法-进阶(六)图的接口实现

59 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第17天,点击查看活动详情

摘要

本期通过实现图的接口函数,来进一步理解在代码中,图的结构是如何形成的,比如怎么定义边、如何定义顶点、如何定义边与顶点的连接关系等等。

上期介绍了图的实现方案,以及图的接口,顶点的结构类,边的结构类等。本期继续实现图的接口。

准备

首先创建一个图的类,定义顶点和边的类放在图的类中,还要定义存储顶点和边的属性。这里使用 HashMap 存储顶点,可以快速找到某一个顶点,使用 HasSet 存储边,保证存储的边不会重复。如下代码:

public class ListGraph<V, E> {
    // 顶点的类
    private static class Vertex<V, E> {......}
    // 边的类
    private static class Edge<V, E> {......}
    // 存储顶点的属性
    private Map<V, Vertex<V, E>> vertices = new HashMap<>();
    // 存储边的属性
    private Set<Edge<V, E>> edges = new HashSet<>();
}

代码中定义顶点和边的类上期的文章中已经存在,这里就不再重复展示。

顶点/边的数量

图的类中,已经分别定义了存储顶点和边的属性变量,所以获取顶点或者边的数量可以直接转换到获取对应变量的 size 就可以了。

// 边的数量
public int edgesSize() {
    return edges.size();
}
// 顶点的数量
public int verticesSize() {
    return vertices.size();
}

添加顶点

在添加一个顶点前,需要先判断要添加的顶点是否已经在顶点集合中,如果不存在,才能添加到顶点集合中。因为 Map 中已经有判断某个元素是否已经在集合中的方法,所以不需要自定义实现,代码如下:

public void addVertex(V v) {
    if (vertices.containsKey(v)) return;
    Vertex<V, E> vertex = new Vertex<>(v);
    vertices.put(v, vertex);
}

添加边

添加边的逻辑比添加顶点要多一些,首先要确定边的起始顶点(from)和结束顶点(to)。然后要将没有存储到顶点集合中的起始顶点/结束顶点放到顶点集合中。最后将要添加的边的实例分别放到起始顶点的出度集合和结束顶点的进度集合中,也不要忘记把边放到存储边的集合中。

边放到顶点中的策略是覆盖,即若顶点中已经存在该边,就先移除它,然后再添加新的边。这里可以使用 HashSet 的 remove 函数中返回的 bool 值,来判断起始顶点的边集合是否存在,进而做是否要删除结束顶点的边集合(这个点很巧妙)。代码如下:

public void addEdge(V from, V to, E weight) {
    Vertex<V, E> fromVertex = vertices.get(from);
    if (fromVertex == null) {
        fromVertex = new Vertex<>(from);
        vertices.put(from, fromVertex);
    }

    Vertex<V, E> toVertex = vertices.get(to);
    if (toVertex == null) {
        toVertex = new Vertex<>(to);
        vertices.put(to, toVertex);
    }

    Edge<V, E> edge = new Edge<>(fromVertex, toVertex);
    edge.weight = weight;
    if (fromVertex.outEdges.remove(edge)) {
        toVertex.inEdges.remove(edge);
        edges.remove(edge);
    }
    fromVertex.outEdges.add(edge);
    toVertex.inEdges.add(edge);
    edges.add(edge);
}

代码中可以看到要传入的参数还有 weight,那么如果边是没有权值,就传入 null。比如添加没有权值的边,可以使用下面的函数:

public void addEdge(V from, V to) {
    addEdge(from, to, null);        
}

移除顶点

删除顶点时,除了要删除这个顶点,也要删除顶点连接的边。大致逻辑是:

  1. 要先将其移除出顶点集合;
  2. 然后遍历顶点出度的边,找到边连接的另外一个顶点,在这个顶点的入度集合也删除这个边,最后在图中的边集合中也删除这个边。
  3. 遍历顶点入度的边也是这样的处理。

具体代码如下:

public void removeVertex(V v) {
    // 删除顶点,通过 remove 机制处理
    Vertex<V, E> vertex = vertices.remove(v);
    if (vertex == null) return;

    // 使用迭代器的原因:在遍历的过程中也会存在删除数组中的元素
    // 顶点存在,且也删除,下面就是删除边 -- 使用迭代器
    for (Iterator<Edge<V, E>> iterator = vertex.outEdges.iterator(); iterator.hasNext();) {
        Edge<V, E> edge = (Edge<V, E>) iterator.next();
        edge.to.inEdges.remove(edge);
        // 删除 vertex.outEdges 中的边
        iterator.remove();
        edges.remove(edge);
    }
    for (Iterator<Edge<V, E>> iterator = vertex.inEdges.iterator(); iterator.hasNext();) {
        Edge<V, E> edge = (Edge<V, E>) iterator.next();
        edge.to.outEdges.remove(edge);
        // 删除 vertex.outEdges 中的边
        iterator.remove();
        edges.remove(edge);
    }

}

代码中使用迭代器遍历边,是因为在遍历的过程中也可能要删除边,遍历数组过程中要删除元素的场景都可以用迭代器实现。

移除边

删除边的逻辑就相对来说简单很多。先找到被边连接的顶点(包括起始顶点和结束顶点),然后分别将起始顶点的出度边集合中的边和结束顶点的进度边集合中的边移除,最后把图中存储边集合的边移除。这样就大功告成,代码如下:

public void removeEdge(V from, V to) {
    Vertex<V, E> fromeVertex = vertices.get(from);
    if (fromeVertex == null) return;
    Vertex<V, E> toVertex = vertices.get(to);
    if (toVertex == null) return;

    Edge<V, E> edge = new Edge<>(fromeVertex, toVertex);
    if (fromeVertex.outEdges.remove(edge)) {
        toVertex.inEdges.remove(edge);
        edges.remove(edge);
    }
}

代码中会先判断边连接的顶点是否存在,如果其中一个顶点不存在,就认为该边也是不存在的,也就没有必要执行移除边的操作了。

总结

  • 本期实现图结构的接口方法;
  • 删除边利用了函数中参数存在就返回 true 的方式来处理,是一个很巧妙的点;
  • 顶点中定义两个边集合分别存储出度和入度的边;
  • 一个边若存在,就一定有两个顶点。但是一个顶点存在,不一定会有边。