图的存储结构
1.邻接矩阵
使用两个数组表示图,一个一维数组存储图中的顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息
无向图例子
无向图的边数组为对称矩阵即aij = aji
顶点的度为该顶点所在行的元素和
求顶点的邻接点就是该行中为1的顶点
有向图例子
有向图的边数组并不对称
从v0到v1的弧在数组中记为a01 = 1
顶点的入度为列和,出度为行和
网图例子(带权值的有向图)
顶点到自身的权值为0,不存在直接连线的权值为无穷
邻接矩阵存储结构定义
typedef char VertexType //顶点类型
typedef int EdgeType //边上的权值类型
const int MAXVEX = 100; //最大的顶点数
const int INFINITY = 65535 //该数表示无穷大
typedef struct
{
VertexType vexs[MAXVEX]; //顶点表
EdgeType arc[MAXVEX][MAXVEX]; //边表,又称邻接矩阵
int numVertexes, numEdges; //顶点数和边数
}MGraph;
无向图创建
#include <iostream>
using namespace std;
void CreatMGragh(MGraph *G)
{
int i,j,k,w;
cout<<"输入顶点数和边数"<<endl;
cin>>&G->numVertexes>>&G->numEdges;
for(i=0; i<G->numVertexes; i++)
{
cin>>&G->vexs[i]; //输入顶点信息
}
for(i=0; i<G->numVertexes; i++)
for(j=0; j<G->numVertexes; j++)
G->arc[i][j] = INFINITY; //邻接矩阵初始化
for(k=0; k<G->numEdges; k++)
{
cout<<"输入边的下标i和上标j,并输入权值";
cin>>&i;
cin>>&j;
cin>>&w;
G->arc[i][j] = w;
G->arc[j][i] = G->arc[i][j]; //由于是无向图,邻接矩阵对称
}
}
创建的时间复杂度为O(n+n^2+e)
2.邻接表
邻接矩阵对于边比顶点少的图,会造成空间上的浪费,于是考虑将顶点存在一维数组中,而边采用链式存储结构保存,类似于树中的孩子表示法。
原则:
1.图中顶点采用一维数组存储
2.每个顶点v的所有邻接点构成一个线性表,采用单链表的形式,无向图称为顶点v的边表,而有向图则称为顶点v作为弧尾的出边表
无向图例子
顶点结点由数据域data和指针域firstedge(指向边表的第一个结点)组成
边表结点由adjvex邻接点域(存储某个顶点的邻接顶点在顶点表中的下标)和next指针域(指向边表中下一个结点指针)
有向图例子
有向图的逆邻接表:即每条边都是以该顶点v为弧头
带权值网图
在有向图的基础上邻接表的边表结点中添加一项权重即可
结点定义
typedef char VertexType; //顶点类型
typedef int EdgeType //边上的权值类型
typedef struct EdgeNode //边表结点
{
int adjvex; //邻接点域,指向该顶点对应的下标
EdgeType weight; //权值
struct EdgeNode *next; //链域,指向下一个邻接点
}EdgeNode;
typedef struct VertexNode //顶点表结点
{
VertexType data;
EdgeNode *firstedge;
}VertexNode,AdjList[MAXVEX];
typedef struct
{
AdjList adjList;
int numVertexes,numEdges; //图中当前顶点数和边数
}GraphAdjList;
无向图的邻接表创建
void CreatALGraph(GrapAdjList *G)
{
int i,j,k;
EdgeNode *e;
cout<<"输入顶点数和边数"<<endl;
cin>>&G->numVertexes>>&G->numEdges; //输入顶点数和边数
for(i=0; i<G->numVertexes; i++)
{
cin>>&G->adjList[i].data; //输入顶点数据信息
G->adjList[i].firstedge = NULL; //将边表置为空表
}
for(k=0; k<G->numEdges; k++)
{
cout<<"输入边的下标i和上标j,并输入权值";
cin>>&i;
cin>>&j;
e = (EdgeNode*)malloc(sizeof(EdgeNode)); //向内存申请空间 生成边表结点
e->adjvex = j; //该边结点的邻接点域指向j
e->next = G->adjList[i].firstedge; //该节点的下一个指向当前顶点结点的下一个
G->adjList[i].firstedge = e; //更新当前顶点指针指向e
//重新申请新的对称边结点 调换顶点顺序
e = (EdgeNode*)malloc(sizeof(EdgeNode)); //向内存申请空间 生成边表结点
e->adjvex = i; //该边结点的邻接点域指向i
e->next = G->adjList[j].firstedge; //该节点的下一个指向当前顶点结点的下一个
G->adjList[j].firstedge = e; //更新当前顶点指针指向e
}
}
以上生成的办法应用了单链表创建中的头插法,对于n个顶点和e条边容易得出空间复杂度为O(n+e)
3.十字链表(有向图的优化)
将邻接表和逆邻接表结合,主要针对有向图,期望了解其入度与出度问题
data | firstin | firstout |
firstin表示入边表头指针(即该顶点为弧尾);
firstout表示出边表头指针(即该顶点为弧头);
tailvex | headvex | headlink | taillink |
tailvex是指弧起点在顶点表的下标
headvex是指弧终点在顶点表下的下标
taillink是指出边表指针域,指向起点相同的下一条边
headlink是指入边表指针域,指向中点相同的下一条边
若是网,可多加一个weight域存储权值
有向图例子
除结构复杂外,其创建图的算法的时间复杂度和邻接表是相同的
4.邻接多重表(无向图的优化)
当更关注边的操作(删除、添加),而不太关注顶点的操作时,可采用邻接多重链表
顶点表结点结构不变,重新定义边表结点结构如下
ivex | ilink | jvex | jlink |
ivex和jvex是某条边依附的两个顶点在顶点表中的位置下标。
ilink指向依附顶点ivex的下一条边
jlink指向依附顶点jvex的下一条边
无向图例子如下
若想遍历某个顶点的所有邻接点,采用firstedge->jvex(第一个)--ilink-->ivex(第二个)--jlink-->jvex(第三个)依次下去直到空
其与邻接表的区别仅在于同一条边在邻接表中用两个结点表示,而在邻接多重表中只用了一个结点表示。
5.边集数组(有向图/网表示)
使用两个一维数组构成,一个数组存储顶点信息,另一个存储边的信息,这个边数组每个元素包含起点的下标、终点的下标和权
这样的结构适合在对边依次进行处理的操作,不太适合对顶点相关的操作
有向图例子如下
图的遍历
1.深度优先遍历(利用递归实现)
也称深度优先搜索,简称DFS。类似于树的前序遍历,主要思想是从任意顶点,按一个方向(类似于先遍历左子树),遍历其邻接点、其邻接点的邻接点......直到尽头后,开始回退(类似于查看右子树)查看有无遗漏,直至回退到起点。
邻接矩阵的存储方式进行DFS(时间复杂度为O(n^2))
typedef int Bootlean; //Boolean为布尔类型
Boolean visted[MAX]; //访问标志的数组
//邻接矩阵的深度优化递归算法
void DFS(MGraph G,int i)
{
int j;
visited[i] = TRUE; //该节点访问标志位置1
cout<<"G.vexs[i]"; //对结点进行操作
for(j = 0; j<G.numVertexes; j++) //循环访问所有顶点
{
if(G.arc[i][j] == 1&& !visited[j]) //若该顶点与原顶点是邻接关系,且未被访问
DFS(G,j); //对未访问的邻接顶点进行递归调用
}
}
//邻接矩阵的深度遍历操作
void DFSTraverse(MGraph G)
{
int i;
for(i=0;i<G.numVertexes;i++)
visited[i] = FALSE;
for(i=0;i<G.numVertexes;i++)
if(!visited[i]) //对未访问的顶点调用DFS
DFS(G,i); //如果是连通图,即任意两顶点之间均有边,该指令只执行一次
}
邻接表的存储方式进行DFS(时间复杂度为O(n+e))
对于点多边少的稀疏图来说,邻接表结构使算法在时间效率上大大提高
typedef int Bootlean; //Boolean为布尔类型
Boolean visted[MAX]; //访问标志的数组
//邻接表的深度优化递归算法
void DFS(MGraphAdjList GL,int i)
{
EdgeNode *p;
visited[i] = TRUE; //该节点访问标志位置1
cout<<GL->adjList[i].data; //对结点进行操作
p = GL->adjList[i].firstedge;
while(p) //p不为空
{
if(!visited[p->adjvex]) //未被访问的边表结点
DFS(G,p->adjvex); //对未访问的邻接顶点进行递归调用
p = p->next; //指向下个边表结点
}
}
//邻接矩阵的深度遍历操作
void DFSTraverse(MGraphAdjList GL)
{
int i;
for(i=0;i<GL->numVertexes;i++) //初始化所有顶点状态
visited[i] = FALSE;
for(i=0;i<G->numVertexes;i++)
if(!visited[i]) //对未访问的顶点调用DFS
DFS(GL,i); //如果是连通图,即任意两顶点之间均有边,该指令只执行一次
}
2.广度优先遍历(利用队列实现)
又称广度优先搜索(BFS),主要思想是利用队列这种结构,压入第一个顶点,弹出该顶点时,将其邻接顶点加在队尾,依次弹出和压入。类似于树的层序遍历。
邻接矩阵的广度优先遍历算法
void BFSTraverse(MGraph G)
{
int i,j;
Queue Q;
for(i=0; i<G.numVertexes; i++) //初始化访问标志数组
visited[i] = FALSE;
InitQueue(&Q); //初始化队列
for(i=0;i<G.numVertexes;i++) //循环所有顶点
{
if(!visited[i]) //未被访问的顶点
{
visited[i] = TRUE;
std::cout<<G.vexs[i]; //对顶点的操作
EnQueue(&Q,i); //将该顶点压入队列
while(!QueueEmpty(Q)) //如果队列不为空
{
DeQueue(&Q,&i); //出队列,赋给i
for(j=0;j<G.numVertexes;j++) //循环所有顶点
{
if(G.arc[i][j] == 1&& !visited[j]) //未被访问且和当前顶点为邻接顶点
{
visited[j] = TRUE;
std::cout<<G.vexs[j]; //对顶点操作
EnQueue(&Q,j); //将此顶点压入队列
}
}
}
}
}
}
邻接表的广度优先遍历算法
void BFSTraverse(GraphAdjList GL)
{
int i,j;
EdgeNode *p;
Queue Q;
for(i=0; i<GL->numVertexes; i++) //初始化访问标志数组
visited[i] = FALSE;
InitQueue(&Q); //初始化队列
for(i=0;i<GL->numVertexes;i++) //循环所有顶点
{
if(!visited[i]) //未被访问的顶点
{
visited[i] = TRUE;
std::cout<<G.vexs[i]; //对顶点的操作
EnQueue(&Q,i); //将该顶点压入队列
while(!QueueEmpty(Q)) //如果队列不为空
{
DeQueue(&Q,&i); //出队列,赋给i
p = GL -> adjList[i].firstedge;
while(p) //循环该顶点的所有边表结点
{
if(!visited[j]) //未被访问
{
visited[j] = TRUE;
std::cout<<G.vexs[j]; //对顶点操作
EnQueue(&Q,j); //将此顶点压入队列
}
p = p->next; //下一个边表结点
}
}
}
}
}
深度优先适合目标比较明确,以找到目标为主要目的,而广度优先遍历适合不断扩大范围时找到最优解的情况。