作为外行人的快递小哥都已经把图这个数据结构整的明明白白的了,你还敢说不会?

“您好,我是XX快递,您有一个包裹等待签收”,快递员总是会给我们带来惊喜。
敬业的快递小哥将包裹安全送达到你的手中,然后启程去送下一份包裹,每一天都走遍无数的大街小巷。

忘忧今天与大家聊的话题,就是快递员走过的路。

什么是图

在数据结构中,树是一种一对多(节点与节点)的非线性数据结构,节点间有明确的层级关系,而图则是一种多对多(顶点顶点)的非线性数据结构,顶点之间不存在父子关系。

有向图和无向图

图按照顶点之间的连通性,可分为有向图和无向图。

  • 无向图:指的是顶点之间的连接没有方向,如快递小哥的路线图中,快递小哥既可以从分治小区走向A*小区,同样又可以从A*小区走向分治小区。
  • 有向图:假设快递小哥从队列公寓走向栈公寓的路是单向通道,只允许从队列公寓走向栈公寓,但不允许从栈公寓返回队列公寓,此时的这个路线图就是有向的。

相关术语

  • 顶点: 类似于树中的节点,在图中,每一个元素都被称之为顶点。
  • 边:顶点与顶点之间的关联关系称之为边。在一个有n个顶点的图中,边的数量一定大于等于n-1。

图的存储

图的存储,有比较常用的两种方式,第一种是邻接矩阵法,他用一个n*n(顶点个数)的矩阵来表示每两个顶点间的关系,具体表示方式如下图所示:

邻接矩阵中,用[A][B] = 1来表示A可以通向B,用[A][B] = 0来表示A不可以通向B;在无向图中,A和B互通,则矩阵的[A][B]位置和[B][A]位置均为1,在无向图中,如果A可以到达B,但是B不可以到达A,则[A][B] = 1, [B][A] = 0;仔细观察可以发现,无向图的矩阵是延着自左上到右下的对角线对称的。
邻接矩阵表示法固然可以清晰表达每个顶点之间的关系,但是却存在一个问题,当顶点之间存在的边相对较少时,邻接矩阵表示法太过浪费空间,尤其是用来表示无向图的时候,因为无向图使用邻接矩阵表示法总是对称的,所以浪费空间的情况更严重。
那么有没有什么方式能够减少空间的使用呢?
我们来看一下第二种表示方式——邻接表法,其表示方法如下图所示,n个顶点的图,使用n个链表来标示,每个链表的头节点标示图中的其中一个顶点,头节点之后的所有后置节点均标示这个顶点可以到达的其他顶点。

如此以来,便大大的节省了存储空间,如果我打算查找A直接到达的顶点,只需要取出头节点为A的链表,然后依次取出其后置节点即可,但是对于有向图,如果需要找到哪些顶点可直达顶点A则需要将所有链表全部遍历,然后过滤出后置节点包含A的链表。
为此,可以引入逆邻接表的方式,链表的后置节点不再保存该顶点可到达的顶点了,而是保存可到达该顶点的顶点。具体使用邻接表还是逆邻接表的方式进行存储,要结合具体的场景来选择。

图的遍历

图的遍历方式有常用的两种方式,深度优先遍历(DFS)和广度优先遍历(BFS)。

深度优先遍历

深度优先遍历的核心就是,选定一个顶点后,不管这个顶点有多少可以直达的其他顶点,先只选择其中的往下走,直到走到无路可走之后,再回退回来选择其他顶点。
如下图所示的快递员的派件过程,就是深度优先遍历的过程

深度优先遍历的代码实现:

/**
 * 启动深度优先遍历
 * @param graph 图的信息
 * @param start 开始的顶点
 */
public static void startDfs(Map<String, List<String>> graph, String start){
    //常见一个栈,用于存放当前路径,用于无路可走时的回退
    Stack<String> stack = new Stack<>();
    stack.push(start);
    //创建一个集合,用于存放已走过的顶点
    List<String> closed = new LinkedList<>();
    //递归遍历
    dfs(graph, stack, closed);
}

/**
 * 递归深度优先遍历
 * @param graph 图信息
 * @param stack 当前路径,用于回退
 * @param closed 已经访问过的顶点
 */
public static void dfs(Map<String, List<String>> graph, Stack<String> stack, List<String> closed){
    //递归出口,当前已无退路,标示已经回退到了起始顶点,遍历结束
    if(stack.empty()){
        return;
    }
    //窥探当前栈顶元素,即当前所处的顶点
    String point = stack.peek();
    //输出当前顶点
    System.out.print(point);
    //将当前顶点添加到已访问过的集合里
    closed.add(point);
    //获取当前顶点可到达的顶点
    List<String> directAccessPoints = graph.get(point);
    if(directAccessPoints != null){
        directAccessPoints.stream()
                //过滤掉已经访问过的顶点
                .filter(item -> !closed.contains(item))
                //遍历剩余的顶点,对剩余的顶点进行深度优先遍历
                .forEach(item -> {
                    stack.push(item);
                    dfs(graph, stack, closed);
        });
    }
    //当前顶点已无路可去,弹栈回退
    stack.pop();
}

广度优先遍历

广度优先遍历的核心理念在于,先找到一个顶点,遍历他的所有可直达的顶点,然后对他的所有可直达的顶点再做广度优先遍历。

广度优先遍历代码实现:

/**
 * 启动广度优先遍历
 * @param graph 图的信息
 * @param start 开始的顶点
 */
public static void startBfs(Map<String, List<String>> graph, String start){
    //创建一个集合,用于存放已走过的顶点
    List<String> closed = new LinkedList<>();
    //输出当前顶点
    System.out.print(start);
    //将当前节点增加到已访问列表
    closed.add(start);
    //递归遍历
    bfs(graph, start, closed);
}

/**
 * 递归广度优先遍历
 * @param graph 图信息
 * @param point 当前顶点
 * @param closed 已经访问过的顶点
 */
public static void bfs(Map<String, List<String>> graph, String point, List<String> closed){
    //获取当前顶点可到达的顶点
    List<String> directAccessPoints = graph.get(point);
    if(directAccessPoints != null){
        //过滤掉已经访问过的顶点
        List<String> filter = directAccessPoints.stream()
                .filter(item -> !closed.contains(item)).collect(Collectors.toList());
        //输出未访问过的直连顶点,并将其添加到访问列表
        filter.forEach(item -> {
            System.out.print(item);
            closed.add(item);
        });
        filter.forEach(item -> {
            bfs(graph, item, closed);
        });
    }
}

拓展

  • 加权图
  • 最小生成树、最大生成树

关于图的生成树的问题,后续会在解算法题遇到的时候再做讲解,本篇暂不涉及最大最小生成树

PS: 数据结构基础到此先告一段落,接下来将开启算法篇,通过实战算法来巩固所学的知识,也会通过算法对这些数据结构做一个更深层次的理解,但是在开始之前,忘忧期望大家能够把之前所讲的基础知识掌握牢固,这是后续解题的关键所在。

第一个青春是上帝给的,第二个青春是靠自己努力的。

发布了13 篇原创文章 · 获赞 186 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/u013054715/article/details/104913126