图的存储和遍历

#include<stdio.h>
#include<stdlib.h>
#include<iostream>
/*
    2017年3月25日17:16:55
    练习王道书中的代码。主要是针对图的多种存储方法的部分。因为图一共有四种存储方法,分别是①邻接矩阵②邻接表③十字链表④邻接多重表
    其中,前两种可以存储有向图和无向图,而十字链表法,只能用来存有向图,邻接多重表只能用来存储无向图。
    这些节点定义很复杂,所以需要理解其本质,然后帮助记忆。
    已经上传CSDN

*/

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/*
    下面第一种结构体是图的邻接矩阵存储方式。
    邻接矩阵需要的参数有
    ①保存顶点信息的数组(顶点可能是字符,也可能书数字,这个信息得要保存下来)
    ②一个矩阵(也就是二维数组)。这个矩阵可以表示三种型式 无向图,有向图,带权值的有向图。下面分别介绍这三个
    无向图 :假如这个矩阵要来保存无向图,那么矩阵中存的就是0或者1,0表示Aij中,i和j的顶点不相连接。否则表示相连接。这个矩阵有以下特点: a.主对角线全为0,因为规定自身和自身节点之间是不连接的。 b.是一个对称矩阵。所以可以采用压缩存储的方式拉力存储这个矩阵。  c.这个矩阵中边的数目是1的个数的一半 d.第i个顶点的度是第i行或者第i列中1的个数。
    有向图 :同上一样。不过矩阵特点不同:a.主对角全为0,。 b.并不是一个对称矩阵 c.弧的条数等于这个矩阵中1的个数。 d.对于第i个顶点,出度数第i行中1的个数,入度数第i列中1的个数。 e.
    有向带权图:只是将有向图中的0全部换成正无穷,将1换成对应边上的权值即可。
    ③用来计数现在这个图中顶点个数和边的个数的两个参数。
*/


#define MaxVertexNum 100//定义顶点个数最多为100个
typedef char VertexType;
typedef int EdgeType;//带权图的边的权值或者图是否连通

typedef struct{
    VertexType Vex[MaxVertexNum];//保存顶点信息的数组
    EdgeType Edge[MaxVertexNum][MaxVertexNum];//矩阵
    int vexnum,arcnum;//图的当前顶点数目和弧数
}MGraph;//这个使用邻接矩阵表示法的时候,需要定义的图的结构体。
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////




////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/*
    图的第二种表示方法:邻接表表示方法。可以针对有向图和无向图两种。
    为什么要采用邻接表呢?当这个图为稀疏图的时候,存储起来是很浪费空间的。所以,和之前一样,采用链表的方式来存储的话,比较节省空间。
    邻接表的构造:
    1.邻接表由两部分组成,第一部分是表头,第二部分是链表部分。所有的表头可以进行顺序存储,这样的话,可以随机快速的访问所有的节点有关信息。

    2.表头由两部分组成①data域,存放的是这个顶点的相关信息。②指针域,指向与该顶点相连接的第一个顶点。 一个图有多少个顶点,就有多少个表头,将这些表头放在一个数组中,就可以使用其在数组中的下标对其进行索引。
        |----|--------|
        |data|firstarc|   这个就是顶点表的格式
        |----|--------|
        顶点域 指针域

    3.链表节点由两部分组成①与adjvex域,类型是int的,存放的是与表头所表示的顶点相连接的第一个顶点所在表头数组的index.第二个区域存放的是nextarc,类型是一个连表节点指针,指向的是与表头顶点相连接的下一个顶点的节点。
        |------|-------|
        |adjvex|nextarc| 这个是边表的格式
        |------|-------|
     邻接点域    指针域

    邻接表的特点:
    1.有多少个顶点,就有多少行
    2.如果是无向图的话,链表节点的个数等于边数的2倍。
    3.如果是有向图的话,连表节点的个数等于弧数。
    4.一行表示的是与该顶点连接的所有顶点。所以,很容易就能算出无向图某个顶点的度。同时,这一行个数也表示的是有向图顶点对应的出度。那么有向图的入度是怎么计算的呢?循环遍历所有的连表节点,如果链表节点存的adjvex中有等于你所求顶点的index,那么入度就+1。
    5.邻接表的表示并不唯一,因为,你所谓的与该顶点连接的第一个顶点,这个"第一"是人为规定的。还有,针对每个链表,进行头插法还是尾差法,都会使节点顺序不一样。
*/

//图的邻接表存储结构的结构体定义方法。//注意总是先定义边,因为顶点需要指向边。
typedef struct ArcNode{//边表节点
    int adjvex;//该弧所指向的弧头所在数组中的位置
    struct ArcNode * next;
    //Infotype info;//该边的权值。
}ArcNode;

typedef struct VNode{//表头节点
    VertexType data;
    ArcNode * first;
}VNode,AdjList[MaxVertexNum]; //注意typedef和数组的结合,可以参考c primer plus 481页


//和二叉树等等一样,进行信息整合,直接用下面的结构体可以直接创建图这个结构类型。
typedef struct{
    AdjList vertices;//邻接表 相当于创建了一个数组 相当于 VNode vertices[100]; 这个vertices是一个内含100个元素的数组名称
    int vexnum,arcnum;//图的顶点和弧数
}ALGraph;//ALGraph是以邻接表存储的图的类型
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/*
    图的第三种存储方法:十字链表法。针对的是有向图。
    前面已经学习了邻接矩阵法和邻接表法,为什么还要学习十字链表法呢?它有什么优势呢?
    在学习邻接表的时候,如果要计算一个节点的出度,那么就是这个节点对应的那一行连表节点,但是要算入度的话,就要循环遍历整个图结构中的所有节点,这个是比较耗时的,所以提出了十字链表法,可以快速的找到有向图的所有入度节点。
    因为牵扯到入度,所以十字链表法指针对的是有向图。
    十字链表法的构造:
    1.十字链表法同样需要两种节点,顶点节点和弧节点。同样,所有的顶点节点都可以进行顺序存储,方便随机索引。

    2.十字链表是为方便快速找到出度和入度的节点而设计的,所以表头节点由三部分构成(邻接表表头由两部分构成,data域和指针域,这个指针指向的是与这个表头相连接的另一个顶点相关的节点)
        ①data域,保存的是这个顶点包含的信息,可以为int类型或者char类型,存放的是int或者char
        ②firstin域,保存的是指向第一个(人为规定的顺序)指向该顶点的弧的指针。,存放的是指针
        ③firstout域,保存的是指向由这个顶点作为弧尾的第一条弧的指针,存放的是指针
        |----|-------|--------|
        |data|firstin|firstout|  这个就是顶点节点的格式。
        |----|-------|--------|
    3.十字链表的弧节点
    下面的是十字链表弧节点的结构,一共含有五个部分
        |-------|-------|-----|-----|----|
        |tailvex|headvex|hlink|tlink|info|
        |-------|-------|-----|-----|----|
        ①tailvex表示的这个弧的尾顶点,存放的是一个index
        ②headvex表示的这个弧的头顶点,存放的是一个index
        ③hlink表示的是指向和这个弧的弧头(第二个参数)相同的弧的指针,存放的是指针
        ④tlink表示的是指向和这个弧的弧尾(第一个参数)相同的弧的指针,存放的是指针
        ⑤info可以存储这个弧的权值,或者是指向保存有关该弧的各种描述信息的一个指针。

    //十字链表的特点:
    1.由这个节点的定义可以看出,hlink指向的所有节点,第二个参数都是一样的,tlink指向的所有单元,第一个值都是一样的。
    2.只能存储有向图。
    3.弧节点的个数刚好等于所有图中弧的个数。
    4.十字链表也不是唯一的。
    5.①所有的顶点节点都构成一个数组,方便随机查找。然后顶点的firstout域,指向一条将该顶点作为弧尾的弧节点,这个弧节点的tlink指向的是另外一个将与该
        弧有同弧尾的另一条弧,实际上就是指向的是,由数组中该顶点引出的另外一条弧。一般这些节点都向右排列开来。这样的话,由该顶点出来的所有的所有的弧,都在一层上。这个功能和邻接表相同。
      ②先将所有的顶点的出度都按照上面的方式排开,然后就构成了一个类似邻接表的型式。
      ③十字链表的顶点节点的第二个参数是firstin,也就是存放的是指向这个顶点的弧的地址。恰好,弧节点的第三个参数指向的是和这个弧的弧头(第二个参数)相同的弧的指针,也就是说,保存的都是
        指向这个顶点的弧的地址。这样,由顶点第二个参数出发,可以找到第一个指向该顶点的弧,然后再由这个弧的第三个参数,可以找到另外一个指向该顶点的弧。
    综合②③的话,就能找到所有和这个顶点"有连接关系"的弧。

*/
typedef struct ArcNode//弧节点
{
    int tailvex;
    int headvex;//该弧的弧头的弧尾的顶点索引值
    struct ArcNode * hlink;
    struct ArcNode * tlink;//指向和该弧的弧头或者弧尾相同的弧
    int info;//描述权值的部分
}ArcNode;
typedef struct VNode//顶点节点
{
    char data;//顶点的数据域
    ArcNode * firstin;//指向第一个指向该顶点的弧节点
    ArcNode * firstout;//指向第一个由该顶点发出的弧节点
}VNode;
typedef struct{//图的定义
    VNode vertexes[100];//顶点表,最多可以存放100个。
    int vexnum;
    int arcnum;//实际这个图中顶点个数以及弧的条数
}GLGraph;


////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/*
    图的第四种存储方法:邻接多重表。针对的是无向图。
    //为什么要使用邻接多重表?
        在邻接表中每一条边如边(i,j),它有两个节点,分别在第i个和第j个链表中,这给某些图的操作带来不便。因为邻接表的表节点主要存储的是顶点所在的index和指针,并没有
    存储边的信息,假如,你要删除一个边,或者删除一条边(2,5),那么就需要在数组中找到2然后删除与之相连的adjvex为5的节点,然后再在数组中找到5,在删除这个链表中
    adjvex为2的节点。比较麻烦,所以引入了邻接多重表。

    //邻接多重表的构造:其实主要需要改变的就是将链表原来只表示index的格式,改变成为可以表示一条边的节点格式。
    1.邻接多重表仍然需要两种节点信息,一种是表头的节点,一种是表节点。表头节点仍然采用的是顺序存储。这个都欧式一样的。
    2.邻接多重表的表头节点结构定义:
        |----|---------|
        |data|firstedge|这个是邻接多重表的表头格式。
        |----|---------|
    其中firstdege保存的是一个指针,这个指针指向这个顶点连接的第一个边的节点。
    3.邻接多重表的表节点结构定义:
        |------|-------|-------|------|-------|------|
        | mark |  ivex | ilink | jvex | jlink | info |这个是邻接多重表表节点的结构格式。
        |------|-------|-------|------|-------|------|
     其中:
        mark为int类型的,用来标记,是否被遍历过。被遍历过的话,标记为1,否则标记为0。保存的是int类型的值,或者枚举类型的值。
        ivex和jvex分别表示该边的一对顶点。 保存的是int类型的index值。
        ilink和jlink表示的是分别下一条依附于顶点ivex的边或者下一条依附于顶点jvex的边。保存的是指针。
        info用来表示的是这个边的权值。或者有关这个边描述的一个指针。

    //邻接多重表的特点:
    1.由表节点的定义可以看出,ilink指向的节点,和该节点具有相同的ivex域。jlink指向的节点,和该节点具有相同的jvex域。
    2.邻接多重表一般是用来保存无向图的。
    3.边节点的个数刚好等于所有图中边的个数。
    4.邻接多重表也不是唯一的。
    5.表节点中,ivex和jvex可以交换位置,因为是无向图,这两个谁在前,谁在后,都是无所谓的。
    6.①表头节点组成一个数组,方便随机存取。
      ②连接的时候,顶点的firstedge指针指向与这个顶点相连接的第一个边,然后这个边的ilink指向与其自身的ivex顶点相同的边(其实就是依附于这个顶点的另外一条边),结构图向右连接排列开来。
      这样的话,顺着一个顶点的指针域,就能很快的找到该依附于该顶点的所有边。
      ③在邻接多重表中,一条边之存储一次。比如第一个链表(第一行节点)中存储了(0,5)这个边,那么顶点5的firstedge指针就应该指向第一行节点的这个(0,5)节点,然后在顺着这个(0,5)节点,
      又因为(0,5)节点中,5在jvex中存着呢,所以,要找依附于顶点5的另外一个边的话,就应该找jlink域指向的指针。这个指针一般是纵向排列的。这样,顺着这个顺序,一路找下来就找完了所有的
      依附于5号顶点的边。
    //其实,无向图的邻接表存储和邻接多重表的存储很像,不同的是,前者将一条边分开存储在两个节点中,而后者将一条边存放在一个节点中,更方便增删。
*/
typedef struct ArcNode//表节点的定义
{
    bool mark;//C++支持bool类型
    int ivex;
    int jvex;
    struct ArcNode * ilink;
    struct ArcNode * jlink;
    int info;
}ArcNode;
typedef struct VNode//顶点的定义
{
    char data;
    ArcNode * firstedge;
}VNode;

typedef struct //图的定义
{

    VNode adjmulist[100];
    int vexnum;
    int arcnum;
}AMLGraph;




int main(void)
{
        ALGraph picture1;//注意这个初始化过程。
        picture1.vertices[0].data='a';//picture是图的名字,然后picture.vertices[0]就是这个图第一个顶点形成的头结点,包含两个域,data域和first域。
        picture1.vertices[0].first=NULL;  
        printf("%c\n",picture1.vertices[0].data);

        //注意下面的程序是不可能运行的,因为你的节点类型已经重复定义了。
        GLGraph picture2;
        picture2.vertexes[0].data='A';
        picture2.vertexes[0].firstin=NULL;
        picture2.vertexes[0].firstout=NULL;
        printf("%c\n",picture2.vertexes[0].data);






    system("pause");
    return 0;
}




////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/*
    图的深度优先搜索原理:(类似于树的先序遍历!!!新东方老师讲的很好)
    1.首先要明白,图可以分为连通图和非联通图。而非连通图中的每一个连通分量就是连通图。所以先从连通图开始考虑问题,然后所有的连通图合起来就是原来的非连通图。
    2.因为图的结构比较复杂,有可能1号顶点已经访问过了,过了一会又回溯回来了,还会进行一次访问。这个时候可以这么处理,针对图中的每一个顶点,设置一个数组,bool visited[n-1];
    如果,该顶点被访问过了,那么其值就是1,未被访问过,其值就是0.这样,每次访问顶点的时候,先检查一下该顶点是否被访问过了就避免二次访问了。
    3.对于连通图而言,其遍历过程如下所述:
        ①从图中某个顶点V0出发访问这个顶点
        ②然后依次从V0的各个未被访问的邻接点出发深度优先搜索遍历图(这个是一个递归过程)
        ③直到图中所有的和V0有路径相通的顶点都被访问到,这个连通图就被访问完成了。
    4.那么对于非联通图呢?
    其实,对于非联通图的搜索很简单。先将图中所有的顶点的访问标志设置为FALSE,然后搜索图中每个顶点。如果未被访问,则以该顶点为起始点,进行深度优先搜索遍历(也
    就是调用连通图的那个函数。)否则继续检测下一个顶点。
    说直白一点,就是对图中每一个顶点调用一次联通图遍历搜索程序。
    5.那么一个图,应该才去连通方式还是非连通方式访问呢?
    答:因为你不知道是否是联通的,所以肯定是作为非联通图来访问的。
    6.注意:新东方和王道的书,给的代码有没有实现的函数,而天勤的和王晓凤的都实现了全部细节。
*/
//下面写代码。先写一些需要被调用的小函数,防止报错。
void VisitFunc(int v)
{
//访问v顶点
}
int FirstAdjVex(AMLGraph G,int v)
{
//求出v顶点在G图中的第一个邻接顶点
}
int NextAdjVex(AMLGraph G,int v ,int w)
{
    //在图G中,除了W顶点,和V顶点连接的下一个顶点
    //如果没有了的话,就返回0.
}

bool visited[100];//访问标记数组。//这个是全局变量。
//下面开始写图的遍历过程
/*
    函数说明:
    1.所传的参数是图
    2.流程
    ①先将这个图中所有的节点都设置为全为访问过
    ②循环针对每一个节点进行遍历访问
*/
void DFSTraverse(AMLGraph G)//现在假设这个图是以邻接多重表存储的。
{
    for(int v=0;v<G.vexnum;v++)
    {
        visited[v]=false;
    }//将所有的节点都设置为未被访问过。做一个初始化。
    for(int v=0;v<G.vexnum;v++)
    {
        if(!visited[v])
        {
            DFS(G,v);//有多少个连通分量,就需要调用多少次DFS。每次调用DFS的v都是不同连通分量中的一个顶点。(和广度优先一样)
        }
    }
}
//联通图的遍历
/*
    函数说明:
    1.首先,参数是图和开始搜索的顶点号
    2.针对连通图的遍历流程
    ①先将该顶点的标志设置为true
    ②访问这个顶点
    ③然后访问第一个与之相连的顶点,然后跳转至①(这个其实就是一个递归)
    ④等到③完成之后,在访问第二个与所给顶点相连接的顶点在跳转至①
    ⑤直到所有的与所给顶点相连接的所有顶点都被访问完成为止。
    //注意,上面的遍历流程用到了递归和for循环。
    对于连通图而言,不论是广度优先还是深度优先,仅需要从图中任意一个顶点出发,就能访问图中所有的顶点。

*/
void DFS(AMLGraph G,int v)//v表示的是从第v个顶点开始深度优先搜索
{
    visited[v]=true;
    VisitFunc(v);

    for(int w=FirstAdjVex(G,v);w!=0;w=NextAdjVex(G,v,w))//注意这个for循环很好。
    {
        if(!visited[w])
        {
            DFS(G,w);//递归调用。注意,这个递归的递归信任问题转换到了for循环的条件判断,如果这个顶点没有与之相连的顶点了,那么w=0,循环结束,函数返回至上一层。
        }
    }
}










////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/*
    图的广度优先搜索原理:(类似于树的层次遍历)
    1.广度优先搜索也分连通图和非联通图。
    2.通要设置数组来标识是否被访问过了。
    3.对于连通图,其遍历过程如下所示:
        ①从图的某个顶点V0出发,先访问这个顶点,然后再去访问V0的所有未被访问的邻接点,访问一个,将一个入队一次。
        ②然后按照这些顶点依次被访问的先后顺序依次访问他们的邻接点(也就是依次出队,访问被出队元素的邻接点),直到图中所有与V0有通路的顶点都被访问到。
        //提到了先后次序,故需要一个队列结构。实际上应该是这样的,先访问V0的邻接点,访问一个,入队一个,直到访问完成所有的V0的邻接点。然后
          在出队,出队的时候采用递归算法即可。
        //其实,广度优先遍历是这样的,先访问所有与V0路径为1的邻接点,然后在访问所有与V0路径长度为2邻接点,这样以此类推。是一层一层的,类似于树的层次遍历,所以需要一个队列。
    4.对于非联通图而言:
        在完成了上述①②之后,则另选一个图中未被访问的顶点作为起始点,重复①②过程,直到图中所有的顶点都被访问到为止。实现的时候,需要使用for循环,
        然后依次判断该顶点是否被访问过了。这个和图的深度优先搜索是一样的。
*/
//注意,广度优先的话,需要先定义一个队列。
typedef struct{
    int data[100];//队列需要存放的是各个顶点的编号,所以是int类型的
    int front;
    int rear;

}Queue;
void initQueue(Queue & Q)//初始化队列,引用类型的
{
    Q.rear=Q.front=0;
}
bool isEmpty(Queue Q)
{
    if(Q.front==Q.rear) return true;
    else return false;
}
bool EnQueue(Queue & Q,int x)
{
    if((Q.rear+1)%100==Q.front) return false;
    Q.data[Q.rear]=x;
    Q.rear=(Q.rear+1)%100;
    return true;
}
bool DeQueue(Queue & Q,int & x)
{
    if(Q.front==Q.rear) return false;
    x=Q.data[Q.front];
    Q.front=(Q.front+1)%100;
    return true;
}
Queue Q;//创建队列的这个函数应该写在main函数中



//非联通图的搜索函数写法
void BFSTraversa(AMLGraph G)//假设图是以AMLGraph存储的
{
    for(int index=0;index<G.vexnum;index++)
    {
        visited[index]=false;//初始化,将所有的标志全部变为false;
    }

    initQueue(Q);//初始化队列

    for(int index=0;index<G.vexnum;index++)
    {
        if(!visited[index])
        {
            BFS(G,index);//如果该节点没有被访问过的话,就先访问这个节点。这个和深度优先搜索是一样的。//有多少个联通分量,那么就需要调用多少次BFS。每次调用的v都是不同连通分量的一个顶点。
        }
    }
}

void BFS(AMLGraph G,int v)//广度优先遍历的联通图遍历算法//对于连通图而言,不管是广度优先还是深度优先,仅需从图中任意一个顶点出发,就能访问图中的所有顶点。
{
    VisitFunc(v);//先访问这个v节点
    visited[v]=true;
    EnQueue(Q,v);//让v节点入队

    while(!isEmpty(Q))//如果队列不是空的
    {
        int u;
        DeQueue(Q,u);//对头元素出队,并且置为u

        for(int w=FirstAdjVex(G,u);w>=0;w=NextAdjVex(G,u,w))//这个算法的理解:假如,BFS函数传的是V0顶点,则BFS函数可以实现将与所有与V0有通路的所有元素都访问一遍。也就是将包含V0的那个联通分量访问完。
        {                                                   //实现过程:先访问v0然后在使用for循环来访问v0的所有路径为1的节点,然后在将其依次入队,然后,由于外面有while循环,所以会再将其出队一个,然后
            if(!visited[w])                                 //在访问该节点与之相连的路径为1的,未被访问的元素,然后再将其入队。接着再出队一个上一层入队的元素,再去访问它。。。就这样不断循环。
            {                                               //最后的结果是:队列中所有元素都出队了,但是没有元素再被加入队列了,因为都被访问了。
                VisitFunc(w);                                       //需要注意的是,这个函数很巧妙很巧妙。按道理来讲没必要先将v访问之后在将其入队,在出队,麻烦。但是为了语法以及算法上的统一,还是这么做了。
                visited[w]=true;
                EnQueue(Q,w);
            }
        }
    }
}

//深度优先遍历和广度优先遍历的函数都是很好很好的。理解算法太重要!!!多去思考算法。这些算法都很精妙。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值