重学数据结构--图

316 阅读3分钟

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

本系列文章为个人学习总结,如果有发现错误或存在疑问之处,欢迎留言指点!

本文是重学数据结构系列的第六篇,系列文章如下:
1.算法时间复杂度和空间复杂度
2.重学数据结构--链表
3.重学数据结构--队列
4.重学数据结构--栈
5.重学数据结构--树

1.为什么会有图?

我们知道,数据之间的关系有3种,分别是:一对一、一对多和多对多,前两种关系的数据可分别用线性表和树结构存储,而多对多的关系就要用图表示。

2.图的相关概念

  • 顶点

  • 边:边有些地方又称为

    • 在有向图中,无箭头一端的顶点通常被称为初始点弧尾,箭头直线的顶点被称为终端点弧头
  • 无向图

    image-20201123204014182

  • 路径:上图中A->C的路径:

    • A->C

    • A->B->C

      如果路径中第一个顶点和最后一个顶点相同,则此路径称为"回路"(或"环")。

  • 有向图

    image-20201123204923066

  • 带权图:带权的图又称

    image-20201123204658983

  • 入度与出度:上面有向图中A的入度为1,出度为2

图的表示方法有两种:一种用二维数组表示(邻接矩阵);另一种是用链表(邻接表)

3.用二维数组表示

首先我们拟定给各个顶点一个序号(从0开始),edges[v1][v2]=weight, 数组edges用于存储两个顶点之间的关系,其中 中v1v2分别表示某两个顶点的序号,我们约定weight=0表示两个顶点之间没有接,weight为其他值表示两个顶点之间的权值(这里没有权值默认为1)。

image-20201124223354073

具体实现

public class Graph {
  
    // 顶点集合
    private ArrayList<String> vertexList;
    //存储图对应的邻接矩阵
    private int[][] edges;
    //表时边的数量
    private int numOfEdges;
​
    /**
     * 构造器
     * @param n 节点数
     */
    public Graph(int n){
        edges = new int[n][n];
        vertexList = new ArrayList<>(n);
        numOfEdges = 0;
    }
​
    /**
     * 获取节点个数
     * @return
     */
    public int getNumOfVertex(){
        return vertexList.size();
    }
​
    /**
     * 显示图对应的矩阵
     */
    public void showGraph(){
        for (int[] link : edges){
            System.out.println(Arrays.toString(link));
        }
    }
​
    /**
     * 获取边的数量
     * @return
     */
    public int getNumOfEdges(){
        return numOfEdges;
    }
​
    /**
     * 返回节点下标对应的数据
     * @return
     */
    public String getValueByIndex(int index){
        return vertexList.get(index);
    }
​
    /**
     * 返回对应权值
     * @param v1
     * @param v2
     * @return
     */
    public int getWeight(int v1,int v2){
        return edges[v1][v2];
    }
​
    /**
     * 插入节点
     * @param vertex
     */
    public void insertVertex(String vertex){
        vertexList.add(vertex);
    }
​
    /**
     * 插入边
     * @param v1
     * @param v2
     * @param weight 权值
     */
    public void insertEdge(int v1,int v2,int weight){
        edges[v1][v2] = weight;
        edges[v2][v1] = weight;
        numOfEdges++;
    }
}
​

测试

image-20201124225004951

    public static void main(String[] args) {
        //测试一把图是否创建ok
        int n = 5;  //结点的个数
        String Vertexs[] = {"A", "B", "C", "D", "E"};
​
        //创建图对象
        Graph graph = new Graph(n);
        //循环的添加顶点
        for(String vertex: Vertexs) {
            graph.insertVertex(vertex);
        }
​
        //添加边
        //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();
    }
​

4.图的两种遍历方式

1.深度优先搜索

深度优先搜索又称深搜DFS

访问思路

  • 深度优先搜索,从初始访问结点出发,初始访问结点可能有多个邻接结点,深度优先遍历的策略就是首先访问

    第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问它的第一个邻接结点, 可以这样理解:

    每次都在访问完当前结点后首先访问当前结点的第一个邻接结点。

  • 这样的访问策略是优先往纵向挖掘深入,而不是对一个结点的所有邻接结点进行横向访问。

  • 显然,深度优先搜索是一个递归的过程

image-20201125143929227

数组存储:String Vertexs[] = {"v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8"};

深度优先搜索的过程类似于树的先序遍历,我们以上图为例对图的进行深度优先遍历,遍历过程为:

  • 首先任意找一个未被遍历过的顶点,这里以节点v1为初始节点进行访问,并标记v1为已访问
  • 遍历V1的邻接节点,v1的第一个邻接节点为v2,标记v2已经访问,遍历v2的邻接节点,v2的第一个邻接节点是v3,但是v2跟v3没有联系,继续下一个邻接节点是v4(做标记),然后是v8(做标记),然后v5
  • 当继续遍历v5的邻接节点时,根据之前做的标记,所有有联系的邻接节点都被访问过了。此时,从v5回退到v8,看v8是否有未被访问过的邻接节点,如果没有继续回退到v4,v2,v1;
  • 通过遍历v1,找到一个未被访问过的顶点v3,然后访问v3的邻接节点v6,然后v7
  • 由于v7没有未被访问的邻接节点,所以回退到v6,v3,v1,都没有未被访问过的
  • 最后一步需要判断是否所有顶点都被访问,如果还有没被访问的,以未被访问的顶点为第一个顶点,继续依照上边的方式进行遍历。

根据上边的过程,可以得到上图通过深度优先搜索获得的顶点的遍历次序为:

V1 -> V2 -> V4 -> V8 -> V5 -> V3 -> V6 -> V7

具体实现

以上面图的二维数组表示为基准

    //记录是否被访问
    private boolean[] isVisited;
   
​
    /**
     * 得到第一个邻接节点的下标
     * @param index
     * @return
     */
    public int getFirstNeighbor(int index){
        for (int j = 0; j < vertexList.size(); j++) {
            if(edges[index][j]>0){
                return j;
            }
        }
        return -1;
    }
​
    /**
     * 根据前一个邻接节点的下标来获取下一个邻接节点
     * @param v1 当前遍历节点序号
     * @param v2 v1节点已被访问过的节点序号
     * @return
     */
    public int getNextNeighbor(int v1,int v2){
        for (int j = v2+1; j < vertexList.size(); j++) {
            if(edges[v1][j]>0){
                return j;
            }
        }
        return -1;
    }
​
​
    /**
     * 深度优先遍历方法
     * @param isVisited
     * @param i
     */
    private void dfs(boolean [] isVisited,int i){
        System.out.print(getValueByIndex(i)+"->");
        isVisited[i] = true;
        // 查找节点i的第一个邻接节点
        int w = getFirstNeighbor(i);
        while (w != - 1){
            if(!isVisited[w]){
                dfs(isVisited,w);
            }
            // 如果w节点已经被访问过
            w = getNextNeighbor(i,w);
        }
    }
​
    public void dfs(){
        isVisited = new boolean[vertexList.size()];
        for (int i = 0; i < getNumOfVertex(); i++) {
            if(!isVisited[i]){
                dfs(isVisited,i);
            }
        }
    }
​

测试

    public static void main(String[] args) {
        //测试一把图是否创建ok
        int n = 8;  //结点的个数
​
        String Vertexs2[] = {"v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8"};
        //创建图对象
        Graph graph2 = new Graph(n);
        //循环的添加顶点
        for(String vertex: Vertexs2) {
            graph2.insertVertex(vertex);
        }
​
        graph2.insertEdge(0, 1, 1);
        graph2.insertEdge(1, 3, 1);
        graph2.insertEdge(1, 4, 1);
        graph2.insertEdge(3, 7, 1);
        graph2.insertEdge(4, 7, 1);
        graph2.insertEdge(0, 2, 1);
        graph2.insertEdge(2, 5, 1);
        graph2.insertEdge(2, 6, 1);
​
        graph2.dfs();
​
    }

2.广度优先搜索

广度优先搜索又称广搜bfs。类似于一个分层搜索(树的层次遍历)的过程,广度优先搜索需要使用一个队列以保持访问过的结点的顺序,以便按这个顺序来访问这些结点的邻接结点。

访问思路

  • 从初始访问节点出发,让其入队,标记为已经访问
  • 取队头元素,查找其未访问的邻接节点,标记为已经访问,并依次入队
  • 如果列队不为空,继续取队头元素,查找其未访问的邻接节点标记为已经访问并依次入队
  • 直到队列为空

image-20201125143929227

以上图为例,遍历过程为:

  • 取v1入队,标记为已经访问,出队,v1的邻接节点v2,v3入队,并标记为已经访问
  • v2出队,v4,v5入队并标记;v3出队,v6,v7入队并标记
  • v4出队,v8入队并标记;v5出队,v6出队,v7出队,v8出队。

最后遍历结果为:

V1 -> V2 -> v3 -> V4 -> V5 -> V6 -> V7 -> V8

具体实现

这里仍然以上面图的二维数组表示为基准

    /**
     * 广度优先遍历方法
     * @param isVisited
     * @param i
     */
    private void bfs(boolean [] isVisited,int i){
        // 表示队列的头节点对应下标
        int u;
        // 邻接节点w
        int w;
        // 队列,记录节点的访问顺序
        LinkedList<Object> queue = new LinkedList<>();
        System.out.print(getValueByIndex(i)+"->");
        isVisited[i] = true;
        queue.addLast(i);
            
        while (!queue.isEmpty()){
            // 取出队列的头节点下标
            u = (Integer) queue.removeFirst();
            // 得到第一个邻接节点的下标w
            w = getFirstNeighbor(u);
            while (w !=-1){
                if(!isVisited[w]){
                    System.out.print(getValueByIndex(w)+"->");
                    isVisited[w] = true;
                    queue.addLast(w);
                }
                // 以u为前驱节点,找到w后面的下一个邻接节点
                w = getNextNeighbor(u,w);
            }
        }
    }
​
    public void bfs(){
        isVisited = new boolean[vertexList.size()];
        for (int i = 0; i < getNumOfVertex(); i++) {
            if(!isVisited[i]){
                bfs(isVisited,i);
            }
        }
    }

测试:

    public static void main(String[] args) {
        //测试一把图是否创建ok
        int n = 8;  //结点的个数
​
        String Vertexs2[] = {"v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8"};
        //创建图对象
        Graph graph2 = new Graph(n);
        //循环的添加顶点
        for(String vertex: Vertexs2) {
            graph2.insertVertex(vertex);
        }
​
        graph2.insertEdge(0, 1, 1);
        graph2.insertEdge(1, 3, 1);
        graph2.insertEdge(1, 4, 1);
        graph2.insertEdge(3, 7, 1);
        graph2.insertEdge(4, 7, 1);
        graph2.insertEdge(0, 2, 1);
        graph2.insertEdge(2, 5, 1);
        graph2.insertEdge(2, 6, 1);
​
        graph2.bfs();
​
    }

\