数据结构----结构–非线性结构–图
一.图(Graph)
1.图的结构
图是多对多的结构
2.图的基本概念
图的基本概念为G=(V,E)
V是顶点的集合
E是边的集合
G是图
一个图其实是顶点和边的二元组合
观察下图,思考V和E集合中的元素分别都是什么
V={V1,V2,V3,V4,V5}
E={(V1,V2),(V1,V3),(V1,V5),(V2,V3),(V2,V5),(V3,V4),(V3,V5),(V4,V5)}
G是V和E的二元组合
注意:图中更重要的是边(顶点与顶点之间的关系)
3.图的一些相关概念
这里将无向图和有向图进行对比说明(如果有相同的概念那么不会在有向图中重复书写)
无向图:
1.边的表示方法为(V1,V2)
了解:
依附:V1到V2这条边就是依附于V1和V2这两个顶点的
2.完全图:图中所有可能存在的边都存在
关于完全图的一个知识点
n个节点的无向完全图边有Cn2个(n在C的右下角,2在n的左上角)
3.路径:一个点到另外一个点所经过的边的总数
4.简单路径:除起点和终点可能相同以外不允许顶点重复出现
5.连通:一个点到另一个点可通
6.连通图:图中任意两点均有路径可通
7.子图:如果G1是G的子图,那么V1∈V,E1∈E
8.度:图中的度指当前节点关联的边的长度
极大连通子图(也叫连通分量),对于一个连通图来讲它的极大连通子图就是1,再看下面的一张图进行理解
此图中的极大连通子图为2
有向图:
1.边的表示方法为 <V1,V2>
2.完全图:图中所有可能存在的边都存在
关于完全图的一个知识点
n个节点的无向完全图边有An2个(n在C的右下角,2在n的左上角)
3.强连通:一个点到另一个点双向可通
4.连通图:图中任意两点均有路径双向可通
5.有向图的边也叫弧,箭头的那一端叫弧首,另一端叫弧尾
8.度:有向图中指出去的边的总数叫入度,指进来的边的总数叫入度,出度入度之和就是度
3.图的存储方式
1.图的存储方式有两种,一种是用邻接矩阵(二维数组)进行存储,一种是用邻接链表进行存储
将下图分别用邻接矩阵和邻接链表进行存储
1.邻接矩阵(二维数组)
如下图
顶点之间没有边的正常应该是无穷这里用0来进行代替
行代表指向谁,如第一行代表V1指向V2,V3,V5
列代表被谁指向,如第一列代表V1被V2,V3,V5指向
注意:如果一个图的邻接矩阵沿对角线对称且无特殊声明那这个图就是无向图
2.邻接链表
如下图
2.两种存储方式应用场合的不同
邻接矩阵适用于边多的图
邻接链表适用于边少的图
3.两种存储方式的区别
邻接矩阵是唯一的
邻接链表不唯一
4.用邻接矩阵(二维数组)实现图的存储(代码实现)
操作步骤
1.创建一个结构体(此结构体中的属性有顶点的个数,边的个数,指向邻接矩阵的指针)
1.得到顶点的个数,边的个数
2.申请一个邻接矩阵(动态申请空间,记得要赋初值)
3.添加边
代码如下
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
typedef struct Node {
int nVertex;
int nEdge;
int* pMatrix;
}Graph;
Graph* CreateGraph() {
//申请一个结构体类型的变量
Graph * pGraph= (Graph*)malloc(sizeof(Graph));
int nV;
int nE;
printf("请输入点数\n");
scanf_s("%d", &nV);
printf("请输入边数\n");
scanf_s("%d", &nE);
pGraph->nVertex = nV;
pGraph->nEdge = nE;
//申请矩阵
pGraph->pMatrix= (int*)malloc(sizeof(int) * nV * nE);
memset(pGraph->pMatrix, 0, sizeof(int) * nV * nE);
//添加边
int v1, v2;
int i;
for (int i = 0; i < nE; i++) {
printf("输入两个顶点确定一条边\n");
scanf_s("%d%d", &v1, &v2);
if (v1 != v2 && v1 >= 1 && v1 <= nV && v2 >= 1 && v2 <= nV && pGraph->pMatrix[(v1 - 1) * nV + (v2 - 1)] == 0) {//如果符合创建边的条件,那么创建这条边
pGraph->pMatrix[(v1 - 1) * nV + (v2 - 1)] = 1;
pGraph->pMatrix[(v2 - 1) * nV + (v1 - 1)] = 1;
}
else {//如果输入的两个顶点不符合这个图创建边的条件,那么这里的i变量的值不会增大,以此保证边的总数不变
i--;
}
}
return pGraph;
}
int main() {
Graph* pGraph = CreateGraph();
for (int i = 0; i < pGraph->nVertex * pGraph->nVertex; i++) {
if (i % pGraph->nVertex == 0) printf("\n");
printf("%d ", pGraph->pMatrix[i]);
}
return 0;
}
二.BFS和DFS
图的广度优先遍历就是BFS
图的深度优先遍历就是DFS
1.深度/深度优先的底层算法–回溯法(BackTracking)
1.回溯法解决的问题
1.求子集的问题
2.求排列的问题
3.求集合的问题
4.求棋盘的问题
2.回溯常见的写法
循环嵌套递归
3.用回溯法解决一道全排列的问题(此题的网址为https://leetcode.cn/problems/permutations/)
题目:
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
方法一:标记法
方法二:交换法
用方法一来解决此问题
代码如下
//下面代码不是明显的标记写法,但是和标记的思路相同
class Solution {
public:
void BT(vector<vector<int>>& result, vector<int> nums, vector<int> temp, int x) {
//如果这个排列是全排列的一个答案(这里是以是否达到答案排列的长度进行判断的)
if (temp.size() == x) {
result.push_back(temp);
return;
}
//对可能的元素进行选择(每次选择一个元素放到排列的当前位置上)
for (int i = 0; i < nums.size(); i++) {
temp.push_back(nums[i]);
vector<int>::iterator ite = nums.begin();
ite += i;
ite = nums.erase(ite);
//继续看当前排列的下一个位置
BT(result, nums, temp, x);
//为了当前排列当前位置下一个元素的正常判断,进行还原
ite = temp.end()-1;
vector<int>::iterator ite2 = nums.begin();
nums.insert(ite2, *ite);
temp.pop_back();
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> result;
vector<int> temp;
BT(result,nums,temp,nums.size());
return result;
}
};
2.DFS:深度优先遍历
操作步骤:
1.申请一个标记数组,赋初值
2.从给出的顶点开始遍历
(1)打印当前顶点
(2)对当前顶点进行标记(就是给标记数组相应的位置进行值的修改)
(3)找当前顶点邻接的且未标记的顶点进行处理,重复2
代码如下:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
typedef struct Node {
int nVertex;
int nEdge;
int* pMatrix;
}Graph;
Graph* CreateGraph() {
//申请一个结构体类型的变量
Graph* pGraph = (Graph*)malloc(sizeof(Graph));
int nV;
int nE;
printf("请输入点数\n");
scanf_s("%d", &nV);
printf("请输入边数\n");
scanf_s("%d", &nE);
pGraph->nVertex = nV;
pGraph->nEdge = nE;
//申请矩阵
pGraph->pMatrix = (int*)malloc(sizeof(int) * nV * nE);
memset(pGraph->pMatrix, 0, sizeof(int) * nV * nE);
//添加边
int v1, v2;
int i;
for (int i = 0; i < nE; i++) {
printf("输入两个顶点确定一条边\n");
scanf_s("%d%d", &v1, &v2);
if (v1 != v2 && v1 >= 1 && v1 <= nV && v2 >= 1 && v2 <= nV && pGraph->pMatrix[(v1 - 1) * nV + (v2 - 1)] == 0) {//如果符合创建边的条件,那么创建这条边
pGraph->pMatrix[(v1 - 1) * nV + (v2 - 1)] = 1;
pGraph->pMatrix[(v2 - 1) * nV + (v1 - 1)] = 1;
}
else {//如果输入的两个顶点不符合这个图创建边的条件,那么这里的i变量的值不会增大,以此保证边的总数不变
i--;
}
}
return pGraph;
}
void MyDFS(Graph* pGraph, int begin, char* flagArr) {
//打印
printf("%d ", begin);
//标记
flagArr[begin - 1] = 1;
//遍历当前顶点的邻接点
for (int i = 0; i < pGraph->nVertex; i++) {
if (pGraph->pMatrix[(begin - 1) * pGraph->nVertex + i] == 1 && flagArr[i] != 1) {//如果是邻接且未处理的顶点,那么就遍历此顶点
MyDFS(pGraph, i + 1, flagArr);
}
}
}
void DFS(Graph* pGraph,int begin) {
//申请一个标记数组
char* flagArr = (char*)malloc(sizeof(char) * pGraph->nVertex);
memset(flagArr, 0, sizeof(char) * pGraph->nVertex);
MyDFS(pGraph, begin, flagArr);
}
int main() {
Graph* pGraph = CreateGraph();
//进行DFS遍历
DFS(pGraph, 2);
return 0;
}
3.BFS:广度优先遍历
操作步骤:
1.申请一个标记数组和一个队列,给标记数组赋初值
2.将遍历的第一个顶点放入到队列中,并对此顶点进行标记(就是给标记数组相应的位置进行值的修改)
3.将队列中的首元素弹出,输出,将弹出顶点的邻接的且未标记的顶点入队,并对此顶点进行标记,然后重复3,直到队列为空此操作结束
代码如下:
void BFS(Graph* pGraph, int begin) {
if (pGraph == NULL || begin<1 || begin > pGraph->nVertex)return;
//申请一个标记数组
char* flagArr = (char*)malloc(sizeof(char) * pGraph->nVertex);
memset(flagArr, 0, sizeof(char) * pGraph->nVertex);
//申请一个队列
queue<int> qe;
//起点入队
qe.push(begin);
flagArr[begin - 1] = 1;
//对队列进行处理
while (!qe.empty()) {
//弹出 打印
int x = qe.front();
cout<<x;
qe.pop();
//邻接点
for (int i = 0; i < pGraph->nVertex; i++) {
if (pGraph->pMatrix[(x - 1) * pGraph->nVertex + i] == 1 && flagArr[i] != 1) {
//入队 标记
qe.push(i + 1);
flagArr[i] = 1;
}
}
}
//释放空间
free(flagArr);
flagArr = NULL;
}
int main() {
Graph* pGraph = CreateGraph();
//进行BFS遍历
BFS(pGraph, 1);
return 0;
}
三.迪杰斯特拉算法(Dijkstra)
1.迪杰斯特拉算法的用途
它可以求在有向的、带正权值的图中,点与点之间的最短路径
2.迪杰斯特拉算法的实现用到了什么基础算法
迪杰斯特拉的实现用到了贪心的算法
什么是贪心
在有多个选择的时候不考虑长远的情况,只考虑眼前的这一步,在眼前这一步选择当前的最好的方案
3.看下面例子进一步理解迪杰斯特拉
1.题目:求出下图中从V1到其他各个顶点的最短路径
2.进行解决
操作步骤:
1.画出该图的邻接矩阵如下图
2.我们从上图可以知道V1到各个顶点的路径大小为{X,6,31,15}(注意集合中分别表示V1到各个顶点的距离,到自己本身为X,因为不需要算到自己的距离)
那我们运用贪心的算法取当前V1所到顶点最短的那个顶点V2(且V1到此顶点的最短距离就是当前的这个距离,之后不需要再进行改变了),然后从V1->V2再从V2到其他的各个点(如果此时所得的路径小的话,就对集合进行更新,),得到的新的路径大小为{X,6,31,9}
然后再从集合中选择一个最小的且没有选择过的顶点,这里选择的是V4(且V1到此顶点的最短距离就是当前的这个距离,之后不需要再进行改变了),那此时就是先从V1->V4,再从V4到其他的各个点(如果此时所得的路径小的话,就对集合进行更新),得到的新的路径大小为{X,6,31,9}
重复选择一个最小的且没有选择过的顶点(且V1到此顶点的最短距离就是当前的这个距离,之后不需要再进行改变了),直到所有顶点都被选择过了,就会得到V1到其他各个顶点的最短路径,结束操作
4.迪杰斯特拉的时间复杂度
使用迪杰斯特拉的时间复杂度为O(n的平方)
了解:弗洛伊德算法是求多个点中任选两点之间的最短距离
四.最小生成树
1.最小生成树可以解决的问题
将一个连通图的总长度变成最小的且该图还是一个连通图
注意:图和树的一个区别就是图可以有环,树中不能有环
所以为什么该操作叫最小生成树就可以用上面这个知识点来解释,这里的操作其实就是选择最小的边保证各个顶点是连接的,而这种操作也会导致图中不会出现环,所以该操作叫最小生成树
2.实现最小生成树的两种方法
1.克鲁斯卡尔(Kruscal)
克鲁斯卡尔是从边出发进行操作的
操作步骤:
1.先将之前图中的所有顶点都画出来
2.对图中所有的边进行排序,选择最小的边,添加到图中(注意此边的连接的两个垫顶点和之前图中的一样),依次选择直到所有的顶点都连上了结束(注意如果将一条添加到图中后,形成了环,那么这条边我们不用,舍弃掉,继续找下一个最小的边)
2.普利姆(Prim)
普利姆是从点出发进行操作的
操作步骤:
1.任选一个顶点,然后从当前顶点到另外顶点的边中,找到那条最小的边,然后在图中添加这条边和这条边连接的顶点
2.然后从当前所有顶点到另外所有顶点的边中,找到那条最小的边,然后在图中添加这条边和这条边连接的顶点(注意如果找到的边所连接的那个顶点式已经存在的那么这条边和这个顶点舍弃掉,因为如果使用了那就会形成环了),重复操作2直到所有的顶点都连上了结束
五.拓扑排序(Topological Order)
1.拓扑排序的服务结构
拓扑排序的服务结构是有向无环图(DAG )(全称为Directed Acylic Graph)
注意:它们两个是充分必要条件,拓扑排序能验证它所服务的结构是不是DAG,并且它也只能为DAG服务
2.拓扑排序的功能
为一个项目中具备依赖关系的多个活动求得可执行的线性顺序
2.看一个用拓扑排序解决的问题,更好的理解拓扑排序
1.题目:
小李的课程表如下,你该如何给他安排课程,才能让他把所有的课修完
2.解决步骤如下:
1.根据上图中的课程(作为图中的顶点)和学某个课程所需要先学的课程(此依赖关系作为图中的边)画出一张图,如下
注意:此图的顶点代表的是活动,边代表的是活动之间的依赖关系,所以此图也叫做AOV网
AVO网和DAG的关系:DAG一定是AOV网,但是AOV网不一定是DAG
2.计算图中所有顶点的入度存入 到一个数组中去
3.申请一个队列
4.将数组中所有入度为0的顶点放入到队列中,将位于队列中的首位的顶点弹出,将依赖于它的顶点的入度减1(更新数组)直到队列中没有顶点了继续,重复操作4直到数组中没有入度为0的顶点结束操作
问题:拓扑排序是如何检测它所服务的结构是不是DAG的
看拓扑序列中的顶点总数是不是图中的顶点总数相同,
如果是那么它所服务的结构就是DAG
如果不是那么它所服务的结构就不是DAG
六.二部图(二分图)
1.二部图的定义
G=(U,V,E)
U和V是两个顶点的集合(U和V各自内部是没有边关系的,U和V这两个集合之间有边关系),E是产生在U和V之间的边的集合
2.观察下面的两张图判断是否是二分图
第一张图如下
此图不是二部图
看下图进行理解
我们将此图中的顶点进行阵营的划分,对号为一个顶点的集合(第一个阵营),圆圈为另一个顶点的集合(第二个阵营),我们发现此图进行划分阵营后,相同阵营的顶点之间会有连接,所以此图不是二部图
第二张图如下
此图是二部图
看下图进行理解
我们将此图中的顶点进行阵营的划分,对号为一个顶点的集合(第一个阵营),圆圈为另一个顶点的集合(第二个阵营)可以发现相同阵营的顶点之间是没有连接的,所以此图是二部图