图:邻接表、逆邻接表和十字链表

邻接表

数据结构

邻接表由表头顶点表和顶点单链表构成:
表头节点表(顺序存储)

数据域指针域
datafirstarc
datafirstarc

顶点单链表(链式存储)

数据域数据域指针域
adjvexinfonextarc

例子
在这里插入图片描述

typedef int VertexId;

//弧上存储信息
struct InfoType{
	...
}

//顶点单链表元素
struct ArcNode{
	VertexId adjvex;
	struct ArcNode *nextarc;
	struct InfoType *arc;
}

//表头顶点表
struct GraphNode{
	VertexId ver;
	struct ArcNode *arc;
}

建图、删图

数据由road[RoadNum][InfoNumsOfRoad]cross[CrossNum][InfoNumsOfCross]构成:

  • road包含7个信息:#(id,length,speed,channel,from,to,isDuplex),id从RoadIdShift==5000开始,连续递增,显然通过id查找road是O(1)操作。
  • cross包含:#(id,roadId,roadId,roadId,roadId) ,每个cross最多和4个road相连,roadId==-1时表示无此路,id从1开始,连续递增,显然通过id查找cross是O(1)操作。
#define CrossNum 200
#define RoadNum 200
#define InfoNumsOfRoad 7 //二维数组形参声明,列数需要显式声明,一旦修改,所有含此形参的函数都需要修改,宏定义更方便调整
#define InfoNumsOfCross 5
//vernum是真实的节点数量,比CrossNum稍小
void CreateGraph(GraphNode **g,int vernum,int road[][InfoNumsOfRoad],int cross[][InfoNumsOfCross]){
	ArcNode *p,*q;
	InfoType *inf;
	(*g) = (GraphNode *)malloc(sizeof(GraphNode)*CrossNum);//返回动态数组首地址,此时二级指针可以使用下标运算符访问了
	for(int i=0;i<vernum;i++){  //按照crossId顺序 初始化表头顶点表
		(*g)[i].ver=cross[i][0];
		(*g)[i].arc=NULL;//初始化不要忘了!!!
	} 
	for(int i=0;i<vernum;i++){//遍历表头顶点表,存储图的弧
		q=(ArcNode*)malloc(sizeof(ArcNode));
		q->adjvex=-1; //设置头节点,个人编程习惯
		q->nextarc=NULL; 
		(*g)[i].arc = q;

		for(int j=0;j<4;j++){
			if(cross[i][j+1]!=-1){ //道路是通的
				int rid = cross[i][j+1]; 
				if(road[rid-RoadIdShift][4]==(*g)[i].ver){ //cross为道路的from节点
					p=(ArcNode*)malloc(sizeof(ArcNode));
					p->adjvex = road[rid-RoadIdShift][5];//保存道路to节点

					//存储道路信息
					inf = (InfoType *)malloc(sizeof(InfoType));
                    inf->direct = j;
                    inf->roadid = road[rid-RoadIdShift][0];
                    inf->length = road[rid-RoadIdShift][1];
                    inf->Maxspeed = road[rid-RoadIdShift][2];
                    inf->Channelnum = road[rid-RoadIdShift][3];
                    inf->Singledirect = road[rid-RoadIdShift][6];
                    ...//其他信息

 					p->info = inf;
                    q->nextarc = p;
                    q = p;
                    q->nextarc = NULL;					
				}
				else if(road[rid-RoadIdShift][5]==(*g)[i].ver){
					p=(ArcNode*)malloc(sizeof(ArcNode));
					p->adjvex=road[rid-RoadIdShift][4];

					//存储道路信息
					inf = (InfoType *)malloc(sizeof(InfoType));
                    inf->direct = j;
                    inf->roadid = road[rid-RoadIdShift][0];
                    inf->length = road[rid-RoadIdShift][1];
                    inf->Maxspeed = road[rid-RoadIdShift][2];
                    inf->Channelnum = road[rid-RoadIdShift][3];
                    inf->Singledirect = road[rid-RoadIdShift][6];
                    ...

 					p->info = inf;
                    q->nextarc = p;
                    q = p;
                    q->nextarc = NULL;
				}
			}
		}
	}
}

删图时注意链表释放内存的处理方式,使用临时变量temp指针,在删除当前节点的时候先记录下一节点的地址。

void DestryGraph(GraphNode **g,int vernum){
    for(int i=0;i<vernum;i++){
        ArcNode *del = (*g)[i].arc->nextarc;
        if(del!=NULL) {
            ArcNode *temp = del->nextarc;
            while(temp){
                free(del);
                del = temp;
                temp = temp->nextarc;
            }
        }
    }
    free((*g));
}

malloc创建二维数组的三种方式:利用二级指针、利用指针数组、一维数组模拟二维数组

打印邻接表

//打印邻接表法创建的图
void PrintGraph(const GraphNode *g, int vernum)
{
    ArcNode *p, *q;
    printf("图的顶点以及其对应的邻接顶点为:\n");  //打印邻接点
    for (int i = 0; i<vernum; i++) {
        printf("%d :\n", g[i].ver);
        p = g[i].arc;
        q = p->nextarc;//头指针
        while (NULL != q) {
            printf("\troadid-%d\t\tcross%d->cross%d\n", q->info->roadid,g[i].ver,q->adjvex);
            q = q->nextarc;
        }
        printf("\n");
    }
}

图的基本操作

查找节点v的第一个邻接节点
//返回顶点v的第一个邻接顶点,若无,返回-1
int FirstAdjVex(const GraphNode *g, int v) {
    if (NULL != g[v].arc->nextarc)
        return (g[v].arc->nextarc->adjvex - 1); // v从0开始,id从1开始
    else return -1;
}
查找节点v的相对于w的下一个邻接节点
//返回v的相对于w的下一邻接顶点,若无,返回-1
int NextAdjVex(const GraphNode *g, int v, int w) {
    ArcNode *p = g[v].arc->nextarc; //头指针
    while (NULL != p) {
        if (p->adjvex - 1 == w) { // v从0开始,id从1开始;w是返回值,和v一样
            p = p->nextarc;
            if (p != NULL)
                return p->adjvex - 1;  // v从0开始,id从1开始
            else
                return -1;
        }
        else {
            p = p->nextarc;
        }
    }
    return -1;
}
删除指定边

思路:获取边的弧头和弧尾,然后将弧元素删除

//删除弧头为In,弧尾为Out的边,注意cross要减1
int DeleteEdgeOfGraph(GraphNode *g,int In,int Out){
    ArcNode *p = g[In].arc->nextarc;
    ArcNode *tmp;
    while(p!=NULL&&p->adjvex-1!=Out){
        tmp = p;
        p=p->nextarc;

    }
    if(p->adjvex-1!=Out){
        return -1;
    }
    tmp->nextarc=p->nextarc;
}
删除指定节点

思路:删除所有和指定节点邻接的节点

void DeleteVertexOfGraph(GraphNode *g,int vernum,int v){
    g[v].arc->nextarc=NULL;
    for(int i=0;i<vernum;i++){
        ArcNode *p = g[i].arc,*tmp;

        while(p->nextarc!=NULL && p->nextarc->adjvex-1!=v){
            p=p->nextarc;
        }
        if(p->nextarc!=NULL && p->nextarc->adjvex-1==v){
            if(p->nextarc->nextarc!=NULL) {
                tmp = p->nextarc;
                p->nextarc = p->nextarc->nextarc;
                free(tmp);
            }
            else{
                tmp = p->nextarc;
                p->nextarc=NULL;
                free(tmp);
            }
        }
    }
}
DFS遍历

简述算法过程

1、任选一顶点作始点 v ,访问该顶点
2、沿深度方向,依次遍历 v 的未访问邻接点——直到本次遍历结束
3、一次遍历完时,若有未访问顶点:任选一个未访问顶点作起始点,GOTO第二步

void DFS(GraphNode *g,int v,int(*Visit)(GraphNode *g,int v),int visited[],int vernum){
	//从顶点v出发,深度优先搜索遍历连通图G
	visited[v]=1;
	Visit(g,v);
	
	int w;
	for(w=FirstAdjVex(g,v);w!=-1;w=NextAdjVex(g,v,w)){
		if(!Visited[w]) DFS(g,w,Visit,visited,vernum);
	}
}
void DFSTraverse(GraphNode *g, int vernum, int(*Visit)(GraphNode *g, GraphNode *b, int v), int v) {
    int visited[CrossNum];
    for (int i = 0; i < vernum; i++) { //初始化
        visited[i] = 0;
    }

    DFS(g,w,Visit,visited,vernum); //提前加这句话能够指定遍历起点
    for (v = 0; v < vernum; v++)  //查询标志位数组,查缺补漏
        if (!visited[v]) DFS(g,w,Visit,visited,vernum);
}
BFS遍历

使用队列(queue)来实现:
1、把根节点放到队列的末尾。
2、每次从队列的头部取出一个元素,查看这个元素所有的下一级元素,把它们放到队列的末尾。

void BFSTraverse(GraphNode *g,int vernum,int(*Visit)(GraphNode *g,int v),int v){
    int visited[CrossNum];
    for(int i=0;i<vernum;i++){
        visited[i]=0;
    }
    queue<int> que;
    visited[v]=1;
    Visit(g,v);
    que.push(v);
    while(!que.empty()){
        int currver=que.front();
        if(visited[currver]==0) {//队头再判断一次是否遍历过
            visited[currver] = 1;
            Visit(g, currver);
        }
        que.pop();
        ArcNode *curr = g[currver].arc->nextarc;

        while(curr!=NULL){
            int nextver = curr->adjvex-1;
            if(visited[nextver]==0){  //压栈时,遍历过的节点不必在压入栈中,可以减少运运算量,但还是可能造成同一节点多次入栈,所以对队头还要再判断一次
                que.push(nextver);
            }
            curr=curr->nextarc;
        }
    }
}

逆邻接表

邻接表获取节点的出度非常容易,但是获取它的入度就需要遍历整个图,计算量太大,我们可以在邻接表的基础上在构建一个逆邻接表,降低这一操作的复杂度。

//逆邻接表
void ReverseGraph(GraphNode **g, int vernum, int cross[][InfoNumsOfCross], GraphNode **b) {
    (*b) = (GraphNode *)malloc(sizeof(GraphNode)*CrossNum); //初始化表头顶点表
    for (int i = 0; i < vernum; i++) { //初始化表头结点表
        (*b)[i].ver = cross[i][0];
        ArcNode *q = (ArcNode *)malloc(sizeof(ArcNode)); //头结点
        q->adjvex = -1; //头结点标识
        q->nextarc = NULL; //防止野指针
        (*b)[i].arc = q;
    }

    //遍历所有弧,构建逆邻接表
    for (int i = 0; i < vernum; i++) {
        ArcNode *p1 = (*g)[i].arc->nextarc;//顶点i的第一个出度
        while (p1 != NULL) {
            int k = p1->adjvex - 1;//记录p1的节点编号,注意cross偏移量
            ArcNode *p2 = (ArcNode*)malloc(sizeof(ArcNode));
            p2->adjvex = i+1;
            p2->nextarc = (*b)[k].arc->nextarc; //k是i的出度,所以头插法插入b[k]
            (*b)[k].arc->nextarc = p2;
            p1 = p1->nextarc;//顶点i的下一个出度
        }
    }
};

十字链表Orthogonal List

十字链表是为了便于求得图中顶点的度(出度和入度)而提出来的。它是综合邻接表和逆邻接表形式的一种链式存储结构。

在十字链表存储结构中,有向图中的顶点的结构如下所示:
在这里插入图片描述
其中data表示顶点的具体数据信息,而firstIn则表示指向以该顶点为弧头的第一个弧节点。而firstOut则表示指向以该顶点为弧尾的第一个弧节点。为了表示有向图中所有的顶点,采用一个顶点数组存储每一个结点,如下图所示。
在这里插入图片描述
另外,在十字链表存储结构中,有向图中的每一条弧都有一个弧结点与之对应,具体的弧结点结构如下所示:
在这里插入图片描述
其中的tailVex表示该弧的弧尾顶点在顶点数组xList中的位置,headVex表示该弧的弧头顶点在顶点数组中的位置。hLink则表示指向弧头相同的下一条弧,tLink则表示指向弧尾相同的下一条弧。

从十字链表的数据结构来看,每一个顶点对应两个链表:以该顶点为弧尾的弧结点所组成的链表以及以该顶点为弧头的弧结点所组成的链表。

如下图所示的一个有向图:
在这里插入图片描述
其对应的顶点以及弧结点如下所示。拿结点A说明,该结点对应两个链表(绿色和黄色标记的)。绿色链表表示以结点A为弧头的弧组成的链表。黄色链表表示以结点A为弧尾的弧组成的链表。
在这里插入图片描述
注意:这里的弧节点是有向的,即CD和DC不是同一个元素。见图中顶点C的firstOut指针上挂着的第二个弧节点和D的firstOut指针上挂着的第三个弧节点。

struct OLArcNode{
    int tailVex;
    int headVex;
    OLArcNode *tLink;
    OLArcNode *hLink;
    InfoType *info;
};

struct OLGraph{
    int ver;
    OLArcNode *firstIn;
    OLArcNode *firstOut;
};

建图

void CreateOLGraph(OLGraph **g,int vernum,int road[][7],int cross[][5]) {
    (*g) = (OLGraph *) malloc(sizeof(OLGraph) * vernum);
    for (int i = 0; i < vernum; i++) {
        (*g)[i].ver = cross[i][0];
        (*g)[i].firstIn = NULL;
        (*g)[i].firstOut = NULL;
    }

    //遍历所有节点,建立所有以当前节点为弧尾的链表,动态分配链表元素
    for (int i = 0; i < vernum; i++) {
        OLArcNode *tp = (OLArcNode *) malloc(sizeof(OLArcNode)); //头节点
        OLArcNode *hp = (OLArcNode *) malloc(sizeof(OLArcNode)); //头节点
        tp->tailVex = -1;
        tp->headVex = -1;
        tp->info = NULL;
        tp->tLink = NULL;
        tp->hLink = NULL;
        hp->tailVex = -1;
        hp->headVex = -1;
        hp->info = NULL;
        hp->tLink = NULL;
        hp->hLink = NULL;

        (*g)[i].firstIn = hp;
        (*g)[i].firstOut = tp;


        int currver = cross[i][0];
        for (int j = 1; j < 5; j++) {
            int curroad = cross[i][j];
            if (curroad != -1) {  //存在road,动态分配一个OLArcNode
                OLArcNode *p;
                if (road[curroad - RoadIdShift][4] == currver) { //from 方向
                    int nextver = road[curroad - RoadIdShift][5];
                    p = (OLArcNode *) malloc(sizeof(OLArcNode));
                    p->tailVex = currver;
                    p->headVex = nextver;
                    p->hLink = NULL;
                    p->tLink = NULL;

                    InfoType *inf = (InfoType *) malloc(sizeof(InfoType));
                    inf->roadid = curroad;
                    inf->direct = -1;
                    inf->length = road[curroad - RoadIdShift][1];
                    inf->Maxspeed = road[curroad - RoadIdShift][2];
                    inf->Channelnum = road[curroad - RoadIdShift][3];
                    inf->Singledirect = road[curroad - RoadIdShift][6];
                    //...
                    p->info = inf;
                } else {  //to 方向
                    int nextver = road[curroad - RoadIdShift][4];
                    p = (OLArcNode *) malloc(sizeof(OLArcNode));
                    p->tailVex = currver;
                    p->headVex = nextver;
                    p->hLink = NULL;
                    p->tLink = NULL;

                    InfoType *inf = (InfoType *) malloc(sizeof(InfoType));
                    inf->roadid = curroad;
                    inf->direct = -1;
                    inf->length = road[curroad - RoadIdShift][1];
                    inf->Maxspeed = road[curroad - RoadIdShift][2];
                    inf->Channelnum = road[curroad - RoadIdShift][3];
                    inf->Singledirect = road[curroad - RoadIdShift][6];
                    //...
                    p->info = inf;
                }

                //hLink和tLink两个链表都采用头插法
                OLArcNode *q = (*g)[i].firstOut->tLink;
                (*g)[i].firstOut->tLink = p;
                p->tLink = q;

//                q = (*g)[i].firstIn->hLink;
//                (*g)[i].firstIn->hLink=p;
//                p->hLink=q;
            }

        }
    }

    //遍历所有节点,所有弧元素已经完成动态分配,建立以当前节点为弧头的链表的指针链接
    for (int i = 0; i < vernum; i++) {
        int currver = (*g)[i].ver;
        for (int j = 1; j < 5; j++) {
            int curroad = cross[i][j];
            if (curroad != -1) {  //存在road(弧),建立hLink链表
                OLArcNode *p;
                if (road[curroad - RoadIdShift][4] == currver) { //from 方向
                    int lastver = road[curroad - RoadIdShift][5];

                    //获取弧元素p
                    OLArcNode *p = (*g)[currver-1].firstOut->tLink;
                    while (p != NULL && p->headVex != lastver) {
                        p=p->tLink;
                    }
                    if(p==NULL){
                        printf("unkown error!");
                        exit(EXIT_FAILURE);
                    }

                    //将弧元素p头插法加入hLink链表
                    OLArcNode *q = (*g)[lastver-1].firstIn->hLink;
                    (*g)[lastver-1].firstIn->hLink = p;
                    p->hLink = q;

                }
                else {  //to 方向
                    int lastver = road[curroad - RoadIdShift][4];

                    //获取弧元素p
                    OLArcNode *p = (*g)[currver-1].firstOut->tLink;
                    while (p != NULL && p->headVex != lastver) {
                        p=p->tLink;
                    }
                    if(p==NULL){
                        printf("unkown error!");
                        exit(EXIT_FAILURE);
                    }

                    //将弧元素p头插法加入hLink链表
                    OLArcNode *q = (*g)[lastver-1].firstIn->hLink;
                    (*g)[lastver-1].firstIn->hLink = p;
                    p->hLink = q;

                }
            }
        }
    };
}

打印十字链表

void PrintOLGraph(OLGraph *g,int vernum){
    for(int i=0;i<vernum;i++){
        OLArcNode *tp = g[i].firstOut->tLink;
        OLArcNode *hp = g[i].firstIn->hLink;

        //打印以节点i为弧尾为链
        printf("\n以结点%d为弧尾的链\n",g[i].ver);
        while(tp!=NULL){
            printf("\t%d->%d\t",tp->tailVex,tp->headVex);
            tp=tp->tLink;
        }
        //打印以节点i为弧头的链
        printf("\n以结点%d为弧头的链\n",g[i].ver);
        while(hp!=NULL){
            printf("\t%d->%d\t",hp->tailVex,hp->headVex);
            hp=hp->hLink;
        }
        printf("\n------------------\n");
    }
}

十字链表

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值