数据结构 - 图

前言

前面我们学习了线性表,栈、队列和树。前面三者都属于线性表范畴,它的的数据元素是被串起来的,仅有线性关系,每个元素仅有一个直接前驱和一个直接后继,是属于一对一关系。在树里面,每个元素之间存在着明显的层次关系,每一层的元素可能和下一层的多个元素相关,但只能和上一层的一个元素相关,属于一对多的关系。而图是一种较线性表和树更为复杂的数据结构,在图的结构中,节点和节点的关系是任意的,图中任意两个数据元素都可能相关。

图的定义

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

在图中需要注意的是:

(1)线性表中我们把数据元素叫元素,树中将数据元素叫结点,在图中数据元素,我们则称之为顶点(Vertex)。

(2)线性表可以没有元素,称为空表;树中可以没有节点,称为空树;但是,在图中不允许没有顶点(有穷非空性)。

(3)线性表中的各元素是线性关系,树中的各元素是层次关系,而图中各顶点的关系是用边来表示(边集可以为空)。

图的基本概念

  • (1)无向图
    如果图中任意两个顶点之间的边都是无向边(简而言之就是没有方向的边),则称该图为无向图(Undirected graphs)。


    (1)无向图
  • (2)有向图
    如果图中任意两个顶点之间的边都是有向边(简而言之就是有方向的边),则称该图为有向图(Directed graphs)。


    (2)有向图
  • (3)完全图
    ①无向完全图:在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。(含有n个顶点的无向完全图有(n×(n-1))/2条边)如下图所示:


    无向完全图

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


    有向完全图

    注意:当一个图接近完全图时,则称它为稠密图(Dense Graph),而当一个图含有较少的边时,则称它为稀疏图(Spare Graph)。
  • (4)顶点的度

顶点Vi的度(Degree)是指在图中与Vi相关联的边的条数。对于有向图来说,有入度(In-degree)和出度(Out-degree)之分,有向图顶点的度等于该顶点的入度和出度之和。

  • (5)邻接

①若无向图中的两个顶点V1和V2存在一条边(V1,V2),则称顶点V1和V2邻接(Adjacent);

②若有向图中存在一条边,则称顶点V3与顶点V2邻接,且是V3邻接到V2或V2邻接直V3;

注意:无向图中的边使用小括号“()”表示,而有向图中的边使用尖括号“<>”表示。

  • (6)路径

在无向图中,若从顶点Vi出发有一组边可到达顶点Vj,则称顶点Vi到顶点Vj的顶点序列为从顶点Vi到顶点Vj的路径(Path)。

  • (7)连通

若从Vi到Vj有路径可通,则称顶点Vi和顶点Vj是连通(Connected)的。

  • (8)权
    有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的数叫做权(Weight)。

图的存储结构

图的存储结构除了要存储图中的各个顶点本身的信息之外,还要存储顶点与顶点之间的关系,因此,图的结构也比较复杂。常用的图的存储结构有邻接矩阵和邻接表等。

  • (1)无向图:


    1)无向图:

    我们可以设置两个数组,顶点数组为vertex[4]={v0,v1,v2,v3},边数组arc[4][4]为上图右边这样的一个矩阵。对于矩阵的主对角线的值,即arc[0][0]、arc[1][1]、arc[2][2]、arc[3][3],全为0是因为不存在顶点的边。

  • (2)有向图:

我们再来看一个有向图样例,如下图所示的左边。顶点数组为vertex[4]={v0,v1,v2,v3},弧数组arc[4][4]为下图右边这样的一个矩阵。主对角线上数值依然为0。但因为是有向图,所以此矩阵并不对称,比如由v1到v0有弧,得到arc[1][0]=1,而v到v没有弧,因此arc[0][1]=0。


(2)有向图:

注:由于存在n个顶点的图需要n*n个数组元素进行存储,当图为稀疏图时,使用邻接矩阵存储方法将会出现大量0元素,这会造成极大的空间浪费。这时,可以考虑使用邻接表表示法来存储图中的数据。

邻接表表示法

首先,回忆我们在线性表时谈到,顺序存储结构就存在预先分配内存可能造成存储空间浪费的问题,于是引出了链式存储的结构。同样的,我们也可以考虑对边或弧使用链式存储的方式来避免空间浪费的问题。

邻接表由表头节点和表节点两部分组成,图中每个顶点均对应一个存储在数组中的表头节点。如果这个表头节点所对应的顶点存在邻接节点,则把邻接节点依次存放于表头节点所指向的单向链表中。

  • (1)无向图:下图所示的就是一个无向图的邻接表结构。


    无向图

     从上图中我们知道,顶点表的各个结点由data和firstedge两个域表示,data是数据域,存储顶点的信息,firstedge是指针域,指向边表的第一个结点,即此顶点的第一个邻接点。边表结点由adjvex和next两个域组成。adjvex是邻接点域,存储某顶点的邻接点在顶点表中的下标,next则存储指向边表中下一个结点的指针。例如:v1顶点与v0、v2互为邻接点,则在v1的边表中,adjvex分别为v0的0和v2的2。
    注:对于无向图来说,使用邻接表进行存储也会出现数据冗余的现象。例如上图中,顶点V0所指向的链表中存在一个指向顶点V3的同事,顶点V3所指向的链表中也会存在一个指向V0的顶点。

  • (2)有向图:若是有向图,邻接表结构是类似的,但要注意的是有向图由于有方向的。因此,有向图的邻接表分为出边表和入边表(又称逆邻接表),出边表的表节点存放的是从表头节点出发的有向边所指的尾节点;入边表的表节点存放的则是指向表头节点的某个顶点,如下图所示。


    有向图
  • (3)带权图:对于带权值的网图,可以在边表结点定义中再增加一个weight的数据域,存储权值信息即可,如下图所示。


    带权图

邻接无向图

    1. 邻接表无向图介绍
      邻接表无向图是指通过邻接表表示的无向图。

上面的图G1包含了"A,B,C,D,E,F,G"共7个顶点,而且包含了"(A,C),(A,D),(A,F),(B,C),(C,D),(E,G),(F,G)"共7条边。

上图右边的矩阵是G1在内存中的邻接表示意图。每一个顶点都包含一条链表,该链表记录了"该顶点的邻接点的序号"。例如,第2个顶点(顶点C)包含的链表所包含的节点的数据分别是"0,1,3";而这"0,1,3"分别对应"A,B,D"的序号,"A,B,D"都是C的邻接点。就是通过这种方式记录图的信息的。

邻接表无向图代码实现

struct ENode
{
    int nVindex;  // 该边所指的顶点的位置
    ENode *pNext; // 指向下一个边的指针
};

struct VNode
{
    char data;  // 顶点信息
    ENode *pFirstEdge; // 指向第一条依附该顶点的边
};
listUDG(char *vexs, int vlen, char edges[][2], int elen)
{
    m_nVexNum = vlen;
    m_nEdgNum = elen;

    // 初始化"邻接表"的顶点
    for (int i = 0; i < vlen; i ++)
    {
        m_mVexs[i].data = vexs[i];
        m_mVexs[i].pFirstEdge = NULL;
    }

    char c1,c2;
    int p1,p2;
    ENode *node1, *node2;
    // 初始化"邻接表"的边
    for (int j = 0; j < elen; j ++)
    {
        // 读取边的起始顶点和结束顶点
        c1 = edges[j][0];
        c2 = edges[j][1];
        p1 = GetVIndex(c1);
        p2 = GetVIndex(c2);

        node1 = new ENode();
        node1->nVindex = p2;
        if (m_mVexs[p1].pFirstEdge == NULL)
        {
            m_mVexs[p1].pFirstEdge = node1;
        }
        else
        {
            LinkLast(m_mVexs[p1].pFirstEdge, node1);
        }

        node2 = new ENode();
        node2->nVindex = p1;
        if (m_mVexs[p2].pFirstEdge == NULL)
        {
            m_mVexs[p2].pFirstEdge = node2;
        }
        else
        {
            LinkLast(m_mVexs[p2].pFirstEdge, node2);
        }
    }

}

邻接有向图

邻接表有向图是指通过邻接表表示的有向图。

上面的图G2包含了"A,B,C,D,E,F,G"共7个顶点,而且包含了",,,,,,,,"共9条边。

上  图右边的矩阵是G2在内存中的邻接表示意图。每一个顶点都包含一条链表,该链表记录了"该顶点所对应的出边的另一个顶点的序号"。例如,第1个顶点(顶点B)包含的链表所包含的节点的数据分别是"2,4,5";而这"2,4,5"分别对应"C,E,F"的序号,"C,E,F"都属于B的出边的另一个顶点。

struct ENode
{
    int nVindex;  // 该边所指的顶点的位置
    ENode *pNext; // 指向下一个边的指针
};

struct VNode
{
    char data;  // 顶点信息
    ENode *pFirstEdge; // 指向第一条依附该顶点的边
};
listDG(char *vexs, int vlen, char edges[][2], int elen)
{
    m_nVexNum = vlen;
    m_nEdgNum = elen;

    // 初始化"邻接表"的顶点
    for (int i = 0; i < vlen; i ++)
    {
        m_mVexs[i].data = vexs[i];
        m_mVexs[i].pFirstEdge = NULL;
    }

    char c1,c2;
    int p1,p2;
    ENode *node1;
    // 初始化"邻接表"的边
    for (int j = 0; j < elen; j ++)
    {
        // 读取边的起始顶点和结束顶点
        c1 = edges[j][0];
        c2 = edges[j][1];
        p1 = GetVIndex(c1);
        p2 = GetVIndex(c2);

        node1 = new ENode();
        node1->nVindex = p2;
        if (m_mVexs[p1].pFirstEdge == NULL)
        {
            m_mVexs[p1].pFirstEdge = node1;
        }
        else
        {
            LinkLast(m_mVexs[p1].pFirstEdge, node1);
        }
    }
}

你可能感兴趣的:(数据结构 - 图)