图的基本介绍

前面学过的 线性表

  • 线性表:局限于一个 直接前驱 和 一个 直接后继 的关系
  • 树:只能有一个直接前驱(父节点)

当我们需要表示 多对多 的关系时,就需要用到图

lua 定义数据结构 数据结构路径定义_lua 定义数据结构

比如:城市交通图。他就是一个图,对应程序中的图如下所示

lua 定义数据结构 数据结构路径定义_深度优先遍历_02

图是一种 数据结构,其中节点可以具有 零个或多个相邻元素,两个节点之间的链接称为 ,节点页可以称为 顶点。

图的常用概念

lua 定义数据结构 数据结构路径定义_邻接矩阵_03

  • 顶点(vertex)
  • 边(edge)
  • 路径:路径就是一个节点到达另一个节点所经过的节点连线
    D -> C 的路径就有两条(针对无向图来说):
  • D → B → C
  • D → A → B → C
  • 无向图:顶点之间的连接没有方向
    比如上图它是一个 无向图;比如 A-B,可以是 A->B 也可以是 B <- A
  • 有向图:顶点之间的连接是有方向的。
  • 带权图:边有权值时,则叫做带权图,同时也叫
  • lua 定义数据结构 数据结构路径定义_邻接矩阵_04

比如上图中,北京 到 上海这一条边上有一个数值 1463,这个可能是他的距离,这种就叫做 边带权值

图的表示方式

有两种:

  • 二维数组表示:邻接矩阵
  • 链表表示:邻接表

邻接矩阵

邻接矩阵是表示图形中 顶点之间相邻关系 的矩阵,对于 n 个顶点的图而言,矩阵的 row 和 col 表示的是 1....n 个点

邻接表

由于邻接矩阵有一个缺点:它需要为每个顶点都分配 n 个边的空间,其实有很多边都是不存在的(比如上面的 0,0 不链接,也需要表示出来),这 会造成空间的一定损失

邻接表 的实现只关心 存在的边,因此没有空间浪费,由 数组 + 链表组成

图的快速入门案例

要求用代码实现如下图结构

思路分析:

  • 每一个顶点需要用一个容器来装,这里使用简单的 String 类型来表示 A、B ... 等节点
  • 这些所有的顶点,我们用一个 List 来存储
  • 它对应的矩阵使用一个二维数组来表示,节点之间的关系

代码实现

public class Graph {
    private ArrayList<String> vertex;//表示图的顶点信息
    private int[][] edge;//表示图中边的信息
    private int numOfEdges;//表示边的条数
    public static void main(String[] args) {
        //测试图
        int n = 5;
        String[] arr = {"A","B","C","D","E"};
        Graph graph = new Graph(5);
        //添加顶点
        for(String value : arr){
            graph.insertVertex(value);
        }
        //添加边
        //A-B A-C B-C B-D B-E
        graph.insertEdge(0,1,1);
        graph.insertEdge(0,2,1);
        graph.insertEdge(1,2,1);
        graph.insertEdge(1,3,1);
        graph.insertEdge(1,4,1);

        //显示图
        graph.showGraph();
    }

    //创建图
    public Graph(int n){
        vertex = new ArrayList<>(n);
        edge = new int[n][n];
        numOfEdges = 0;//默认为0条边
    }

    //添加顶点
    public void insertVertex(String s){
        vertex.add(s);
    }

    /**
     * 添加边
     * @param v1 第一个节点
     * @param v2 第二个节点
     * @param weight 边的权重,我们规定0表示不能连接,1表示连接
     */
    public void insertEdge(int v1,int v2,int weight){
        edge[v1][v2] = 1;
        edge[v2][v1] = 1;
        numOfEdges++;
    }

    //获取顶点的数量
    public int getVertexSize(){
        return vertex.size();
    }
    //获取边的数量
    public int getEdgeSize(){
        return numOfEdges;
    }
    //根据下标获取顶点值
    public String getValueByIndex(int i){
        return vertex.get(i);
    }
    //返回两个顶点之间的权值
    public int getWeight(int v1,int v2){
        return edge[v1][v2];
    }
    //显示图矩阵
    public void showGraph(){
        for(int[] g : edge){
            System.out.println(Arrays.toString(g));
        }
    }
}

lua 定义数据结构 数据结构路径定义_深度优先遍历_05

图的深度优先遍历介绍

所谓图的遍历,则是 对节点的访问。一个图有很多个节点,如何遍历这些节点,需要特定策略,一般有两种访问策略:

  1. 深度优先遍历(DFS,Depth First Search)
  2. 广度优先遍历(BFS,Broad First Search)

深度优先遍历基本思想

图的深度优先搜索(Depth First Search),简称 DFS。

从初始访问节点出发,初始访问节点可能有多个 邻接节点(直连),深度优先遍历的策略为:

  • 首先访问第一个邻接节点
  • 然后以这个 被访问的邻接节点作为初始节点
  • 然后循环这个操作。

可以这样理解:每次都在访问完 当前节点 后,首先 访问当前节点的第一个邻接节点

可以看到:这样的访问策略是 优先往纵向 挖掘深入,而 不是 对一个节点的所有邻接节点进行 横向访问

那么显然深度优先搜索是一个 递归过程

深度优先遍历算法步骤

基本思想看完,可能还是不清楚是如何遍历的,看看他的遍历步骤:

  1. 访问初始节点 v,并标记节点 v 为已访问
  2. 查找节点 v 的第一个邻接(直连)节点 w
  3. 如果节点 w 不存在,则回到第 1 步,然后从 v 的下一个节点继续
  4. 如果节点 w 存在:
  1. 未被访问过,则对 w 进行深度优先遍历递归(即把 w 当做另一个 v,执行步骤 123)
  2. 如果已经被访问过:查找节点 v 的 w 邻接节点的下一个邻接节点,转到步骤 3
  3. lua 定义数据结构 数据结构路径定义_结点_06

以上图作为例子:添加节点的顺序为 A、B、C、D、E。

代码实现

//获取当前元素的第一个邻结点下标,找到就返回其下标,没有找到就返回-1
private int getFirstNeighbor(int index){
    for(int i = 0; i < numOfEdges; i++){
        if(edge[index][i] > 0){
            return i;
        }
    }
    return -1;
}
//获取当前元素的第一个邻结点的下一个领结点的下标,没有则返回-1
//其中index表示要获取谁的下一个邻结点,i表示已经找到的前一个邻结点的下标
private int getNextNeighbor(int index,int i){
    for(int j = i+1; j < numOfEdges; j++){
        if(edge[index][j] > 0){
            return j;
        }
    }
    return -1;
}
//图的深度遍历
private void dfs(boolean[] isVisited,int i){
    //先输出当前节点的信息
    System.out.print(getValueByIndex(i) + "->");
    //将当前节点设置为访问过
    isVisited[i] = true;
    //找当前结点的第一个邻结点
    int w = getFirstNeighbor(i);
    while(w != -1){
        //找到了
        if(!isVisited[w]){
            //没有访问则递归遍历
            dfs(isVisited,w);
        }
        //如果已经访问过,则查找下一个
        w = getNextNeighbor(i,w);
    }
}
//重载深度遍历,对每一个节点都进行深度遍历
public void dfs(){
    for(int i = 0; i < numOfEdges; i++){
        if(!isVisited[i]){
            dfs(isVisited,i);
        }
    }
}

lua 定义数据结构 数据结构路径定义_邻接矩阵_07

图的广度优先遍历

图的广度优先搜索(DFS,Broad First Search),类似于一个 分层搜索的过程,广度优先遍历需要使用一个队列以保持访问过的节点的顺序,以便按这个顺序来访问这些节点的邻接节点。

算法步骤

  1. 访问初始节点 v ,并标记节点 v 为已访问
  2. 节点 v 入队列
  3. 当队列非空时,继续执行,否则算法结束(仅对 v 的节点算法结束)。
  4. 出队列,取得队头的节点 u
  5. 查找节点 u 的第一个邻接节点 w
  6. 若节点 u 的邻接节点 w 不存在,则跳转到步骤 3;否则执行以下三个步骤:
  1. 若节点 w 尚未被访问,则访问节点 w 并标记为已访问
  2. 节点 w 入队列
  3. 查找节点 u 的继 w 邻接节点后的下一个邻接节点 w,转到步骤 6

这里可以看到,与深度优先不同的是:

  • 广度优先:找到第一个邻接节点,访问之后,会继续寻找这个节点的下一个邻接节点(非第一个)
  • 深度优先:每次只找第一个邻接节点,然后以找到的节点作为基础找它的第一个邻接节点,如果找不到才回溯到上一层,寻找找它的下一个邻接节点(非第一个)
  • lua 定义数据结构 数据结构路径定义_邻接矩阵_08

就如同上图:

  • 左侧的是广度优先,先把 A 能直连的都访问完,再换下一个节点
  • 右图是深度优先,每次都只访问第一个直连的节点,然后换节点继续,访问不到,则回退到上一层,找下一个直连节点

代码实现

//图的广度优先
private void bfs(boolean[] isVisited,int i){
    int u;//队里的头节点
    int w;//当前节点的邻结点
    System.out.print(getValueByIndex(i) + "->");
    isVisited[i] = true;
    //创建一个队列
    LinkedList list = new LinkedList();
    //将当前元素添加到队列尾部
    list.addLast(i);
    while(!list.isEmpty()){
        //从队列头部取出一个数据
        u = (Integer) list.removeFirst();
        w = getFirstNeighbor(u);
        while(w != -1){
            if(!isVisited[w]){
                System.out.print(getValueByIndex(w) + "->");
                isVisited[w] = true;
                list.addLast(w);
            }
            w = getNextNeighbor(u,w);
        }

    }
}
public void bfs(){
    for(int i = 0; i < numOfEdges;i++){
        if(!isVisited[i]){
            bfs(isVisited,i);
        }
    }
}

lua 定义数据结构 数据结构路径定义_结点_09

图的深度优先 VS 广度优先

由于前面讲解的点较少,恰好输出顺序一致,现在来对比下多一点的点。如下图

lua 定义数据结构 数据结构路径定义_深度优先遍历_10

进行代码测试遍历:

lua 定义数据结构 数据结构路径定义_lua 定义数据结构_11

lua 定义数据结构 数据结构路径定义_深度优先遍历_12