图的基本概念、存储及基本操作(邻接矩阵法与邻接表法)

本文详细介绍了图的基本概念,包括无向图、有向图、简单图和完全图,并阐述了图的两种主要存储方法——邻接矩阵和邻接表。邻接矩阵适用于稠密图,可以快速判断边的存在,但空间效率低;邻接表适合稀疏图,节省空间,便于查找邻接点。此外,还讨论了图的基本操作,如求顶点度、深度优先搜索(DFS)、广度优先搜索(BFS)以及判断图的连通性。
摘要由CSDN通过智能技术生成

图的基本概念、存储及基本操作(邻接矩阵法与邻接表法)

1. 图的基本概念

1.1 图的定义

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

在这里插入图片描述

图的定义与线性表定义的对比:

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

1.2 图的分类

1.2.1 无向图

无向边: 若顶点vi到vj之间的边没有方向,则称这条边为无向边(Edge),用无序偶对(vi,vj)来表示。

**若E是无向边(简称边)的优先集合时,则图G为无向图。**如下图所示:

在这里插入图片描述

  • G=(V,E)
  • V = {V1,V2,V3,V4,V5,V6}
  • E = {<V1,V2>,<V1,V4>,<V2,V5>,<V1,V3>,<V3,V4>,<V3,V5>,<V3,V6>,<V4,V6>,<V5,V6>}
1.2.2 有向图

有向边: 若从顶点vi到vj的边有方向,则称这条边为有向边,也称为弧(Arc)。用有序偶来表示,vi称为弧尾(Tail),vj称为弧头(Head)。

若E是有向边(也称弧)的有限集合时,则G为有向图。如下图所示:

See the source image
  • G=(V,E)
  • V = {1, 2, 3, 4, 5}
  • E = {<1,3>,<1,2>,<2,3>,<2,4>,❤️,5>,<4,3>,<4,5>}
1.2.3 简单图

在图中,若不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图。满足以下两个条件

  1. 无重复边
  2. 不存在结点到自身的边
1.2.4 多重图

若图G中某两个顶点之间的边数大于1条,又允许顶点通过一条边和自身关联,则称图为多重图。

1.2.5 完全图

完全图又分无向完全图与有向完全图。

无向完全图: 在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。

含有n个顶点的无向完全图有n(n-1)/2条边。

有向完全图: 在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。

含有n个顶点的有向完全图有n×(n-1)条边。

对于具有n个顶点和e条边数的图,无向图 0 ≤ e ≤ n ( n − 1 ) / 2 0≤e≤n(n-1)/2 0en(n1)/2, 有向图 0 ≤ e ≤ n ( n − 1 ) 0≤e≤n(n-1) 0en(n1)

稀疏图与稠密图: 有很少条边或弧的图称为稀疏图,反之称为稠密图。这里稀疏和稠密是模糊的概念,都是相对而言的。
: 有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的数叫做权(Weight)。这些权可以表示从一个顶点到另一个顶点的距离或耗费。
: 带权的图通常称为网(Network)。
子图: 设有两个图G=(V,{E})和G’=(V’,{E’}),如果$V’∈V且E’∈E,则称G’为G的子图(Sub-graph)。

1.3. 概念

1.3.1 图的顶点与边间关系

邻接点: 对于无向图G=(V,{E}),如果边(v,v’)∈E,则称顶点v和v’互为邻接点(Adjacent),即v和v’相邻接。

边(v,v’)依附(incident)于顶点v和v’,或者说(v,v’)与顶点v和v’相关联

: 顶点v的度(Degree)是和v相关联的边的数目,记为TD(v)。

对于有向图G=(V,{E}),如果弧∈E,则称顶点v邻接到顶点v’,顶点v’邻接自顶点v。弧和顶点v,v’相关联。以顶点v为
头弧的数目称为v的入度(InDegree),记为ID(v);以v为尾的弧的数目称为v的出度(OutDegree),记为OD(v);顶点v的度为TD(v)=ID(v)+OD(v)。

路径: 无向图G=(V,{E})中从顶点v到顶点v’的路径(Path)是一个顶点序列 ( v = v i 0 , v i 1 , . . . , v i m = v ′ ) , 其 中 ( v i , j − 1 , v i , j ) ∈ E , 1 ≤ j ≤ m (v=v_{i0},v_{i1},...,v_{im}=v'),其中(v_{i,j-1},v_{i,j})∈E,1≤j≤m (v=vi0,vi1,...,vim=v)(vi,j1,vi,j)E1jm,其中,。如果G是有向图,则路径也是有向的,顶点序列应满足 < v i , j − 1 , v i , j > ∈ E , 1 ≤ j ≤ m 。 <v_{i,j-1},v_{i,j}>∈E,1≤j≤m。 <vi,j1,vi,j>E1jm

路径的长度是路径上的边或弧的数目。

回路: 第一个顶点和最后一个顶点相同的路径称为回路或环(Cycle)。
简单路径: 序列中顶点不重复出现的路径称为简单路径。
简单回路: 除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称为简单回路或简单环。

1.3.2 连通图相关术语

连通: 在无向图G中,如果从顶点v到顶点v’有路径,则称v和v’是连通的。
连通图: 如果对于图中任意两个顶点vi、vj∈V,vi和vj都是连通的,则称G是连通图(Connected Graph)。
连通分量: 无向图中的极大连通子图称为连通分量。连通分量的概念强调:

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

强连通:在有向图中,如果有一对顶点v和w,从v到w和从w到v之间都有路径,则称这两个顶点是强连通的。

强连通图: 在有向图G中,如果对于每一对vi、vj∈V、vi≠vj,从vi到vj和从vj到vi都存在路径,即图中任何一对顶点都是强连通的,则称G是强连通图。
强连通分量: 有向图中的极大强连通子图称做有向图的强连通分量。
如下图,图1不是强连通图,因为顶点A到顶点D存在路径,而D到A就不存在,但图2是强连通图,图2是图1的极大强连通子图即强连通分量。

在这里插入图片描述

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

如下图所示:图G1即为图G的最小生成树。

image-20210930204254925

.

如果一个图有n个顶点和小于n-1条边,则是非连通图,如果它多于n-1条边,必定构成一个环,因为这条边使得它依附的那两个顶点之间有了第二条路径。随便加哪两顶点的边都将构成环。不过有n-1条边并不一定是生成树。

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

对有向树的理解比较容易,所谓入度为0其实就相当于树中的根结点,其余顶点入度为1就是说树的非根结点的双亲只有一个。

有向图的生成森林: 一个有向图的生成森林由若干棵有向树组成,含有图中全部顶点,但只有足以构成若干棵不相交的有向树的弧。

2. 图的存储与基本操作

图的存储必须要完整、准确地反映顶点集与边集的信息。

2.1 邻接矩阵存储

定义:

用一个有一位数组存储图中顶点得信息,用一个二维数组存储图中边的信息(即各顶点之间的邻接关系),存储顶点之间邻接关系的二维数组称为邻接矩阵。

图解:

邻接矩阵表示有向图如下图所示:

邻接矩阵表示无向图如下图所示:

See the source image

邻接矩阵表示网如下图所示:

See the source image
特点
  • 图的邻接矩阵是唯一的,适于存储边的数目较多的稠密图。
  • 无向图的邻接矩阵一定是一个对称矩阵,可采用压缩存储的思想,只存储上(下)三角形阵的元素即可。
  • 不带权的有向图的邻接矩阵一般是一个稀疏矩阵。
  • 矩阵中第 i 行或 第 i 列有效元素个数之和就是顶点的度。
  • 在有向图中 第 i 行有效元素个数之和是顶点的出度,第 i 列有效元素个数之和是顶点的入度。

邻接矩阵代码存储:

typedef int InfoType;
#define	MAXV 100				//最大顶点个数
#define INF 32767				//INF表示∞
//以下定义邻接矩阵类型
typedef struct 
{  	int no;						//顶点编号
	InfoType info;				//顶点其他信息
} VertexType;					//顶点类型
typedef struct  				//图的定义
{  	int edges[MAXV][MAXV]; 		//邻接矩阵
   	int n,e;   					//顶点数,边数
	VertexType vexs[MAXV];		//存放顶点信息
} MGraph;	

2.2 邻接表存储

定义:

图的邻接表存储方式是一种按顺序分配与链式分配相结合的存储方法.
为每个顶点建立一个单链表,边结点由三个域组成,adjvex指示与顶点i邻接的顶点的编号,nextarc指向对应下一条边的节点,info存储与边相关的信息,如权值等。表头节点有两个域组成,data存储顶点i对应的名称或其他信息,firstarc指向对应顶点i的链表中的第一个边节点。

图解

表示有向图的邻接表与逆邻接表如下图所示:

See the source image 表示无向图的邻接表如下图所示: See the source image
特点
  1. 顶点 vi 的出度为第i个单链表中的结点个数。
  2. 顶点 vi 的入度为整个单链表中邻接点域值是i-1的结点个数。
  3. 找出度易,找入度难

邻接表代码存储:

//以下定义邻接表类型
typedef struct ANode           	//边的节点结构类型
{	int adjvex;              	//该边的终点位置
   	struct ANode *nextarc; 		//指向下一条边的指针
   	InfoType info;           	//该边的相关信息,这里用于存放权值
} ArcNode;
typedef int Vertex;
typedef struct Vnode      		//邻接表头节点的类型
{	Vertex data;            	//顶点信息
    ArcNode *firstarc;     		//指向第一条边
} VNode;
typedef VNode AdjList[MAXV];	//AdjList是邻接表类型
typedef struct 
{	AdjList adjlist;         	//邻接表
    int n,e;                 	//图中顶点数n和边数e
} ALGraph;                   	//图的邻接表类型

2.3 邻接矩阵与邻接表优缺点:

邻接矩阵的优缺点
  • 优点:
    • 可以快速判断两个顶点之间是否存在边
    • 可以快速添加边或者删除边。
  • 缺点:
    • 是如果顶点之间的边比较少,会比较浪费空间。因为是一个 n∗nn∗n 的矩阵。
邻接表的优缺点:
  • 优点:
    • 方便找任一顶点的所有“邻接点”
    • 节约稀疏图的空间:需要N个头指针+2E个结点(每个结点至少2个域)
    • 对无向图而言,方便计算任一顶点的度
  • 缺点:
    • 对有向图而言,只能计算“出度”,需要构造“逆邻接表”来方便计算“入度”。

2.4. 图的基本操作

图的基本操作与实现

(1)自选存储结构,输入含n个顶点(用字符表示顶点)和e条边的图G;

(2)求每个顶点的度,输出结果;

(3) 指定任意顶点x为初始顶点,对图G作DFS遍历,输出DFS顶点序列(提示:使用一个栈实现DFS);

(4) 指定任意顶点x为初始顶点,对图G作BFS遍历,输出BFS顶点序列(提示:使用一个队列实现BFS);

(5) 输入顶点x,查找图G:若存在含x的顶点,则删除该结点及与之相关连的边,并作DFS遍历(执行操作3);否则输出信息“无x”;

(6)判断图G是否是连通图,输出信息“YES”/“NO”;

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#define maxSize 100
using namespace std;
 
int Count=0;
const int Max=50;
typedef char type;
bool visited[100] = { false };
 
typedef struct                         ///定义数据结构:栈
{
    int *top;
    int *base;
    int Size;
}Stack;
 
typedef struct                        ///定义数据结构:队列
{
    int *Front;
    int *Rear;
    int Size;
} Queue;
 
typedef struct                       ///定义数据结构:图
{
    type vexs[Max];
    bool arcs[Max][Max];
    int vexnum,arcnum;
} graph;
 
void InitStack(Stack &s)             ///对栈进行初始化
{
    if(!(s.top=s.base=(int *)malloc(maxSize*sizeof(int))))
        return ;
    s.Size=maxSize;
}
 
void push(Stack &s,int e)            ///入栈:将元素e压入栈
{
    *s.top=e;
    s.top++;
}
 
void pop(Stack &s)                  ///出栈:栈顶指针减一
{
    if(s.base==s.top)
        return ;
    s.top--;
}
 
int top(Stack s)                    ///返回栈顶元素
{
    s.top--;
    return *s.top;
}
 
void InitQueue(Queue &q)            ///初始化队列
{
    if(!(q.Front=q.Rear=(int *)malloc(maxSize*sizeof(int))))
        return ;
    q.Size=maxSize;
}
 
void Q_push(Queue &q,int e)         ///入队列:将元素e入队列
{
    *q.Rear=e;
    q.Rear++;
}
 
int Q_pop(Queue &q)                 ///出队列:返回队头元素,队头指针加一
{
    int e;
    if(q.Front==q.Rear)
        return 0;
    e=*q.Front;
    q.Front++;
    return e;
}
 
void Create(graph *g)               ///建立邻接矩阵并输出
{
    int i,j;
    memset(g->arcs,0,sizeof(g->arcs));
    for(i=0; i<g->arcnum; ++i)
    {
        int x,y;
        cin>>x>>y;
        g->arcs[x][y]=1;
        g->arcs[y][x]=1;
    }
    cout<<"无向邻接表矩阵为:"<<endl;
    for(i=0; i<g->vexnum; i++)
    {
        for(j=0; j<g->vexnum; j++)
            cout<<g->arcs[i][j]<<" ";
        cout<<endl;
    }
}
 
void Du(graph *g)                    ///统计每个顶点的度
{
    int i,j;
    for(i=0; i<g->vexnum; i++)
    {
        int amount=0;
        for(j=0; j<g->vexnum; ++j)
            if(g->arcs[i][j])
                ++amount;
        cout<<"顶点"<<g->vexs[i]<<"的度为"<<amount<<endl;
    }
}
 
void DFS(graph *g,int k)            ///DFS深度优先搜索
{
    int t,i;
    Stack s;
    InitStack(s);
    cout<<g->vexs[k]<<" ";
    visited[k]=true;
    push(s,k);
    while(s.base!=s.top)
    {
        t=top(s);
        if(i==g->vexnum)                              ///如果內层循环未找到没被访问的元素,则出栈
            pop(s);
        for(i=0; i<g->vexnum; i++)
        {
            if(g->arcs[t][i]==1&&visited[i]==false)   ///找出邻接矩阵第t+1行中第一个未被访问的元素
            {
                visited[i]=true;                      ///找到后标志为1,并输出,入栈
                cout<<g->vexs[i]<<" ";
                push(s,i);
                break;                                ///找到之后跳出內层循环,返回外层循环起始处,重复之前操作
            }
        }
    }
}
 
void BFS(graph *g, int k)            ///BFS广度优先搜索
{
    Queue q;
    InitQueue(q);
    visited[k]=true;
    cout<<g->vexs[k]<<" ";
    Q_push(q,k);
    while(q.Front!=q.Rear)
    {
        int t=Q_pop(q);
        for(int w=0; w<g->vexnum; w++)
        {
            if (g->arcs[t][w]!=0&&visited[w]==false)   ///找出邻接矩阵中t+1行所有未被访问的元素
            {
                visited[w]=true;                       ///每找到一个标志为1并入队列
                cout<<g->vexs[w]<<" ";
                Q_push(q,w);
            }
        }
    }
}
 
void Is_in_graph(graph *g,char ch)      ///查询图中是否存在顶点ch
{
    int i,j,k;
    for(i=0; i<g->vexnum; i++)          ///遍历顶点数组
        if(ch==g->vexs[i])
            break;
    if(i==g->vexnum)                    ///如果循环正常结束则i=g->vexnum,此时说明不存在该顶点
    {
        printf("无%c",ch);
        return ;
    }
    else
    {
        for(j=0; j<g->vexnum; j++)     ///如果存在,则将与顶点相关的边覆盖
            for(k=0; k<g->vexnum; k++)
            {
                if(j>=i&&k>=i)
                    g->arcs[j][k]=g->arcs[j+1][k+1];
                if(j>=i&&k<i)
                    g->arcs[j][k]=g->arcs[j+1][k];
                if(j<i&&k>=i)
                    g->arcs[j][k]=g->arcs[j][k+1];
            }
    }
    for(j=0; j<g->vexnum; j++)         ///覆盖该顶点
        if(j>=i)
            g->vexs[j]=g->vexs[j+1];
    g->vexnum--;
    cout<<"删除该顶点后剩余顶点为:\n";
    for(j=0; j<g->vexnum; j++)
        cout<<g->vexs[j]<<" ";
    cout<<endl;
    cout<<"删除后邻接矩阵为:\n";
    for(j=0; j<g->vexnum; j++)
    {
        for(k=0; k<g->vexnum; k++)
            cout<<g->arcs[j][k]<<" ";
        cout<<endl;
    }
    cout<<"从第一个顶点开始的DFS遍历为:\n";
    DFS(g,0);
}
 
 
void Is_connect(graph *g,int k)        ///利用DFS搜索的思想遍历图,每遍历一个顶点计数加一
{
    visited[k]=true;
    Count++;
    for(int i=0; i<g->vexnum; i++)
        if(g->arcs[k][i]==1&&visited[i]==false)
        {
            Is_connect(g,i);
        }
}
 
void main_menu()                      ///主菜单
{
    cout<<"-----------------------------------\n";
    cout<<"---------程序主菜单----------------\n";
    cout<<"-----------------------------------\n";
    cout<<"---------1图的创立及邻接矩阵-------\n";
    cout<<"---------2查询图中各顶点的度-------\n";
    cout<<"---------3DFS遍历图----------------\n";
    cout<<"---------4BFS遍历图----------------\n";
    cout<<"---------5查询顶点是否在图中-------\n";
    cout<<"---------6判断是否为连通图---------\n";
    cout<<"---------7退出---------------------\n";
    cout<<"---------回车键返回菜单------------\n";
    cout<<"-----------------------------------\n";
    cout<<"---------请选择:(1-7)--------------\n";
}
 
int main()
{
    int i,num;
    char ch;
    graph g;
    while(1)
    {
        system("cls");
        main_menu();
        scanf("%d",&num);
        system("cls");
        switch(num)                                   ///通过输入1-7调用不同的功能
        {
        case 1:
            printf("1图的创立及邻接矩阵\n\n");
            printf("请输入顶点的个数:");
            cin>>g.vexnum;
            cout<<endl;
            printf("请输入边的条数:");
            cin>>g.arcnum;
            printf("请输入各顶点:");
            for(i=0; i<g.vexnum; ++i)
                cin>>g.vexs[i];
            cout<<endl;
            printf("请输入各条边i-j:\n");
            Create(&g);
            printf("输入任意键返回主菜单:");
            system("pause");
            break;
        case 2:
            printf("2查询图中各顶点的度\n\n");
            Du(&g);
            printf("输入任意键返回主菜单:");
            system("pause");
            break;
        case 3:
            printf("3DFS遍历图\n\n");
            printf("请输入开始的顶点:");
            cin>>ch;
            cout<<endl;
            for(i=0;i<g.vexnum;i++)
                if(ch==g.vexs[i])
                    break;
            if(i>=g.vexnum)
            {
                printf("输入数字有误!");
                printf("输入任意键返回主菜单:");
                system("pause");
                break;
            }
            printf("从该点开始DFS搜索为:");
            DFS(&g,i);
            cout<<endl;
            memset(visited,0,sizeof(visited));           ///将访问标志数组初始化为0
            printf("输入任意键返回主菜单:");
            system("pause");
            break;
        case 4:
            printf("4BFS遍历图\n\n");
            printf("请输入开始的顶点:");
            cin>>ch;
            cout<<endl;
            for(i=0;i<g.vexnum;i++)
                if(ch==g.vexs[i])
                    break;
            if(i>=g.vexnum)
            {
                printf("输入数字有误!");
                printf("输入任意键返回主菜单:");
                system("pause");
                break;
            }
            printf("从该点开始BFS搜索为:");
            BFS(&g,i);
            cout<<endl;
            memset(visited,0,sizeof(visited));         ///将访问标志数组初始化为0
            printf("输入任意键返回主菜单:");
            system("pause");
            break;
        case 5:
            printf("5查询顶点是否在图中\n\n");
            printf("请输入要查询的顶点:");
            cin>>ch;
            cout<<endl;
            Is_in_graph(&g,ch);
            cout<<endl;
            memset(visited,0,sizeof(visited));        ///将访问标志数组初始化为0
            printf("输入任意键返回主菜单:");
            system("pause");
            break;
        case 6:
            printf("6判断是否为连通图\n\n");
            Is_connect(&g,0);
            if(Count==g.vexnum)
                printf("YES!");
            else
                printf("NO!");
            cout<<endl;
            Count=0;
            memset(visited,0,sizeof(visited));         ///将访问标志数组初始化为0
            printf("输入任意键返回主菜单:");
            system("pause");
            break;
        case 7:
            printf("退出成功");
            return 0;
        }
    }
}
 
 
 

3. 扩展

逆邻接表
在邻接表中对于有向图有一个很大的缺陷,如果我们比较关心顶点入度那么就需要遍历所有链表。为了避免这种情况出现,我们可以采用逆邻接表来存储,它存储的链表是别的顶点指向它。这样就可以快速求得顶点的入度。

如何对有向图的入度和出度都关心,那么久可以采取十字链表的方式。相当于每一个顶点对应两个链表,一个是它指向别的顶点,一个是别的顶点指向它。

Reference

https://www.zybuluo.com/guoxs/note/257430

https://blog.csdn.net/JYL1159131237/article/details/78504961

https://blog.csdn.net/JD_coder/article/details/79954592

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小枫学IT

如果觉得有用的话,可以支持一下

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值