数据结构-图

本文详细介绍了图的基本概念,包括有向图、无向图、简单图和完全图,以及子图、连通图和强连通图。还讨论了图的存储方式,如邻接矩阵、邻接表、十字链表和邻接多重表。此外,文章阐述了图的遍历方法,包括广度优先搜索和深度优先搜索,并介绍了图的应用,如最小生成树、最短路径和拓扑排序。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、图的基本概念

        一)基本性质

                1、线性表可以为空表,树可以是空树,但图不可以是空图,即图中不能一个顶点也没有,途中的顶点集V(Vertex)一定非空,但边集E(Edge)可以为空

                2、顶点集V和边集E中的元素数量必须是有限的

                3、任意一条边的两边必须连接着两个顶点

        二)有向图

                1、E是有向边的有限集合,称该图为有向图,这种有向的边也称为弧

                2、弧是顶点的有序对,记为<v,w>

                3、弧<v,w>的v称为弧尾,w称为弧头,<v,w>称为从v到w的弧,也称为v邻接到w

        三)无向图

                1、E是无向边的有限集合时,称该图为无向图

                2、无向图的任意两个结点互为邻接点,且<w,v>的两顶点可以互换

                3、无向图和有向图均只对边的相关性质做了限制

        四)简单图、多重图

                1、简单图即不存在重复边(平行边),且不存在顶点指向自身的边(自环)

                2、若图不满足简单图的定义,则其可能为多重图和伪图

                3、多重图指即含有平行边的图,伪图指含有平行边或自环的图

                4、这里提供另一种解释,伪图和多重图不做区分,且多重图的定义和简单图相对,不是简单图就是多重图(我们只研究简单图,故这里不做深入探讨)

        五)完全图

                1、对于无向图,其|E|在0到C-n^2之间,当|E|取最大值时,即每个顶点间都存在一条边,此时该图被称为完全图

                2、对于有向图,其|E|在0到2*C-n^2之间,当|E|取最大值时,即每个顶点间都存在两条方向相反的弧,此时该图被称为完全图

        六)子图

                1、若存在两个图G(V,E)和G'(V',E'),当V'是V的子集,E'是E的子集,则称G'是G的子图

                2、若G和G'的顶点相同,但边集不同,则称G'为G的生成子图

        七)连通、连通图、连通分量

                1、这些性质和概念仅存在于无向图中

                2、若顶点v和顶点w之间存在路径,则称这两个顶点是连通的

                3、若无向图中任意两个顶点都是连通的,则称该图为连通图,否则称为非连通图

                4、无向图的极大连通子图称为连通分量,极大要求该连通子图包含其所有的边

                5、一个图中的连通分量可能不止一个

                6、非连通图最多有C-2^n-1条边,即将n-1个顶点组成一个完全图

        八)强连通图、强连通分量

                1、这些性质和概念仅存在于有向图中

                2、对于一对顶点w、v,若同时存在从w到v的路径和从v到w的路径,则称这对顶点是强连通的

                3、若有向图中的任意一对顶点都是强连通的,则称该图为强连通图

                4、有向图的极大连通子图称为强连通分量,强连通分量至少有n条边,构成一个环路

        九)生成树、生成森林

                1、无向图才有生成树的概念

                2、连通图的生成树即指包含该树中所有结点的极小连通子图,极小要求其边数最少且保持连通

                3、非连通图的连通分量的生成树构成了生成森林

        十)概念区分

                1、有向图和无向图是两个概念,且不存在重合

                2、简单图是一个很宽泛的概念,本章节讨论的图均为简单图(有向简单图或无相简单图)

                3、完全图的概念适用于有向图和无向图,但定义略有不同,本质都是在满足简单图的定义下,使边数最多

                4、子图个概念适用于有向图和无向图,且定义相同

                5、连通、连通图、连通分量均为无向图的概念,仅在讨论无向图时才有意义

                6、强连通、强连通分量均为有向图的概念,仅在讨论有向图时才有意义

                7、生成树、生成森林是对连通图和非连通图的讨论,故仅在讨论无向图时才有意义

        十一)顶点的度、入度和出度

                1、无向图中讨论顶点的度,度即依附于顶点V的边的个数,且度总数=2E

                2、有向图中讨论顶点的度、入度和出度,入度即以顶点V为终点的弧的数量,出度即以顶点V为起点的弧的数量,顶点V的入度+出度=度,且有向图的入度总数=出度总数=E

        十二)边的权和网

                图中的每条边都可以根据实际需求赋予一定的权值,这种被赋予了权值的图被称为带权图,也称为网

        十三)稠密图、稀疏图

                1、边数很少的图称为稀疏图,反之为稠密图

                2、稀疏图和稠密图没有明确定义,只是一个模糊的感觉

                3、一般情况,当图中|E|=|V|log |V|时,称该图为稀疏图

        十四)路径、路径长度和回路

                1、两顶点vp和vq间路径,由一系列顶点序列构成,且该序列从vp开始,到vq结束,路径也可以将路上的边考虑进去

                2、路径上边的个数称为路径长度

                3、路径序列的第一个顶点和最后一个顶点相同,称为回路或环

                4、若图有n个顶点,但由超过n-1条路径,则该图中一定有环

        十五)简单路径、简单回路

                1、在路径序列中,若不存在重复的顶点,则称该路径为简单路径

                2、在路径序列中,若除首尾顶点外,不存在重复的顶点,则称该路径为简单回路

        十六)距离

                1、从vp到vq的最短路径若存在,则称此路径的长度为vp到vq的距离

                2、若两顶点间不存在路径,则距离为∞

        十七)有向树

                一个顶点的入度为0,其余顶点的入度为1的有向图,称为有向树

二、图的存储及基本操作

        一)邻接矩阵法

                1、用一维数组存储顶点,用一个二维数组存储图的边,在简单应用中,可以忽略顶点的记录,仅记录边

                2、对于无权图,若二维数组中,A[i][j]为1,则说明Vi和Vj间存在边,A[i][j]为0,则说明两顶点间不存在边 

                3、对于带权图,若二维数组中,A[i][j]为0或∞,则说明两顶点间不存在边,反之说明两顶点间存在边,且数组值就是边的权值

                4、对于无向图,其邻接矩阵一定为对角矩阵,可以采取上三角法对其进行压缩存储

                5、对于无向图,其第i行不为0和∞的元素个数就是顶点Vi的度,

                6、对于有向图,其第i行不为0和∞的元素个数就是顶点Vi的出度,其第i列不为0和∞的元素个数就是顶点Vi的入度

                7、邻接矩阵法可以很轻松的确定两顶点间是否存在边,但若想要获取边的总数,只能遍历

                8、稠密图更适合使用邻接矩阵法,因为即使两顶点间不存在边也要占用一个二维数组的空间

                9、若邻接矩阵为A,则矩阵A^n对应位置[i][j]的值即为顶点i到顶点j,长度为n的路径的数目(了解即可)

                10、邻接矩阵存储结构的代码定义

#define MAXVERTEXNUM 100;
typedef struct{
    VertexType V[MAXVERTEXNUM];
    EdgeType[MAXVERTEXNUM][MAXVERTEXNUM];
    int vNum, eNum;
}MGraph;

        二)邻接表法

                1、将每个顶点顺序存储,将顺序存储的每个顶点作为链首,用链表存储依附于该顶点的所有边(链表中存储着顶点,由链首和链表结点构成的序列即为边),我们称所有的链表为邻接表,称某个顶点对应的单链表为边表,称顶点顺序存储的表为顶点表

                2、无向图的邻接表需要的存储空间为|V|+2|E|,因为每个边都连接着两个顶点,即一条边会在两个不同的链表中各出现一次

                3、有向图的邻接表需要的存储空间为|V|+|E|,因为有向图中为有向边,每条边只会出现一次

                4、稀疏图更适合使用邻接表,稠密图也可以使用邻接表法,但是不如使用邻接矩阵合适

                5、邻接表找到一个顶点所有的边非常简单,但是要判断两顶点之间是否存在边则需要遍历对应的两个链表(有向图只需要遍历一个链表)

                6、邻接表求出度只需要遍历对应链表,但是求解入度只能遍历邻接表

                7、邻接表并不唯一,因为单链表中边的排序方式是可以改变的

                8、邻接表存储结构定义的代码表示

#define MAXVERTEXNUM 100;
typedef struct ENode{
    int v;//定义边表结点
    struct ENode *next;//边表结点需要有指向下一个边的指针
}ENode;

typedef struct{
    VertexType data;//定义顶点表结点,顶点表需要存储图中所有结点的数据
    ENode *firstNode;//还需要一个指针指向依附于该顶点的第一条边
}VNode, AdjList[MAXVERTEXNUM];//这里直接将顶点表定义出来

typedef struct{
    AdjList L;//邻接表中应包括顶点表和对应的边表,边表在定义顶点表时就已经链接到顶点上,不需要额外处理
    int v, e;//用于记录顶点数和边数
}AGraph;

        三)十字链表

                1、十字链表仅用于存储有向图

                2、十字链表类似于邻接表,但做了很大的改进

                3、顶点结点有三个域,一个data域,两个指针域,两个指针分别指向以该顶点为弧尾和弧头的第一个弧

                4、弧结点有五个域,一个data域,两个记录域,两个指针域,两个记录域分别记录当前弧的弧头和弧尾对应的顶点所存储的位置,两个指针域分别指向与当前弧的弧尾或弧头相同的弧

                5、十字链表仅仅是增设了部分域,本质并未增设结点,所依其占用的存储空间为|V|+|E|

        四)邻接多重表

                1、邻接多重表仅用于存储无向图

                2、邻接多重表类似于十字链表

                3、顶点结点有两个域,一个data域,一个指针域,指针域指向依附于该顶点的第一个边

                4、边表结点有五个域,一个data域,两个记录域,两个指针域,两个记录域分别记录该边连接的两个顶点,两个指针域分别指向依附于第一个记录域顶点的下一条边和依附于第二个记录域顶点的下一条边

三、图的遍历

        一)基本概念

                1、图的遍历即按照某种方式对图中所有结点进行遍历操作

                2、图的遍历类似于树的遍历,但是要难很多,因为图中会存在环,我们需要解决环对遍历产生的影响,同时图可能不连通

                3、解决环路的方案为,设立一个标志数组,每个结点被访问之后,将标志数组对应位置修改为true,处理连通分量的方案为,每次遍历结束标志数组,如果存在连通分量未被访问,则对该连通分量再次进行遍历

        二)广度优先搜索

                1、类似于二叉树的层次遍历

                2、广度优先不具备自动回退的特性,所以无法使用递归代码实现,而是借助队列实现,核心代码在于FirstNeighbor函数和NextNeighbor函数

                3、思路从开始顶点出发,访问该结点,并依次访问该结点的所有邻接顶点,每访问一个邻接顶点就将其置入队列,之后每次从栈顶弹出一个顶点,重复上面操作

                4、代码实现如下,性能分析:因为需要辅助队列,故最坏情况时,一个顶点邻接着剩余所有顶点,这样除入口顶点外的所有顶点都要入队,此时空间复杂度为o(|V|),邻接矩阵存储时,每次循环都需要遍历对应行或列,需要|V|,且visited中的每个顶点都会经历一次循环,故时间复杂度诶o(|V|^2),邻接表存储时,每个visited中的顶点都会进入循环,但是所有循环共耗时|E|,故时间复杂度为o(|E|+|V|)

void BFSTraverse(Graph G){
    initVisited(visited[]);//初始化标记数组,将其元素置为false
    initQueue(Q);//初始化队列,用于遍历
    for(int i=0; i<=G.vernum; i++)//遍历整个标记数组,寻找标记为false的元素
        if(!visited[i])
            BFS(G,i);
}

void BFS(Graph G,int v){
    visit(v);//从顶点v开始遍历,对首个遍历的顶点单独进行访问,修改数组,入队的操作
    visited[v]=true;
    EeQueue(Q,v);
    while(!isEmpty(Q))//此时顶点v已经入队,队列不为空,可以开始循环
        DeQueue(Q,v);//循环方式为出队,访问邻接顶点,访问后立即入队
        for(w=FirstNeighbor(G,v); w>0; w=NextNeighbor(G,v,w))
            if(!visited(w))
                visit(w);
                visited[w]=ture;
                EeQueue(Q,w);
}

                5、对于BFS算法,其每次从入口顶点v到w的循环所遍历的顶点即为v到w的单源最短路径

                6、广度优先生成树即通过BFS得到的一棵遍历树,需要注意的是,邻接矩阵的生成树唯一,但因为邻接表不唯一,所以邻接表的生成树不唯一

        三)深度优先搜索

                1、深度优先搜索类似于树的先序遍历

                2、思路是从入口顶点v开始,先访问该顶点,再访问与其邻接的任意顶点w1,再访问与w1邻接的的任意顶点w2,以此类推

                3、与树先序遍历的不同点类似于BSF,首先是需要一个用于记录顶点是否被访问过的标记数组,其次需要留意连通分量不止一个的情况

                4、代码实现,性能分析:因为递归需要借助递归工作栈,且在最差情况下,所有顶点连成一条线,则会递归到最深处,此时空间复杂度为o(|V|),对于邻接矩阵存储方式,分析方式同BFS,为o(|V|^2),对于邻接表存储方式,分析同BFS,为o(|V|+|E|)

void DFSTraverse(Graph G){
    initVisited(Visited[]);//标记数组初始化为全false
    for(int i=0; i<G.vexnum; i++)
        if(!visited[i])//当且仅当未被访问过才进入递归体
            DFS(G,i);
}

void DFS(Graph G, int v){//以下为递归体
    visit(v);//递归体需先访问顶点
    visited[v]=true;//再将标记数组对应值改为true
    for(w=FirstNeighbor(G,v); w>=0; w=NextNeighbor(G,v,w))
        if(!visited[w])//循环体,这里需要找到第一个邻接且未被访问过的顶点,并进入递归
            DFS(G,w);
}

                5.深度优先搜索也存在一棵深度优先生成树,当然要注意,非连通图是生成森林

        四)图的遍历与图的连通性

                1、可以用图的遍历算法来判断图的连通性

                2、无向图较为简单,只要遍历算法仅执行一次就完成遍历,就说明其为连通图

                3、有向图较为复杂,因为有向图即使连通,也可能无法通过一次遍历对所有顶点完成遍历,因为有向图的连通分为强连通和非强连通

四、图的应用

        一)最小生成树

                1、通常情况下,最小生成树不唯一,仅当图中个边权值互不相等时,最小生成树唯一

                2、最小生成树的权值之和总是唯一的,即生成树不唯一,但权值总和唯一

                3、最小生成树的边数一定等于顶点数-1

                4、非连通图不存在生成树,只存在生成森林

                5、Prim算法求解最小生成树

                        1)从最小生成树中只有一个顶点开始,每次找到当前与之权值最小的顶点加入树中,之后将这两个顶点看为一个整体,再次寻找权值最小的边加入树中,以此类推

                        2)Prim算法的时间复杂度为o(|V|^2):每次选择一个顶点,并找到与之相连的权值最小的边,所以外循环遍历了所有的顶点,内循环最坏情况下也会遍历所有顶点。因此Prim算法适用于边稠密的图

                6、Kruskal算法求解最小生成树

                        1)从树中有n个顶点开始(n=|V|),每次选择权值最小的边,将其对应的两个顶点连接起来,并将这两个顶点视为一个整体,之后重复以上步骤

                        2)Kruskal算法的时间复杂度为o(|E|log |E|):选择边最坏情况要选|E|次,每次选完边加入树的操作可以看作为并查集,故加入树的操作log |E|。因此Kruskal算法更适合边稀疏而顶点较多的算法

                        3)Prim和Kruskal都是找边,因为对于最小生成树,最后所有的顶点一定都在树中,真正有意义的也就是边,只不过Prim算法从一个顶点开始,Kruskal从所有顶点开始

        二)最短路径

                1、BFS算法求解最短路径

                        1)BFS算法仅能用于求解所有边权值相同的图的最短路径

                        2)BFS算法每次运行只能求解出一条单源最短路径,即从某一顶点到其他顶点的最短路径,无法一次性求解出所有顶点间的最短路径

                        3)需要引入两个数组,一个数组为distance数组,即距离数组,用于记录该顶点到源点的最短路径长度;一个数组是path数组,用于记录当前顶点到源点路径的前驱

                        4)BFS算法求解最短路径思路与BFS搜索相似,只是将visit函数进行了修改

                        5)具体修改方式为:循环体中,每次会从队列中弹出一个顶点,找到弹出顶点第一个未被访问的邻接顶点,对该顶点对应位置的dist数组进行修改dist[w]=dist[u]+1,即该顶点的路径长度为其前驱(也就是弹出的那个顶点)到源点的路径长度再加上1;再修改path[w]=u,即该顶点的前驱为u

                2、Dijkstra算法求解最短路径

                        1)Dijkstra算法与Prim算法较为相似

                        2)Dijkstra算法可用于求解带权图,但是在一次执行中,其仅能求解出单源最短路径

                        3)Dijkstra算法本质是一种迭代算法

                        4)在BFS引入的两个数组的基础上,需要再引入一个数组final,用于记录所有顶点是否已经找到最短路径(源点对应位置初始为true)

                        5)算法思路:遍历final数组,找到第一个还未找到最短路径、且其dist数组值最小的顶点,将其final数组值修改为true,之后检查与该顶点邻接的其他顶点,若以该顶点为中转,到达其他邻接顶点的路径更短,就更新邻接顶点的dist数组,并修改path。之后循环,直至final均为true

                        6)Dijkstra算法的时间复杂度为o|V|^2

                        7)Dijkstra算法无法处理带有负权值的图,因为Dijkstra每轮都会确定一条当前最短的边,若存在负权值,则当前最短的边组成的当前最短路径,可能在负权值对应上存在更短的路径

                3、Floyd算法求解最短路径

                        1)Floyd算法在一次执行后,即可获取所有顶点间的最短路径,且可以处理带权图

                        2)Floyd算法引入两个矩阵,一个矩阵A用于存储两顶点间最短路径的长度,另一个矩阵path用于存储两顶点间的中转点(注意,矩阵只存储两个顶点间的一个中转点,但两个顶点间可能不止一个中转点,要确定所有中转点,需要再考虑顶点与中转点间是否依然存在中转点)

                        2)Floyd算法思路:首先初始化两个矩阵,初始化时,不允许通过任何顶点进行终止(即两顶点间的最短路径就是两顶点间的边,若没有边就是不存在最短路径);之后加入顶点V0,考虑以V0作为中转点时,能否改变各个顶点的最短路径(遍历矩阵,对于A[i][j],当A[i][0]+A[0][j]<A[i][j]时,即通过V0中转存在更短的路径),若能,则修改path矩阵和A矩阵,之后再加入V1,同上

                        3)Floyd算法可以求解带负权值的路径,但无法求解整体权值为负的环路(负权回路),因为这样的环路,每循环一次,权值就会更小

        三)有向无环图描述表达式

                1、有向无环图称为Directed Acyclic Graph,即DAG

                2、常用有向无环图表示算术表达式,并求解所需顶点最少时,顶点数量

                3、求解方式:对于算术表达式在有向无环图的表达,其参与运算的树一个不会重复,故先将所有运算数放到最底层,每个运算数占据一个顶点,在将运算符按运算先后顺序标号,按照标号先后顺序,用运算符顶点连接对应的运算数,注意同一优先级的运算符在同一层,不同优先级在不同层(所谓同一优先级,即两个运算符的运算互不影响,不同优先级,即高优先级的运算符依赖低优先级运算符先完成运算),最后对所得有向无环图进行化简,将共用的部分合并,合并时仅需要考虑同一层的元素能否合并,不同层一定不能合并

        四)拓扑排序

                1、首先引入AOV网Activity On Vertex,该网用DAG表示一个工程。顶点表示活动,有向边<w,u>表示顶点w的活动一定先于顶点u的活动发生

                2、拓扑排序是一种对有向无环图顶点的排序,若存在一条从顶点A到顶点B的路径,则在拓扑排序中A一定在B之前

                3、拓扑排序思路:从AOV中取出一个入度为0的顶点放入拓扑排序中,并删除以之为起点的所有有向边,重复以上步骤直至AOV为空,或不存在入度为0的顶点(此时拓扑排序生成失败,该图存在回边)

                4、若每次从AOV中取出一个出度为0的顶点,则可以生成逆拓扑排序

                5、可以用DFS算法生成拓扑排序和逆拓扑排序,思路为将DFS中的visit部分修改为print输出顶点,(逆拓扑排序还需将print的语句位置放在递归体最后,即在出递归时print)

        五)关键路径

                1、在带权有向图中,以顶点表示事件,以边表示活动,以边上的权值表示该活动完成的开销,这样的图称为AOE,Acitivity On Edge,即用边表示活动的网络

                2、AOE网中,只有顶点所代表的的事件发生了,以该顶点为起点的活动才能开始;某一顶点只有在指向该顶点的所有活动均结束才能发生;有些活动可以并行进行

                3、在AOE中,仅有一个顶点入度为0,该顶点称为开始顶点(源点);也仅有一个顶点出度为0,该顶点称为结束顶点(汇点);AOE表示一个工程,源点表示工程的开始,汇点表示工程的结束

                4、从源点到汇点的路径可能有很多条,具有最长路径长度的路径被称为关键路径,只有改变关键路径的长度,才能影响到整个工程

                5、从零时刻开始,每个事件最快发生的时间被称为最早发生时间;给定工程总消耗情况下,每个事件最晚发生的时间被称为最晚发生时间;最早发生时间和最晚发生时间之差为时间余量

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值