本篇博客是考研期间学习王道课程 传送门 的笔记,以及一整年里对数据结构知识点的理解的总结。希望对新一届的计算机考研人提供帮助!!!
关于对图
章节知识点总结的十分全面,涵括了《王道数据结构》课程里的全部要点(本人来来回回过了三遍视频),其中还陆陆续续补充了许多内容,所以读者可以相信本篇博客对于考研数据结构“图”章节知识点的正确性与全面性;
但如果还有自主命题的学校,还需额外读者自行再观看对应学校的自主命题材料。
数据结构与算法
笔记导航
- 第一章 绪论
(无)
- 第二章 线性表
- 第三章 栈和队列
- 第四章 串-KMP(看毛片算法)
- 第五章 树和二叉树
- 第六章 图
⇦当前位置
- 第七章 查找(B树、散列表)
- 第八章 排序 (内部排序:八大排序动图演示与实现 + 外部排序)
数据结构与算法 复试精简笔记 (未完成)
- 408 全套初复试笔记汇总 传送门
如果本篇文章对大家起到帮助的话,跪求各位帅哥美女们,
求赞 、求收藏 、求关注!
你必考上研究生!
我说的,耶稣来了也拦不住!
精准控时:
如果不实际操作代码,只是粗略过一下知识点,需花费 80 分钟左右过一遍
这个80分钟是我在后期冲刺复习多次尝试的时间,可以让我很好的在后期时间紧张的阶段下,合理分配复习时间;
但是刚开始看这份博客的读者也许会因为知识点陌生、笔记结构不太了解,花费许多时间,这都是正常的。
重点!!!学习一定要多总结多复习!重复、重复、再重复!!!
食用说明书:
第一遍学习王道课程时,我的笔记只有标题和截图,后来复习发现看只看图片,并不能很快的了解截图中要重点表达的知识点。
所以再第二遍复习中,我给每一张截图中标记了重点,以及每张图片上方总结了该图片对应的知识点以及自己的思考。
最后第三遍,查漏补缺。
所以 ,我把目录放在博客的前面,就是希望读者可以结合目录结构去更好的学习知识点,之后冲刺复习阶段脑海里可以浮现出该知识结构,做到对每一个知识点熟稔于心!
请读者放心!目录展示的知识点结构是十分合理的,可以放心使用该结构去记忆学习!
注意(⊙o⊙)!,每张图片上面的文字,都是该图对应的知识点总结,方便读者更快理解图片内容。
稀疏图
e < nlogn
稠密图
// 邻接矩阵法
#define MAX_VERTEX_NUMBER 100 // 顶点数目的最大值
typedef struct
{
char vexter[MAX_VERTEX_NUMBER]; // 顶点表
int edge[MAX_VERTEX_NUMBER][MAX_VERTEX_NUMBER]; // 边表
int vexNum, arcNum; // 图的当前 顶点数 和 边数
} MGraph;
#define VERTEX_MAX_SIZE 100 // 最大顶点数
#define MAXINIT 1024 // 表示最大值
typedef char VertexType; // 顶点的数据类型,假设为char
typedef int ArcType; // 边的数据类型,假设为int(权值)
typedef struct
{
VertexType vers[VERTEX_MAX_SIZE]; // 顶点表
ArcType arcs[VERTEX_MAX_SIZE][VERTEX_MAX_SIZE]; // 邻接矩阵
int verNum, arcNum; // 图当前的顶点数和边数
} AMGraph;
// 确定顶点ver在图G的顶点表中的位置
int LocateVex(AMGraph G, VertexType ver)
{
for (int i = 0; i < G.verNum; i++)
{
if (G.vers[i] == ver)
{
return i;
}
}
return -1; // ver不存在,返回-1
}
// 采用邻接矩阵创建图,该算法时间复杂度O(n^2)
bool CreateUDN(AMGraph &G)
{
int i, j, info, rows, columns;
VertexType v1, v2;
cin >> G.verNum >> G.arcNum; // 输入总顶点数、总边数
for (i = 0; i < G.verNum; i++)
{
cin >> G.vers[i]; // 依次输入顶点的信息
}
memset(G.arcs, MAXINIT, sizeof(G.arcs));
for (j = 0; j < G.arcNum; j++) // 构造邻接矩阵
{
cin >> v1 >> v2 >> info; // 输入一条边依附的顶点以及权值
rows = LocateVex(G, v1); // 确定v1、v2的位置
columns = LocateVex(G, v2);
G.arcs[rows][columns] = info;
G.arcs[columns][rows] = info;
}
return true;
}
邻接矩阵 | 优点 | 缺点 |
---|---|---|
1 | 判断两个顶点之间是否有边很方便 | 增加、删除顶点不方便 |
2 | 计算各点的度很方便 | 统计边的数量不方便 |
3 | 空间复杂度高,导致只适合存储稠密图 |
上一节提到的邻接矩阵法,空间复杂度高,不适合存储稀疏图
邻接表是图的一种链式存储结构,适用存储稀疏图
// ? 邻接表法
#define MAX_VERTEX_NUMBER 100
// ! 弧
typedef struct ArcNode
{
int adjvex; // 弧指向哪个结点
struct ArcNode *next; // 指向下一条弧的指针
} ArcNode;
// ! 邻接表的顶点
typedef struct VNode
{
char data; // 顶点的信息
ArcNode *first; // 第一条弧
} VNode, AdjList[MAX_VERTEX_NUMBER];
// ! 使用邻接表存储的图
typedef struct
{
AdjList vertices;
int verNum, arcNum;
} ALGraph;
邻接矩阵 | 优点 | 缺点 |
---|---|---|
1 | 判断两个顶点之间是否有边很方便 | 增加、删除顶点不方便 |
2 | 计算各点的度很方便 | 统计边的数量不方便O(n^2) |
3 | 空间复杂度高,导致只适合存储稠密图O(n^2) |
邻接表法 | 缺点 | 优点 |
---|---|---|
1 | 判断顶点之间是否有边不方便 | 增加、删除顶点很方便 |
2 | 计算点的入度不方便 | 统计边的数目方便O(n+e) |
3 | 结点的链表顺序不唯一 | 空间效率高,适用于稀疏图O(n+e) |
4 | 无向图多了一份边的冗余数据 |
// ? 十字链表法
#define MAX_VERTEX_NUMBER 100
// ! 弧结点
typedef struct OLNode
{
int headVex; // 弧头顶点编号
int tailVex; // 弧尾顶点编号
int info; // 权值
struct OLNode *hLink; // 弧头顶点相同的下一条弧
struct OLNode *tLink; // 弧尾顶点相同的下一条弧
} OLNode;
// ! 十字链表的顶点
typedef struct VexNode
{
char data; // 顶点的信息
OLNode *firstIn; // 第一条以该顶点为弧头的弧
OLNode *firstOut; // 第一条以该顶点为弧尾的弧
} VexNode, CrossList[MAX_VERTEX_NUMBER];
// ! 使用邻接表存储的图
typedef struct
{
CrossList vertices;
int verNum, arcNum;
} OLGraph;
1)对无向图的存储是否有更优的方案呢?
2)邻接多重表存储无向图
// ? 邻接多重表
#define MAX_VERTEX_NUMBER 100
// ! 弧结点
typedef struct ArcNode
{
int iVex, jVex; // 弧的两个点
int info; // 权值
struct ArcNode *iLink, *jLink; // 依附各自顶点的下一条边
} ArcNode;
// ! 邻接多重表的顶点
typedef struct VexNode
{
char data; // 顶点的信息
ArcNode *firstEdge; // 与该顶点相连的第一条边
} VexNode, AdjList[MAX_VERTEX_NUMBER];
// ! 使用邻接表存储的图
typedef struct
{
AdjList vertices; // 存储图中顶点的数组
int verNum, arcNum;
} AMLGraph;
// 判断图G是否存在边或(x,y)
bool Adjacent(Graph g, int x, int y)
// 邻接矩阵:O(1)
return (a[x][y]!=0); // 不为空,存在边
// 邻接表法
// 最好情况:遍历第一条边就找到了
故时间复杂度:O(1)
// 最坏情况,遍历到最后一条边也没找到,因为顶点只能连接n-1条边,也就是|V|-1条边
故时间复杂度:O(|V|)
// 列出图G中与结点x邻接的边
int Neighbors(Graph g, int x)
// 邻接矩阵
遍历 x行 或者 x列,总数之和
// 邻接矩阵
// 出边情况
O(1): 0或者O(1)条边
o(|V|): 连接了|V|-1条边
// 入边情况
O(|E|): 遍历所有的边结点
// 在图G中插入顶点x
bool InsertVertex(Graph g, char x)
// 邻接矩阵
二维数组的赋0操作在初始化数组的时候就已经完成
故只需要对顶点数组的相应位置进行赋值操作即可 O(1)
// 邻接表
在顶点数组的相应位置进行赋值操作即可 O(1)
// 在图G中删除顶点x
bool DeleteVertex(Graph g, int x)
// 邻接矩阵 O(|V|)
1、看下图中标红的0,在删除顶点表、边表之后,如果把后面元素前移,那么就会有大量的元素移动,开销太大
2、所以直接将两个表的对应位置赋0,在在顶点结构体里设置一个bool的变量,判断此处是否为空位置
// 邻接表法
// 无向图
1、该结点的first指针为空,不存在与它向连的边 O(1)
2、遍历x结点的结点链发现有|V|-1条,就是说和每一个顶点它都相连了,这时就需要遍历所有结点的结点链(去删除冗余边)
最坏的情况:在遍历所有顶点的结点链的过程,和x相连的弧结点每次都在最后被找到,时间复杂度 O(|E|)
// 有向图
// 出边
遍历x顶点的弧结点链即可 O(1)~O(|V|),x最多连接|V|-1条边
// 入边
遍历所有边 O(|E|)
最小生成树不唯一(形态不同,权值和相同)
最小生成树 = 最大连通无环图,边数 = 顶点数 - 1;多一条回路,少一条不连通
Prim算法的执行过程十分类似于寻找图的最短路径的Dijkstra算法(见下节)
下面开始正式介绍算法的思想过程
先总结下代码关键:(牢牢记住三步)
isJoin[i] = true;
for (j = 1; j < verNum; j++) // 本循环使用来更新isJoin[],lowCost[]数组信息
{
if (!isJoin[j])
{
if (edge[i][j] < lowCost[j])
{
lowCost[j] = edge[i][j];
}
}
}
// 本循环用来选择加入的新结点
temp = 100;
for (k = 1; k < verNum; k++)
{
if (!isJoin[k])
{
if (lowCost[k] < temp)
{
temp = lowCost[k];
i = k; // ! 这一步就很妙,下一轮更新数据全靠i
// i记录下一个要加入的新结点
}
}
}
cout << lowCost[i] << " "; // 每轮循环输出加入的边的代价
flag++; // 每轮循环结束,代表有一个新结点加入
完整代码(王道)
#include
using namespace std;
#define MAX_SIZE 16
int ver, arc; // 输入的点数、边数
int edge[MAX_SIZE][MAX_SIZE]; // 邻接矩阵法
void PrimAlgorithm(int edge[MAX_SIZE][MAX_SIZE], int verNum)
{
bool isJoin[MAX_SIZE]; // ! 记录各结点是否已加入
int lowCost[MAX_SIZE]; // ! 记录 未加入结点 加入 结点集 的最小代价
memset(isJoin, false, sizeof(isJoin));
memset(lowCost, 100, sizeof(lowCost)); // 将这些数组初始化
int flag = 1; // 记录已经加入几个点,默认为1,就是初始加入的结点
int i = 0, j, k, temp;
lowCost[i] = 0; // ! 可省略
while (flag < verNum) // (flag == verNum)还循环个头!
{
isJoin[i] = true;
for (j = 1; j < verNum; j++) // 本循环使用来更新isJoin[],lowCost[]数组信息
{
if (!isJoin[j])
{
if (edge[i][j] < lowCost[j])
{
lowCost[j] = edge[i][j];
}
}
}
// 本循环用来选择加入的新结点
temp = 100;
for (k = 1; k < verNum; k++)
{
if (!isJoin[k])
{
if (lowCost[k] < temp)
{
temp = lowCost[k];
i = k; // ! 这一步就很妙,下一轮更新数据全靠i
}
}
}
cout << lowCost[i] << " "; // 每轮循环输出加入的边的代价
flag++;
}
}
int main()
{
cin >> ver >> arc;
for (int i = 0; i < ver; i++)
{
for (int j = 0; j < ver; j++)
{
cin >> edge[i][j];
}
}
PrimAlgorithm(edge, ver);
}
下面开始正式介绍算法思想过程
// 实现并查集其实很简单,不需要实现操作,等到加入新边的时候调用就行
FindRoot(int parent[],int s){
int x = s;
while(parent[x]>0)
x = parent[x];
return x;
}
void kruskal(){
int num = 1,i,vex1,vex2;
int parent[520]; // 并查集数组
memset(parent, 0 , sizeof(parent)); // 初始化parent数组
for(num = 1,i = 1; num < vertexNum; i++){ // vertexNum已经按权值从小到大排序好了
vex1 = FindRoot(parent,edge[i].from);
vex2 = FindRoot(parent,edge[i].to); // 查找边的两个的老大
if(vex1!=vex2){ // 两个老大不同,表示不属于一个集合,说明可以加入新边
cout<
问题
解
无小结
步骤总结:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NVRP89o7-1642514497431)(第6章 图_img/image-
#include
using namespace std;
// ? 已经成功实现 —— 基于邻接表实现
#define MAX_VERTEX_NUM 100
int verNum, arcNum; // 图的顶点数和边数
int x, y;
int inDegree[MAX_VERTEX_NUM]; // 记录顶点入度
int print[MAX_VERTEX_NUM]; // 记录拓扑序列
typedef struct ArcNode // 边链表结点
{
int adjvex;
// int info; // 边的权值
struct ArcNode *nextArc;
} ArcNode;
typedef struct VNode // 顶点表结点
{
int data;
ArcNode *firstArc;
} VNode, AdjList[MAX_VERTEX_NUM];
typedef struct
{
AdjList vertices; // 邻接表
int verNum, arcNum; // 图的顶点数和弧数
} Graph; // Graph是以邻接表存储的图类型
bool TopologicalSort(Graph g)
{
stack s; // 声明辅助栈,用于存储入度为0的顶点
for (int i = g.verNum; i > 0; i--) // 为了让编号比较小的顶点先出栈 (晚进栈)
{
if (inDegree[i] == 0)
{
s.push(i); // 将初始所有入度为0的顶点入栈
}
}
int count = 0; // 计数,记录当前已经输出的顶点数
while (!s.empty()) // 栈非空,表示还存在入度为0的顶点
{
int flag;
int v;
ArcNode *p;
flag = s.top();
s.pop(); // 栈顶元素出栈
print[count++] = flag; // 记录输出的顶点
// 本段循环的目的:由于输出栈顶元素这个操作,所以需要对与栈顶顶点相连的顶点的入度减一
for (p = g.vertices[flag].firstArc; p != NULL; p = p->nextArc)
{
v = p->adjvex;
if (!(--inDegree[v]))
{
s.push(v); // 入度为0,入栈
}
}
}
if (count < g.verNum)
{
return false; // 拓扑排序失败,存在回路
}
return true; // 拓扑排序成功
}
int main()
{
cin >> verNum >> arcNum;
Graph graph;
graph.verNum = verNum;
graph.arcNum = arcNum;
memset(inDegree, 0, sizeof(inDegree));
memset(print, -1, sizeof(print));
for (int i = 1; i <= verNum; i++)
{
graph.vertices[i].firstArc = NULL;
}
for (int i = 1; i <= arcNum; i++)
{
cin >> x >> y;
inDegree[y]++;
ArcNode *arc = (ArcNode *)malloc(sizeof(ArcNode));
arc->adjvex = y;
arc->nextArc = graph.vertices[x].firstArc;
graph.vertices[x].firstArc = arc;
}
if (TopologicalSort(graph))
{
for (int i = 0; i < verNum; i++)
{
cout << "v" << print[i] << " ";
}
}
return 0;
}
#include
using namespace std;
#define MAX_VERTEX_NUM 16
#define MAX_ARC_NUM 128
int verNum, arcNum; // 图的顶点数和边数
int x, y, z;
int inDegree[MAX_VERTEX_NUM]; // 记录顶点入度
int print[MAX_VERTEX_NUM]; // 记录拓扑序列
int ve[MAX_VERTEX_NUM]; // 事件的最早发生时间
int vl[MAX_VERTEX_NUM]; // 事件的最迟发生时间
int e[MAX_ARC_NUM]; // 活动的最早发生时间
int l[MAX_ARC_NUM]; // 活动的最迟发生时间
int d[MAX_ARC_NUM]; // 活动的时间余量
typedef struct ArcNode // 边链表结点
{
int adjvex;
int info; // 边的权值
struct ArcNode *nextArc;
} ArcNode;
typedef struct VNode // 顶点表结点
{
int data;
ArcNode *firstArc;
} VNode, AdjList[MAX_VERTEX_NUM];
typedef struct {
AdjList vertices; // 邻接表
int verNum, arcNum; // 图的顶点数和弧数
} Graph; // Graph是以邻接表存储的图类型
// !!! 第一步:先求拓扑序列
bool TopologicalSort(Graph g) {
stack s; // 声明辅助栈,用于存储入度为0的顶点
for (int i = 1; i <= g.verNum; i++)
{
if (inDegree[i] == 0)
{
s.push(i); // 将初始所有入度为0的顶点入栈
}
}
int count = 0; // 计数,记录当前已经输出的顶点数
while (!s.empty()) // 栈非空,表示还存在入度为0的顶点
{
int flag;
int v;
ArcNode *p;
flag = s.top();
s.pop(); // 栈顶元素出栈
print[count++] = flag; // 记录输出的顶点
// 本段循环的目的,因为输出栈顶元素,与栈顶顶点相连的顶点的入度减一
for (p = g.vertices[flag].firstArc; p != NULL; p = p->nextArc) {
v = p->adjvex;
if (!(--inDegree[v]))
{
s.push(v); // 入度为0,入栈
}
}
}
if (count < g.verNum) {
return false; // 拓扑排序失败,存在回路
}
return true; // 拓扑排序成功
}
// !!! 第二步:
void EarlistTimeOfVertex(Graph graph, int veList[])
{
for (int i = 0; i < graph.verNum; i++) // 一共有verNum个事件
{
int flag = print[i];
for (int j = 1; j <= graph.verNum; j++)
{
ArcNode *tempArc = graph.vertices[i].firstArc;
while (tempArc != NULL)
{
if (tempArc->adjvex == flag)
{
ve[flag] = max(ve[flag], ve[j] + tempArc->info);
}
tempArc = tempArc->nextArc;
}
}
}
}
void LatestTimeOfVertex(int vlList[])
{
}
void EarlistTimeOfEdge(int eList[])
{
}
void LatestTimeOfEdge(int lList[])
{
}
void CriticalPath(Graph g)
{
}
int main()
{
cin >> verNum >> arcNum;
Graph graph;
graph.verNum = verNum;
graph.arcNum = arcNum;
memset(inDegree, 0, sizeof(inDegree));
memset(print, -1, sizeof(print));
for (int i = 1; i <= verNum; i++)
{
graph.vertices[i].firstArc = NULL;
}
for (int i = 1; i <= arcNum; i++)
{
cin >> x >> y >> z;
inDegree[y]++;
ArcNode *arc = (ArcNode *)malloc(sizeof(ArcNode));
arc->adjvex = y;
arc->info = z;
arc->nextArc = graph.vertices[x].firstArc;
graph.vertices[x].firstArc = arc;
}
if (TopologicalSort(graph))
{
for (int i = 0; i < verNum; i++)
{
cout << "v" << print[i] << " ";
}
}
if (TopologicalSort(graph))
{
EarlistTimeOfVertex(graph, ve);
for (int i = 1; i <= verNum; i++)
{
cout << ve[i] << " , ";
}
}
return 0;
}
不实际操作代码,只是粗略过一下知识点,只需 75 分钟左右过一遍