1:图的存储
数组表示法—–邻接矩阵
邻接表———-邻接表与逆邻接表结合成十字链表,邻接表求入度需要整体遍历,不方便
十字链表——-有向图
邻接多重表—-无向图
这里先介绍最常用也比较复杂的十字链表
2:十字链表
定义:邻接表与逆邻接表相结合的一种链式储存结构,拥于邻接表易求出度的特点,又有逆邻接表易求入度的特点。每一条弧有一个结点,每一个顶点也有一个结点
先弄清楚几个概念(有向图):
弧结点各元素含义:
tailvex:弧尾的位置(是个int型变量)
headvex:弧头的位置(是个int型变量)
hlink:弧头相同的下一条弧(都在同一条链表上)
tlink:弧尾相同的下一条弧(都在同一条链表上)
info:存放弧的相关信息,比如权值(网)
顶点结点各元素含义:
data:存储顶点相关信息,比如顶点的名字(A,B,C····)
firstin:指向该顶点为弧头的第一条弧
firstout:指向该顶点为弧尾的第一条弧
这很像像稀疏矩阵的十字链表,但是有一个很大的不同:
(1):弧与顶点是相互独立的,弧总体是由两条链表构成(弧头相同的链表,弧尾相同的链表)
(2):顶点正常来讲是只需要data域的(所有顶点其实存放在一个数组中的),但是却多了两条链域(firstin,firstout),弧头(尾)的第一条弧 —直接把存放data的数组和两条链域相联系,形成一个整体。
代码如下:
//十字链表储存结构
#define MAX_VERTEX_NUM 20 //顶点数目最多20个,顶点放在数组中
//弧结点
typedef struct ArcBox
{
int tailvex, headvex; //弧的尾与头位置
struct ArcBox *hlink, *tlink; //弧头相同的链域,弧尾相同的链域
}ArcBox;
//顶点结点
typedef struct
{
int nodenum; //为广度优先遍历获取顶点在数组的位置
char data; //顶点数据
ArcBox *firstin, *firstout; //分别指向该顶点的第一条入弧和出弧
}VexNode;
typedef struct {
VexNode xlist[MAX_VERTEX_NUM]; //表头向量
int vexnum, arcnum; //有向图的顶点数与弧数
}OLGraph;
3:构建有向图十字链表
基本步骤:
(1):顶点的创建(相当于数组赋值,先和两条链表没有联系)
1:输入顶点值(字母)
2:记录顶点位置
3:firstin,firstout置空
(2):弧结点的创建
1:输入弧的两个顶点(两个字母)
2:获取弧的两个结点位置
3:分配空间并赋值
4:修改顶点(数组)的firstin,firstout
注意事项
这里输入的是字母%c,采用scanf()输入会有几个问题。
(1):输入字母后的回车会被下一次scanf()读进去,导致会跳一次输入(跳过这次其实是回车)!!!
解决方法:%c前面务必要有一个空格
scanf(“(空格)%c”, &G.xlist[i].data);
(2):连续读入两个字母,如果输入有空格,必须用getchar()把空格读走,即用来吃掉上一句的enter!!!
(3):如果你阅读过C Primer Plus的话,会很清楚的知道scanf的%c读入的特殊性。
代码如下:
//创建有向图的十字链表
int CreatDG(OLGraph &G, int vexnum, int arcnum)
{
for (int i = 0; i < vexnum; i++) {
printf("Please enter %d data:", i+1);
scanf(" %c", &G.xlist[i].data); //输入顶点值
G.xlist[i].nodenum = i; //记录顶点的位置
G.xlist[i].firstin = NULL; //初始化指针
G.xlist[i].firstout = NULL;
}
char v1, v2; //一条弧的两个顶点
int i, j; //弧尾位置,弧头位置
for (int k = 0; k < G.arcnum; k++) {
printf("Please enter %d arc(tail -> head): ", k + 1);
scanf_s(" %c", &v1); //输入弧的第一个顶点,这里%c前面必须要有一个空格!
getchar(); //把输入的空格读走
v2 = getchar(); //输入弧的第二个顶点
i = LocateVex(G, v1); //获取弧的第一个节点位置
j = LocateVex(G, v2); //获取弧的第二个节点位置
ArcBox *p = (ArcBox *)malloc(sizeof(ArcBox)); //分配弧结点空间
p->tailvex = i; p->tlink = G.xlist[i].firstout; //i放进tailvex,tlink(尾巴出去firstout)
p->headvex = j; p->hlink = G.xlist[j].firstin; //j放进headvex,hlink(头进来firstin)
G.xlist[j].firstin = G.xlist[i].firstout = p; //入弧与出弧的插入(数组的firstin firstout有了指向)
}
return 0;
}
4:深度优先遍历
基本定义:顾名思义是按深度优先,即一直往下走 ,而广度优先是一层一层走完在向深处走。
核心思想:递归调用。
基本步骤:
(1):把所有结点标记为false
(2):对没有访问过的结点深度优先遍历
代码如下:
/******************************************************
** DFS深度优先遍历
*******************************************************/
bool DFSvisted[20]; //这里定义最多有20个结点
void DFSTraverse(OLGraph &G)
{
for (int i = 0; i < G.vexnum; i++) //先把每个结点都标记为假,没有访问过
DFSvisted[i] = false;
for (int i = 0; i < G.vexnum; i++) {
if (!DFSvisted[i]) //对没有访问的结点,按深度优先遍历
DFS(G, i);
}
}
void DFS(OLGraph &G, int v)
{
DFSvisted[v] = true; //访问结点标识--真
printf("%c\t",G.xlist[v].data);
ArcBox *DFStemp = G.xlist[v].firstout; //用DFStemp来存储该节点的第一个出去的弧
while (DFStemp)
{
if (!DFSvisted[DFStemp->headvex]) //访问此结点第一个出去的弧
DFS(G, DFStemp->headvex);
DFStemp = DFStemp->tlink; //该结点还有指向其他的弧
}
}
5: 广度优先遍历
基本定义:广度优先是一层一层走完在向深处走。就像跑道(由内而外)一圈走完再往下一圈走
核心思想:这里没有用很明显的递归调用,因为这里需要用队列记录访问过结点的先后顺序
几个难点:
(1):循环结束时即要队列空,也要for循环G.vexnum次
(2):访问过的结点入队列
(3):获取出队的元素所在位置,以便在十字链表中找到该节点为弧尾的第一个结点
******************************************************
** BFS广度优先遍历
*******************************************************/
bool BFSvisted[20]; //这里定义最多有20个结点
void BFSTraverse(OLGraph &G)
{
for (int i = 0; i < G.vexnum; i++) //先把每个结点都标记为假,没有访问过
BFSvisted[i] = false;
LinkQueue Q; //初始化队列
InitQueue(Q);
ArcBox *BFStemp;
char HeadQueue; //出队元素(队头)第一次会出现一进队就出队
int headnum = 0; //队头元素的位置,才好找到十字链表的下一个
for (int i = 0; i < G.vexnum; i++) {
if (!BFSvisted[i]) {
BFSvisted[i] = true;
printf("%c\t", G.xlist[i].data); //访问元素
EnQueue(Q, G.xlist[i].data); //把访问的元素都入队
while (!QueueEmpty(Q)) {
DeQueue(Q, HeadQueue); //出队
//printf("出队:%c\t", DeQueue(Q, HeadQueue));
for (int j = 0; j < G.vexnum; j++) {
if (G.xlist[j].data == HeadQueue)
headnum = G.xlist[j].nodenum; //获取队头位置
}
BFStemp = G.xlist[headnum].firstout; //用BFStemp来存储该节点的第一个出去的弧
while (BFStemp) {
if (!BFSvisted[BFStemp->headvex]) {
BFSvisted[BFStemp->headvex] = true; //把结点都标记为真
printf("%c\t", G.xlist[BFStemp->headvex].data); //访问元素
EnQueue(Q, G.xlist[BFStemp->headvex].data); //把访问的元素都入队
}
BFStemp = BFStemp->tlink;
}
}
}
}
}
下面是遍历与队列情况
BFSTraverse:
A 出队:A D C 出队:D E B 出队:C 出队:E 出队:B F 出队:F G H 出队:G 出队:H I 出队:I
6: 主函数
#include "stdafx.h"
#include "graph.h"
#include "queue.h"
#pragma warning(disable:4996)
int main()
{
OLGraph G;
printf("Please enter vexnum and arcnum: ");
scanf("%d %d", &G.vexnum, &G.arcnum); //输入结点数,弧数
CreatDG(G, G.vexnum, G.arcnum); //创建有向图
printf("\nDFSTraverse:\n");
DFSTraverse(G); //深度优先遍历
printf("\nBFSTraverse:\n");
BFSTraverse(G); //广度优先遍历
printf("\n");
return 0;
}
7: 输出结果
8: 后记
(1):数据结构自己尽量用C语言完成,方便更多的同学看懂核心。BFS广度优先遍历需要队列支持,所以自己临时构建了一个队列及实现队列基本功能。而C++直接有队列库,比较简单。
(2):里面有一个很重要的就是字母的输入scanf的%c用法,希望能牢记,如果用C++的string会更简单些,C语言没有这个库
(3):感谢以下博主文章对我的启发与帮助
https://blog.csdn.net/jkay_wong/article/details/6957336
https://blog.csdn.net/qiye005/article/details/46650933
(4):实验代码百度云链接(VS2017)
链接: https://pan.baidu.com/s/14xjFP51xeaDTA1xR_btVSg 密码: y226
(5):若你觉得文章对你有启发,请注明出处。