《大话数据结构》--图

682 阅读12分钟

图的定义

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

  • 线性表中把数据元素叫元素,树中将数据元素叫结点,在图中数据元素称之为顶点(Vertex)
  • 线性表中可以没有数据元素,称为空表。树中可以没有结点,叫做空树。**在图结构中,不允许没有顶点。**在定义中,强调了顶点集合V有穷非空。
  • 线性表中,相邻的数据元素之间具有线性关系。树结构中,相邻两层的结点具有层次关系。图中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的。

图的术语

无向图

若顶点Vi到Vj之间的边没有方向,则称这条边为无向边(Edge), 用无序偶对(Vi, Vj) 来表示。如果图中任意两个顶点之间的边都是无向边,则称该图为无向图(Undirected graphs)。 

在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图含有n个顶点的无向完全图有nx(n-1)/2条边

有向图

若顶点Vi到Vj之间的边有方向,则称这条边为有向边(Edge), 也称作弧。用有序偶<Vi, Vj> 来表示。Vi称作弧尾,Vj称作弧头。如果图中任意两个顶点之间的边都是有向边,则称该图为有向图(Undirected graphs)。连接顶点A到D的有向边就是弧,A是弧尾,D是弧头,<A, D>表示弧

在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。含有n个顶点的有向完全图有nx (n-1) 条边。

与图的边或弧相关的数叫做权(Weight)。这些权可以表示从一个顶点到另一个顶点的距离或耗费。这种带权的图通常称为网(Network)。

图的顶点与边的关系

对于无向图G= (V,{E}), 顶点v的度(Degree) 是和v相关联的边的数目,记为TD (v)。 顶点A的度为3。

对于有向图G= (V,{E}), 以顶点v为头的弧的数目称为v的入度(InDegree),记为ID (v); 以v为尾的弧的数目称为v的出度(OutDegree),记为0D (v); 顶点v的度为TD (v) =ID (v) +OD (v)。 顶点A的入度是2 (从B到A的弧,从C到A的弧),出度是1 (从A到D的弧),所以顶点A的度为2+1=3.

**第一个顶点到最后一个顶点相同的路径称为回路或环(Cycle)。 序列中顶点不重复出现的路径称为简单路径。除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称为简单回路或简单环。**两个图的粗线都构成环,左侧的环因第一个顶点和最后一个顶点都是B,且C、D、A没有重复出现,因此是一个简单环。而右侧的环,由于顶点C的重复,它就不是简单环了。

连通图

在无向图G中,如果从顶点v到顶点v'有路径,则称v和v'是连通的。如果对于图中任意两个顶点Vi、 vj∈E, Vi和Vj 都是连通的,则称G是连通图。

无向图中的极大连通子图称为连通分量。注意连通分量的概念,它强调: 

  • 要是子图;

  • 子图要是连通的;

  • 连通子图含有极大顶点数;

  • 具有极大顶点数的连通子图包含依附于这些顶点的所有边。

在有向图G中,如果对于每一对Vi. vj∈V、Vi≠Vj, 从Vi到vj和从Vj到Vi都存在路径,则称G是强连通图。有向图中的极大强连通子图称做有向图的强连通分量。

生成树

所谓的一个连通图的生成树是一个极小的连通子图,它含有图中全部的n个顶点,但只有足以构成一棵树的n-1条边

如果一个有向图恰有一个顶点的入度为0,其余顶点的入度均为1,则是一棵有向树

图的定义和总结

图按照有无方向分为无向图和有向图。无向图由顶点和边构成,有向图由顶点和弧构成。弧有弧尾和弧头之分。

图按照边或弧的多少分稀疏图和稠密图。

如果任意两个顶点之间都存在边叫完全图,有向的叫有向完全图。若无重复的边或顶点到自身的边则叫简单图。 

图中顶点之间有邻接点、依附的概念。无向图顶点的边数叫做,有向图顶点分为入度和出度。 图上的边或弧上带权则称为。 

图中顶点间存在路径,两顶点存在路径则说明是连通的,如果路径最终回到起始点则称为,当中不重复叫简单路径。若任意两顶点都是连通的,则图就是连通图,有向则称强连通图。图中有子图,若子图极大连通则就是连通分量,有向的则称强连通分量

 无向图中连通且n个顶点n-1条边叫生成树。有向图中一顶点入度为0其余顶点入度为1的叫有向树。一个有向图由若干棵有向树构成生成森林。 

图的存储结构

邻接矩阵

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

存储结构

static class MGraph {    
    String[] vexs; // 存储顶点的数组
    int[][] arc; // 存储边的数组。
    int numVertexes; // 顶点数    
    int numEdges; // 边数    
    public MGraph() {        
        this.vexs = new String[5];        
        this.arc = new int[5][5];  
        this.numVertexes = 5;
        this.numEdges = 6;  
     }
}

创建图

static void createMGraph(MGraph m) {      
    // 由用户输入改为固定值  
    String[] inputVertexe = {"V0", "V1", "V2", "V3", "V4"};        
    int[][] inputEdge = {{0, 4, 6},{1, 0, 9},{1, 2, 3},{2, 0, 2},{2, 3, 5},{3, 4, 1}};        
    for(int i=0; i<m.numVertexes; i++) { // 存储顶点信息         
        m.vexs[i] = inputVertexe[i];        
    }        
    for (int i=0; i<m.numVertexes; i++) { // 矩阵初始化           
        for (int j = 0; j < m.numVertexes; j++) {                
            if(i == j) {                    
                m.arc[i][j] = 0;                
            } else {                    
                m.arc[i][j] = INFINITY;                
            }            
        }        
    }        
    for (int i = 0; i < m.numEdges; i++) { // 读入 inputEdge条边,建立邻接矩阵        
        int[] input = inputEdge[i];            
        int i1 = input[0];            
        int j1= input[1];            
        m.arc[i1][j1] = input[2];
        //无向图矩阵对称,需要满足aij = aji  由于上图是有向图,注释
        //m.arc[j1][i1] = m.arc[i1][j1];        
    }    
}

测试

public static void main(String[] args) {    
    MGraph m = new MGraph();    
    createMGraph(m);    
    Arrays.stream(m.vexs).forEach(item -> System.out.printf("%-3s", item));    
    System.out.println();    
    Arrays.stream(m.arc).forEach(item -> {        
        Arrays.stream(item).forEach(item1 ->System.out.printf("%-6s", item1));        
        System.out.println();    
    });
}

**n个顶点和e条边的无向网图的创建,时间复杂度为0(n+n2+e),其中对邻接矩阵的初始化耗费了0(n2)的时间。  **

**无向网图和有向网图创建区别  **

创建矩阵时,无向网图需要满足aij = aji。有向网图则不需要

邻接表

邻接表的处理办法

1.图中顶点用一个一维数组存储。对于顶点数组,每个数据元素还需要存储指向第一个邻接点的指针,以便于查找该顶点的边信息。

2.图中每个顶点Vi的所有邻接点构成-一个线性表,由于邻接点的个数不定,所以用单链表存储,无向图称为顶点Vi 的边表,有向图则称为顶点VI 作为弧尾的出边表。

存储结构

// 边表结点
static class EdgeNode {    
    int adjvex;    
    int weight;    
    EdgeNode next;
}
// 顶点表结点
static class VertexNode {    
    String data;    
    EdgeNode firstdge;    
    public VertexNode(String data) {        
        this.data = data;        
        this.firstdge = null;    
    }
}
static class GraphAdjList {    
    VertexNode[] adjList;    
    int numVertexes; // 顶点数    
    int numEdges; // 边数    
    public GraphAdjList() {        
        this.adjList = new VertexNode[5];           
        this.numVertexes = 5; 
        this.numEdges = 6;
    }
}

创建图

static void createMGraph(GraphAdjList graph) {        
    String[] inputVertexe = {"V0", "V1", "V2", "V3", "V4"};        
    int[][] inputEdge = {{0, 4, 6},{1, 0, 9},{1, 2, 3}, {2, 0, 2},{2, 3, 5},{3, 4, 1}};        
    for(int i=0; i<graph.numVertexes; i++) {            
        VertexNode node = new VertexNode(inputVertexe[i]);            
        graph.adjList[i] = node;        
    }        
    for (int i = 0; i < graph.numEdges; i++) {            
        int[] input = inputEdge[i];            
        int vi = input[0];           
        int vj = input[1];           
        EdgeNode newNode = new EdgeNode();            
        newNode.adjvex = vj;            
        newNode.weight = input[2];            
        newNode.next = graph.adjList[vi].firstdge;            
        graph.adjList[vi].firstdge = newNode;            
        // 无向图时满足对称
//      EdgeNode newNode1 = new EdgeNode();
//      newNode1.adjvex = vi;
//      newNode1.weight = input[2];
//      newNode1.next = graph.adjList[vj].firstdge;
//      graph.adjList[vj].firstdge = newNode1;        
    }    
}

测试

public static void main(String[] args) {    GraphAdjList m = new GraphAdjList();    createMGraph(m);    Arrays.stream(m.adjList).forEach(item -> {        if(item.firstdge != null) {            System.out.print("顶点" + item.data + ": ");            EdgeNode node = item.firstdge;            while(node != null) {                System.out.print("V" + node.adjvex + " -> weight:" + node.weight + "   ");                node = node.next;            }            System.out.println();        }    });}

十字链表(对有向图邻接表的改造)

对于有向图来说,邻接表是有缺陷的关心了出度问题,了解入度就必须要遍历整个图才能知道。逆邻接表解决了入度却不了解出度的情况。

于是就有了十字链表(Orthogonal List)。

左侧为邻接表的存储结构,右侧为十字链表的存储结构。

firstin表示入边表头指针,指向该顶点的入边表中第一个结点, firstout 表示出边表头指针,指向该顶点的出边表中的第一个结点。其中tailvex是指弧起点在顶点表的下标,headvex是指弧终点在顶点表中的下标,headlink是指入边表指针域,指向终点相同的下一条边,tallink是指边表指针域,指向起点相同的下一条边。如果是网,还可以再增加一个weight域来存储权值。

顶点依然是存入一个-维数组{V0,V1,V2,V3},就以顶点V0来说,firstout 指向的是出边表中的第一个结点V3。所以V0边表结点的headvex=3,而tailvex 其实就是当前顶点V0的下标0,由于vo只有一个出边顶点,所以headlink和tillink都是空。

虚线箭头的含义,其实就是此图的逆邻接表的表示。对于v0来说,它有两个顶点v1和V2的入边。因此V0的firstin指向顶点V1的边表结点中headvex为0的结点,上图中的①。接着由入边结点的headlink指向下一个入边顶点V2,如图中的②.对于顶点v1,它有一个入边顶点V2,所以它的firstin指向顶点V2的边表结点中headvex为1的结点,如图中的③。顶点V2和V3也是同样有一个入边顶点,如图中④和⑤。

十字链表的好处就是因为把邻接表和逆邻接表整合在了一起, 这样既容易找到以v为尾的弧,也容易找到以V为头的弧,因而容易求得顶点的出度和入度。

邻接多重表(对无向图邻接表的改造)

无向图的邻接表,关注的重点是顶点。邻接多重表关注的重点是边。

重新定义边表节点结构

ivex和jvex是与某条边依附的两个顶点在顶点表中下标。ilink 指向依附顶点ivex的下一条边,jlink 指向依附顶点jvex的下一条边。这就是邻接多重表结构。

结构图

边集数组

边集数组是由两个一维数组构成。一个是存储顶点的信息,另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标(begin)、 终点下标(end) 和权(weight)组成。

边集数组关注的是边的集合

具体使用会在最小生成树中提到

图的遍历

从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫做图的遍历(Traversing Graph)。

深度优先遍历(DFS)

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

**从图中某个顶点v出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到。**事实上,我们这里讲到的是连通图,对于非连通图,只需要对它的连通分量分别进行深度优先遍历,即在先前一个顶点进行一次深度优先遍历后, 若图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。

实现

创建数组标记顶点是否被访问 private static Boolean[] visited;

邻接矩阵遍历

static void DFSTraverse(MGraph m) {    
    for (int i=0; i<m.numVertexes; i++) {        
        visited[i] = false;    
    }    
    for (int i = 0; i < m.numVertexes; i++) {        
        if(!visited[i]) {            
            DFS(m, i);        
        }    
    }
}
static void DFS(MGraph m, int i) {    
    visited[i] = true;    
    System.out.print(m.vexs[i] + " "); // 打印顶点,也可以进行其他操作    
    for (int j = 0; j < m.numVertexes; j++) {        
        if(!visited[j] && (m.arc[i][j] > 0 && m.arc[i][j] < INFINITY)) {            
            DFS(m, j);        
        }    
    }
}

邻接表遍历

static void DFSTraverse(GraphAdjList m) {    
    for (int i=0; i<m.numVertexes; i++) {        
        visited[i] = false;    
    }    
    for (int i = 0; i < m.numVertexes; i++) {        
        if(!visited[i]) {            
            DFS(m, i);        
        }    
    }
}
static void DFS(GraphAdjList m, int i) {    
    visited[i] = true;    
    System.out.print(m.adjList[i].data + " "); // 打印顶点,也可以进行其他操作    
    EdgeNode node = m.adjList[i].firstdge;   
    while(node != null) {        
        if(!visited[node.adjvex]) {            
            DFS(m, node.adjvex);        
        }        
        node = node.next;    
    }
}

邻接表使用的是链表的方式存储的。创建图时使用的是头插法,输出结果可能会因为输入的边的顺序而发生变化。

区别

对于n个顶点e条边的图来说,邻接矩阵由于是二维数组,要查找每个顶点的邻接点需要访问矩阵中的所有元素,因此都需要0(n2)的时间。而邻接表做存储结构时,找邻接点所需的时间取决于顶点和边的数量,所以是0(n+e)。显然对于点多边少的稀疏图来说,邻接表结构使得算法在时间效率上大大提高。

对于有向图而言,区别只是弧存在或不存在,算法上没有变化,可以通用。

广度优先遍历(BFS)

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

将图第一幅图稍微变形,变形原则是顶点A放置在最上第一层,让与它有边的顶点B、F为第二层,再让与B和F有边的顶点C、I、G、E为第三层,再将这四个顶点有边的D、H放在第四层,如第二幅图所示。此时在视觉上感觉图的形状发生了变化,其实顶点和边的关系还是完全相同的。

实现

创建visited数组标记是否访问过,queue队列

private static Boolean[] visited = new Boolean[9];
private static Queue<Integer> queue = new ArrayDeque();

邻接矩阵遍历

static void BFSTraverse(MGraph m) {    
    for (int i = 0; i < m.numVertexes; i++) {        
        visited[i] = false;    
    }    
    for (int i=0; i<m.numVertexes; i++) { // 对每一个结点做循环        
        if(!visited[i]) { // 为访问过就处理            
            visited[i] = true;            
            System.out.print(m.vexs[i] + " ");            
            queue.add(i); // 将顶点的下标加入队列            
            while(!queue.isEmpty()) {                
                i = queue.remove(); // 出队,吧下标赋值给i,                
                for (int j = 0; j < m.numVertexes; j++) {     
                    // 判断其他顶点与当前顶点存在边且顶点未访问过                    
                    if(!visited[j] && m.arc[i][j] > 0 && m.arc[i][j] < INFINITY) {  
                        visited[j] = true; // 将找到的顶点标记为已访问                       
                        System.out.print(m.vexs[j] + " ");                        
                        queue.add(j); // 将找到的顶点加入队列                    
                    }                
                }            
            }        
        }    
    }
}

邻接表遍历

static void BFSTraverse(GraphAdjList m) {    
    for (int i = 0; i < m.numVertexes; i++) {        
        visited[i] = false;    
    }    
    for (int i=0; i<m.numVertexes; i++) { // 对每一个结点做循环        
        if(!visited[i]) {            
            visited[i] = true;            
            System.out.print(m.adjList[i].data + " ");            
            queue.add(i);            
            while(!queue.isEmpty()) {                
                i = queue.remove();                
                EdgeNode edgeNode = m.adjList[i].firstdge; // 找到当前顶点边表链表头指针     
                while(edgeNode != null) {                    
                    if(!visited[edgeNode.adjvex]) { // 若该顶点未被访问
                        visited[edgeNode.adjvex] = true;                       
                        System.out.print(m.adjList[edgeNode.adjvex].data + " ");          
                        queue.add(edgeNode.adjvex);                    
                    }                    
                    edgeNode = edgeNode.next;  // 指向下一个邻接点
                }            
            }        
        }    
    }
}

深度优先遍历和广度优先遍历区别

图的深度优先遍历与广度优先遍历算法,在时间复杂度上是一样的,不同之处仅仅在于对顶点访问的顺序不同。

深度优先更适合目标比较明确,以找到目标为主要目的的情况,而广度优先更适合在不断扩大遍历范围时找到相对最优解的情况。