数据结构系列4——图
图
图的概念
线性表可以是空表,树可以是空树,但是图不可以是空图,图的顶点集V一定非空,但边集E可以为空,此时的图中只有顶点而没有边
基本定义
- 有向图(尖括号)
- 无向图(圆括号)
- 简单图和多重图
- 简单图:不存在重复边,不存在顶点到自身的边
- 多重图与简单图相对
- 完全图(完全简单图)
- 对无向图,$ | E | \(的取值范围是\) 0 \(到\) n ( n - 1 ) / 2 \(,有\) n (n - 1 ) / 2 \(条边的无向图称为完全图(边数为\) n - 1, n - 2, \cdots , 1 $)
- 对有向图,$ | E | \(的取值范围是\) 0 \(到\) n ( n - 1 ) \(,有\) n (n - 1 ) \(条边的无向图称为完全图,有向完全图中任意两个顶点之间都存在方向相反的两条弧(边数为\) n $个 $ n - 1 $)
- 子图
- 生成子图,顶点一样,只有边不同,并非所有E和V的子集都有可能构成子图,可能E的某些边中的顶点不在V中
- 连通、连通图和连通分量
- 连通:顶点v和顶点w有路径存在
- G中任意两个顶点都是连通的称为连通图。否则称为非连通图
- 连通分量:无向图中的极大连通子图(注意是子图,即所有的边和顶点都在,生成树不是连通分量),通俗理解成联通的一块,顶点不能缺也不能多
- 区分极大连通子图和极小连通子图,连通图只有一个极大联通子图,就是它自身,非连通图有多个极大联通子图,称为极大连通子图是因为再加入一个点就会导致其不连通,一个连通图的生成树是极小连通子图(注意是生成树,而不是最小生成树,这里只关注边,而不关注边上的权值),去掉一条边就会导致其不连通
非连通情况下边最多的情况:由 n - 1 个顶点(n - 2 条边)构成一个完全图,此时任意再加入一条边则变成连通图
- 强连通、强连通图和强连通分量
- 强连通:一对顶点v和w,从v到w和从w到v之间都有路径,注意:有路径不一定就强连通,可能只满足部分到达
- G中任何一对顶点都是强连通(互相都有路径)则称为强连通图
- 强连通分量:有向图中的极大强连通子图
有向图强连通情况下边最少的情况:至少需要n条边,构成一个环路(必须是环路,否则仍不满足连通即任意两边可达)
- 生成树和生成森林
- 生成树:包含图中全部顶点的一个极小连通子图,n - 1 条边,砍去一条边就会变成非连通图,再加上一条边就会形成一个回路
- 生成森林:非连通图中,连通分量的生成树构成了非连通图的生成森林
- 顶点的度、入度和出度
- 无向图的度之和 = 边的条数的两倍
- 有向图的入度 = 出度 = 边的条数(可以理解为无向图没有入度和出度的概念,仅强调度)
- 稠密图和稀疏图
- 稀疏图,$ | E | < | V | log | V | $,反之则为稠密图
- 路径长度
- 路径上边的数目
- 简单路径和简单回路
- 简单路径,顶点不重复出现的路径
- 简单回路,除第一个和最后一个顶点外,其余顶点不重复出现的回路
- 有向树
- 一个顶点的入度为0,其余顶点的入度为1的有向图,称为有向树
图的存储
邻接矩阵法
- 行代表出边,对应出度,列代表入边,对应入度
- 设图G对应的矩阵为$ A $, $ A^n[i][j] \(对应从顶点\) i \(到顶点\) j \(的长度为\) n $的路径的数目
const int MAXN = 1e3;
int G[MAXN][MAXN];
// 常用
const int INF = 0x7fffffff;
fill(G[0], G[0] + MAXN * MAXN, 0); // init
fill(G[0], G[0] + MAXN * MAXN, INF); // init
邻接表法
- 表示指向的下一个顶点
- 对于无向图,所需的存储空间为$ O(|V| + 2 |E|) \(,对于有向图,所需的存储空间为\) O(|V| + |E|) $,前者的倍数是两倍是因为每条边在邻接表中出现了两次(对每条边来说由于重复存储,复杂度是数量的两倍)
- 邻接表表示不唯一
const int MAXN = 1e3;
vector<int> Adj[MAXN];
\\ new edge 1 -> 3
Adj[1].push_back(3)
\\ 带权图
struct node {
int v, w;
node(int _v, int _w) : v(_v), w(_w) {}
}
vector<node> Adj[MAXN];
\\ new edge 1-> 3, value equals 0
Adj[1].push_back(node(3, 0));
十字链表
- 针对有向图,每条弧都有有一个结点(仅对应一个结点),每个顶点也有一个结点
- 弧结点中,尾域
tailvex和头域headvex分别表示弧尾和弧头两个顶点,链域hlink指向弧头相同的下一条弧,链域tlink指向弧尾相同的下一条弧,info指向该弧的相关信息(权值等),注意,弧头表示指向的结点,弧尾表示出发的结点,不要混淆 - 顶点结点中,
data域存放顶点相关信息(顶点名称)firstin(指向该顶点)和firstout(该顶点指出)分别指向以该顶点为弧头和弧尾的第一个弧结点 - 图的十字链表表示法不唯一,但一个十字链表表示唯一的图
- 弧结点中,尾域


注意,这里的表示中,结点编号比实际位置大一
邻接多重表
- 针对无向图,在邻接表中,删除两顶点之间的边要分别在两个顶点中执行遍历操作,效率较低,在邻接多重表中,每条边用一个结点表示,每个顶点也用一个结点表示
- 弧结点中,
mark为标志域,可用来标记该条边是否被搜索过,ivex和jvex为该边依附的两个顶点在图中的位置,ilink指向下一条依附于该顶点ivex的边,jlink指向下一条依附于顶点jvex的边,info指向和边相关的各种信息的指针域 - 顶点节点中,
data存储该顶点的相关信息,firstedge域指示第一条依附于该顶点的边 - 在邻接多重表中,所有依附于同一个顶点的边串联在同一个链表中,每个边结点同时连接在两个链表中
- 与邻接表的差别,同一条边在邻接表中用两个结点表示,在邻接多重表中用一个结点表示
- 弧结点中,



图的遍历
与树不同,的结点之间有层级关(父子结点),因此结点的访问有一定顺序,而图不同,顶点之间可能重复访问,因此需要记录是否访问(
vis[MAXN]和inq[MAXN])
概念
- 图的遍历与图的连通性的关系
- 对于无向图,
bfsTrave()或dfsTrave()中调用bfs()或dfs()的数量(一次dfs()或bfs()访问该(顶点所在的)连通分量所能到达的所有顶点) - 对有向图,不能用
dfs()或bfs()的数量表示,因为可能存在如下情况,连通但是不是强连通分量,非强连通分量不能通过一次bfs()或dfs()访问所有顶点
- 对于无向图,
- 广度优先的生成树、广度优先的生成树和各自的生成森林
- 一给定图的邻接矩阵表示唯一,其广度优先或深度优先的生成树也是唯一的,但是邻接表存储表示不唯一,故其广度优先或深度优先的生成树是不唯一的
- 连通图才有生成树,否则是生成森林
- 基于矩阵的遍历所得到的dfs和bfs序列是唯一的,基于邻接表的遍历所得到的dfs和bfs序列是不唯一的
- 注意,各边权值相等时,广度优先算法可以解决单源最短路问题,搜索的层数可以对应到达的路径长度,维护
cnt控制层数

广度优先搜索(BFS)
- 需要一个辅助队列,空间复杂度$ O(|V|) \(,时间复杂度,采用邻接表时\) O(|V| + |E|) \(,采用邻接矩阵时\) O(|V|^2) $
- 邻接矩阵
const int MAXN = 1e3;
const int INF = 0x7fffffff;
int n, G[MAXN][MAXN];
// 记录是否入队而不是是否访问,可以减少不必要的重复访问(入队了但是还未访问,会重复入队该顶点相邻的顶点)
bool inq[MAXN] = {false};
void bfs(int u) {
queue<int> q;
q.push(u);
inq[u] = true;
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v = 0; v < n; v++) {
if (inq[v] == false && G[u][v] != INF) {
q.push(v);
inq[v] = true;
}
}
}
}
void bfsTrave() {
for (int u = 0; u < n; u++) {
if (inq[u] == false) bfs(u);
}
}
- 邻接表
const int MAXN = 1e3;
const int INF = 0x7fffffff;
vector<int> Adj[MAXN];
int n;
// 记录是否入队而不是是否访问,可以减少不必要的重复访问(入队了但是还未访问,会重复入队该顶点相邻的顶点)
bool inq[MAXN] = {false};
void bfs(int u) {
queue<int> q;
q.push(u);
inq[u] = true;
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < Adj[u].size(); i++) {
int v = Adj[u][i];
if (inq[v] == false) {
q.push(v);
inq[v] = true;
}
}
// c++ 11
// for (auto v : Adj[u]) {
// if (inq[v] == false) {
// q.push(v);
// inq[v] = true;
// }
// }
}
}
}
void bfsTrave() {
for (int u = 0; u < n; u++) {
if (inq[u] == false) bfs(u);
}
}
深度优先搜索(DFS)
- 需要一个递归空间栈,空间复杂度$ O(|V|) \(,时间复杂度,采用邻接表时\) O(|V| + |E|) \(,采用邻接矩阵时\) O(|V|^2) $
- 邻接矩阵
const int MAXN = 1e3;
const int INF = 0x7fffffff;
int n, G[MAXN][MAXN];
bool vis[MAXN] = {false};
void dfs(int u, int depth) {
vis[u] = true;
for (int v = 0; v < n; v++) {
if (vis[v] == false && G[u][v] != INF) {
dfs(v, depth + 1);
}
}
}
void dfsTrave() {
for (int u = 0; u < n; u++) {
if (vis[u] == false) dfs(u, 1);
}
}
- 邻接表
const int MAXN = 1e3;
const int INF = 0x7fffffff;
vector<int> Adj[MAXN];
int n;
bool vis[MAXN] = {false};
void dfs(int u, int depth) {
vis[u] = true;
for (int i = 0; i < Adj[u].size(); i++) {
int v = Adj[u][i];
if (vis[v] == false) dfs(v, depth + 1);
}
// c++ 11
// for (auto v : Adj[u]) {
// if (vis[v] == false) dfs(v, depth + 1)
// }
}
void dfsTrave() {
for (int u = 0; u < n; u++) {
if (vis[u] == false) dfs(u, 1);
}
}
- bfs标记层号(从出发顶点依次向外扩散)
const int MAXN = 1e3;
const int INF = 0x7fffffff;
struct node {
int v;
int layer;
node() {}
node(int _v, int _layer) : v(_v), layer(_layer) {}
};
vector<node> Adj[MAXN];
int n;
// 记录是否入队而不是是否访问,可以减少不必要的重复访问(入队了但是还未访问,会重复入队该顶点相邻的顶点)
bool inq[MAXN] = {false};
// 为该子图,若要整个图,加一个bfsTrave
void bfs(int s) {
queue<node> q;
node start;
start.v = s;
start.layer = 0;
q.push(start);
inq[start.v] = true;
while (!q.empty()) {
node cur = q.front();
q.pop();
int u = cur.v;
for (int i = 0; i < Adj[u].size(); i++) {
node next = Adj[u][i];
next.layer = cur.layer + 1;
if (inq[next.v] == false) {
q.push(next);
inq[next.v] = true;
}
}
// c++ 11
// for (auto next : Adj[u]) {
// next.layer = cur.layer + 1;
// if (inq[next.v] == false) {
// q.push(next);
// inq[next.v] = true;
// }
// }
}
}

浙公网安备 33010602011771号