图
本文前半部分参考的是浙大MOOC上陈越老师说讲解,但听到后面一半,觉得有点晦涩难懂,便去看了《大话数据结构》,因此后面的主要内容也是大多参考这本书,除了最后一节的关键路径算法,有点搞不太懂,其他的总算让我有了浅显的了解了。说实话,数据结构实在是太难啃了,书本上讲的所以然,还得靠多刷题,多做实验来解决。
什么是图
- 多对多
- 线性表,树都是图的特殊情况
- 包含:
- 一组顶点,通常用V(Vertex)表示顶点集合
- 一组边,通常用E(Edge)表示边的集合
- 边是顶点对,(v,w),可以互通
- 有向边,<v,w>,单向
- 不考虑重边和自回路
抽象数据类型定义
类型名称,图
数据对象集,G(V,E),由一个非空的有限顶点集合v(顶点必须是要存在的)和一个有限集合E组成
操作集
- Graph Create() 建立并返回空图
- Graph InsertVertex(Graph G,Vertex v)将v插入G中
- Graph InsertEdge(Graph G, Edge e)将边e插入G中
- void DFS(Graph G,vertex V),从顶点v出发深度优先遍历图G
- void BFS(Graph G,Vertex v),从顶点v出发宽度优先遍历图G
- void shortestPath(Graph ,Vertex v, int Dist[]),计算图G中顶点V到任意其他顶点的最短距离
- Void MST (Graph G),计算图G的最小生成树
常见术语
无向图,图中所有的边都是没有方向的
有向图,图中所有的边有可能是双向也可能是单边
权重,每个边具有数值
网络,带权重的图称为网络
邻接点,有边直接相连的顶点
度,跟这个顶点相关的所有顶点的个数
有向图:
- 出度,从这个点发出的边数,对应行非0元素的个数
- 入度,指向改点的边数,对应列非0元素的个数
在程序中表示一个图
邻接矩阵
邻接矩阵,用二维数组表示一个图,G[N] [N] ,若两个顶点存在直接连通的边,则G[N] [N]设置为1,否则,设置为0
问题,对于邻接矩阵存储,如何省下一半空间:
->只存下三角矩阵
- 只管简单,容易理解
- 方便检查任意一对顶点间是否存在边
- 方柏霓找任意顶点的所有邻接点
- 方便计算任意顶点的度
缺点:
- 浪费空间,(存稀疏图,点很多,边很少,有大量无效元素)
- 对稠密图(完全图)较合算
- 浪费时间,统计稀疏图
邻接表
其为一个链表的集合
G[N] 为指针数组,对应矩阵每行一个链表,只存非0元素
邻接表的表示法是不唯一的,对于网络,结构中要增加权重的域,一定要够稀疏才合算
- 方便找任意顶点的所有邻接点
- 节约稀疏图的空间,需要N个头指针+2E个节点,每个节点至少两个域
- 方便计算任意顶点的度,对无向图正确,对有向图,只能计算出度,入度的话吗,则需要构造逆邻接表,存指向自己的边
缺点
无法简便检查任意一对顶点间是否存在边
图的遍历
深度优先搜索(DFS)
先按照一条路走,若无全通则前进,若都已全通则原路返回,直至返回到起点
类似与树的先序遍历
若有N个顶点,E条边,时间复杂度为:
-
邻接表存储图,O(N+E)
-
邻接矩阵存储图,O(N*N)
深度优先搜索(DFS) void DFS(vertex v) { visited[V] = true; for(v的每个邻接点w) if(!visited(W)) DFS(w) }
广度优先搜索(BFS)
相当于树的层序遍历
若有N个顶点,E条边,时间复杂度为:
- 邻接表存储图,O(N+E)
- 邻接矩阵存储图,O(N*N)
void BFS(Vertex v)
{
visited[v] = true;
Enqueene(v, Q);//入队
while (!Isempty(Q))
{
v = Dequeue(Q);//出队
for (v的每个邻接点w)
{
if (!visited(w))
{
visited[w] = true;
Enqueue(w,Q);
}
}
}
}
DFS比较适合判断图中是否有环,寻找两个节点之间的路径,有向无环图(DAG)的拓扑排序,寻找所有强连通片(SCC),无向图中寻找割点和桥等;
BFS则比较适合判断二分图,以及用于实现寻找最小生成树(MST),如在BFS基础上的Kruskal算法。还有寻找最短路径问题(如Dijkstra算法)。
图不连通怎么半
连通:如果从V到w存在一条(无向)路径,则称v和w是连通的
路径:v到w的路径是一系列顶点{v,v1,v2,…,vn,w}的集合,其中任一对相邻的顶点间都有图中的边。
路径的长度:路径中的边数(如果带权,则是所有边的权重和)
简单路径:如果v到w之间的所有顶点都不同。
回路:起点等于终点的路径
连通图:图中任意两顶点均连通
连通分量:无向图的极大连通子图
- 极大顶点数:再加1个顶点就不连通
- 极大边数:包含子图中所有顶点相连的所有边
强连通:有向图中顶点v和w之间存在双向路径,则称v和w是强连通的
强连通图:有向图中任意两顶点均强连通
强连通分量:有向图的极大强连通子图
每调用一次DFS(V),就把v所在的连通分量遍历了一遍。BFS也是一样
//处理未连通的点
void LIstcomponents(Graph G)
{
for (each V in G)
{
if (!visited[v])
{
DFS(V)//or BFS(V)
}
}
}
邻接矩阵表示图
#include<stdio.h>
#include<iostream>
#include <malloc.h>
using namespace std;
//图的邻接表,表示法
#define MaxVertexNum 100 //最大顶点数设置为100
typedef int vertex;
typedef int weightype;
typedef char datatype;
//定义边
typedef struct ENode* ptrToENode;
struct ENode {
vertex v1, v2; //有向边<v1,v2>
weightype weight;
};
typedef ptrToENode Edge;
//邻接点定义
typedef struct AdjvNode* PtrToAdjvNode;
struct AdjvNode
{
vertex adjv;//邻接点的小标
weightype weight;//边权重
PtrToAdjvNode Next;//指向下一个邻接点的指针
};
//顶点表头结点的定义
typedef struct Vnode
{
PtrToAdjvNode FirstEdge;//边表头指针
datatype Data;//存储点的数据
//很多情况下,顶点无数据,此事Data可以不用出现
}Adjlist[MaxVertexNum];//Adjlist是邻接表类型
//定义图
typedef struct GNode* ptrToGNode;
struct GNode
{
int Nv;//顶点数
int Ne;//边数
Adjlist G;
};
typedef ptrToGNode LGraph;//以邻接表存储的图类型
//初始化表
LGraph CreateGraph(int VertexNum)
{
vertex v;
LGraph Graph;
Graph = (LGraph)malloc(sizeof(struct GNode));
Graph->Nv = VertexNum;
Graph->Ne = 0;
//初始化邻接表头指针
//默认顶点编号从0开始,到(Graph->NV-1)
for (v = 0; v < Graph->Nv; v++)
{
Graph->G[v].FirstEdge = NULL;
}
return Graph;
}
void InsertEdge(LGraph Graph, Edge E)
{
//插入边<v1,v2>
//为v2建立新的邻结点
PtrToAdjvNode NewNode;
NewNode = (PtrToAdjvNode)malloc(sizeof(struct AdjvNode));
NewNode->adjv = E->v2;
NewNode->weight = E->weight;
//将v2插入V1的表头
NewNode->Next = Graph->G[E->v1].FirstEdge;
Graph->G[E->v1].FirstEdge = NewNode;
//如果是无向图,还要插入边<v2,v1>
//为v1建立新的邻接点
NewNode = (PtrToAdjvNode)malloc(sizeof(struct AdjvNode));
NewNode->adjv = E->v1;
NewNode->weight = E->weight;
//将v1插入V2的表头
NewNode->Next = Graph->G[E->v2].FirstEdge;
Graph->G[E->v2].FirstEdge = NewNode;
}
LGraph BuildGraph()
{
LGraph Graph;
Edge E;
vertex v;
int Nv, i;
cin >> Nv;