当前位置: 首页 > news >正文

【高阶数据结构】图

  • 1. 图的基本概念
  • 2. 图的存储结构
    • 2.1 邻接矩阵
    • 2.2 邻接表
    • 2.3 邻接矩阵的实现
    • 2.4 邻接表的实现
  • 3. 图的遍历
    • 3.1 图的广度优先遍历
    • 3.2 图的深度优先遍历
  • 4. 最小生成树
    • 4.1 Kruskal算法
    • 4.2 Prim算法
  • 5. 最短路径
    • 5.1 单源最短路径--Dijkstra算法
    • 5.2 单源最短路径--Bellman-Ford算法
    • 5.3 多源最短路径--Floyd-Warshall算法

在这里插入图片描述

点赞👍👍收藏🌟🌟关注💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃

1. 图的基本概念

图是由顶点集合及顶点间的关系组成的一种数据结构:G = (V, E),其中:

顶点集合V = {x|x属于某个数据对象集}是有穷非空集合

E = {(x,y)|x,y属于V}或者E = {<x, y>|x,y属于V && Path(x, y)}是顶点间关系的有穷集合,也叫做边的集合。

(x, y)表示x到y的一条双向通路,即(x, y)是无方向的;Path(x, y)表示从x到y的一条单向通路,即Path(x, y)是有方向的。

顶点和边:图中结点称为顶点,第i个顶点记作vi。两个顶点vi和vj相关联称作顶点vi和顶点vj之间有一条边,图中的第k条边记作ek,ek = (vi,vj)或<vi,vj>。

有向图和无向图:在有向图中,顶点对<x, y>是有序的,顶点对<x,y>称为顶点x到顶点y的一条边(弧),<x, y>和<y, x>是两条不同的边,比如下图G3和G4为有向图。在无向图中,顶点对(x, y)是无序的,顶点对(x,y)称为顶点x和顶点y相关联的一条边,这条边没有特定方向,(x, y)和(y,x)是同一条边,比如下图G1和G2为无向图。注意:无向边(x, y)等于有向边<x, y>和<y, x>

下面是一些常见的图,G2看着是一颗二叉树为什么也说是图呢?
可以这样理解,树是一种特殊(无环连通)的图,图不一定是树。树关注的节点(顶点)中存的值以及连通关系,图关注的是顶点及边的权值。(边由三部分组成:两个顶点、权值)
在这里插入图片描述

树是一种存储式数据结构,节点内存值,然后构成二叉搜索树,AVL树,红黑树。
图是一种表示型数据结构,表示某种场景。

比如说下面的图,顶点可能表示城市,边表示城市之间一个关系(高铁距离、高铁价格、高铁时间。。。)

有了这个东西,提出DFS,BFS遍历,最小生成树(最小代价把图连图),最短路径(一个顶点到其他顶点 或者 多个顶点之间 最短路径)的问题。

在这里插入图片描述

图还可以用来表示社交关系

顶点:人
边:表示两个人是好友
边权值:亲密度等

微信,qq等关系->无向图(强社交关系)
微博,抖音等关系->有向图(弱社交关系、媒体社交)

在这里插入图片描述

完全图(任意两个顶点都有边):在有n个顶点的无向图中,若有n * (n-1)/2条边(n个顶点 1->n-1,2->n-2 … n->0 等差数列),即任意两个顶点之间有且仅有一条边,则称此图为无向完全图,比如上图G1;在n个顶点的有向图中,若有n * (n-1)条边,即任意两个顶点之间有且仅有方向相反的边,则称此图为有向完全图,比如上图G4。

邻接顶点:在无向图中G中,若(u, v)是E(G)中的一条边,则称u和v互为邻接顶点,并称边(u,v)依附于顶点u和v;在有向图G中,若<u, v>是E(G)中的一条边,则称顶点u邻接到v,顶点v邻接自顶点u,并称边<u, v>与顶点u和顶点v相关联。

顶点的度:顶点v的度是指与它相关联的边的条数,记作deg(v)。在有向图中,顶点的度等于该顶点的入度与出度之和,其中顶点v的入度是以v为终点的有向边的条数,记作indev(v);顶点v的出度是以v为起始点的有向边的条数,记作outdev(v)。因此:dev(v) = indev(v) + outdev(v)。注意:对于无向图,顶点的度等于该顶点的入度和出度,即dev(v) = indev(v) = outdev(v)。

路径:在图G = (V, E)中,若从顶点vi出发有一组边使其可到达顶点vj,则称顶点vi到顶点vj的顶点序列为从顶点vi到顶点vj的路径

路径长度:对于不带权的图,一条路径的路径长度是指该路径上的边的条数;对于带权的图,一条路径的路径长度是指该路径上各个边权值的总和

在这里插入图片描述

简单路径与回路:若路径上各顶点v1,v2,v3,…,vm均不重复,则称这样的路径为简单路径若路径上第一个顶点v1和最后一个顶点vm重合,则称这样的路径为回路或环

在这里插入图片描述

子图:设图G = {V, E}和图G1 = {V1,E1},若V1属于V且E1属于E,则称G1是G的子图。

在这里插入图片描述

连通图(连通图是针对无向图来说的):在无向图中,若从顶点v1到顶点v2有路径,则称顶点v1与顶点v2是连通的。如果图中任意一对顶点都是连通的,则称此图为连通图

强连通图(强连通图是针对有向图来说的):在有向图中,若在每一对顶点vi和vj之间都存在一条从vi到vj的路径,也存在一条从vj到vi的路径,则称此图是强连通图。

生成树:一个连通图的最小连通子图称作该图的生成树有n个顶点的连通图的生成树有n个顶点和n-1条边。

2. 图的存储结构

因为图中既有节点,又有边(节点与节点之间的关系),因此,在图的存储中,只需要保存:节点和边关系即可。节点保存比较简单,只需要一段连续空间即可,那边关系该怎么保存呢?

顶点我们可以像并查集哪里一样用vector和map保存,那边如何保存呢?

//V 顶点类型,  W 权值类型, Direction  表示有向/无向
template<class V,class W,bool Direction>
class Graph
{private:vector<V> _vertexs;//顶点集合map<V, int> _IndexMap;//顶点与下标映射
};

2.1 邻接矩阵

因为节点与节点之间的关系就是连通与否,即为0或者1,因此邻接矩阵(二维数组)即是:先用一个数组将顶点保存(将顶点转化成对应的下标,比如说顶点是abcd编号就是0123),然后采用矩阵来表示节点与节点之间的关系。

在这里插入图片描述

注意:

  1. 无向图的邻接矩阵是对称的,第i行(列)元素之和,就是顶点i的度。有向图的邻接矩阵则不一定是对称的,第i行(列)元素之后就是顶点i 的出(入)度。
  2. 如果边带有权值,并且两个节点之间是连通的,上图中的边的关系就用权值代替,如果两个顶点不通,则使用无穷大代替

在这里插入图片描述
3. 用邻接矩阵存储图的有点是能够快速知道两个顶点是否连通,缺陷是如果顶点比较多,边比较少时,矩阵中存储了大量的0成为系数矩阵,比较浪费空间,并且要求两个节点之间的路径不是很好求。

优点:

邻接矩阵存储方式非常适合稠密图
邻接矩阵O(1)判断两个顶点的连接关系,并取到权值

缺点:

相对而言不适合查找一个顶点连接所有边 — O(N)

假设有n个顶点,是不是要所有顶点遍历一遍才知道某个顶点到底和那些顶点相连。
时间复杂度是O(N),N是顶点个数。

假设有100个顶点,我这个顶点只和三个顶点相连只有三条边,但也要遍历100次,能不能有个方法快速把与之相连的三条边都找到呢?

2.2 邻接表

邻接表:使用数组表示顶点的集合,使用链表表示边的关系。

邻接表和哈希桶类似。使用一个指针数组,把自己和连接的顶点边都挂在下面。

  1. 无向图邻接表存储

在这里插入图片描述

注意:无向图中同一条边在邻接表中出现了两次。如果想知道顶点vi的度,只需要知道顶点vi边链表集合中结点的数目即可。

优点:

适合存储稀疏图
适合查找一个顶点连接出去的边

缺点:

不适合确定两个顶点是否相连及权值

  1. 有向图邻接表存储

在这里插入图片描述

注意:有向图中每条边在邻接表中只出现一次,与顶点vi对应的邻接表所含结点的个数,就是该顶点的出度,也称出度表,要得到vi顶点的入度,必须检测其他所有顶点对应的边链表,看有多少边顶点的dst取值是i。

一般情况下有向图,存储一个出边表即可。

总结一下:邻接矩阵和邻接表其实属于相辅相成,各有优缺点的互补结构。具体还是看场景选择用邻接矩阵和邻接表

2.3 邻接矩阵的实现

//类型模板参数: V 顶点类型(int,char...),  W 权值类型(int,double...), Direction  表示有向/无向(默认无向)
//非类型模板参数: MAX_W  默认权值
template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
class Graph
{
public:private:vector<V> _vertexs;        //顶点集合map<V, int> _IndexMap;    //顶点与下标映射vector<vector<W>> _matrix; //邻接矩阵(边的集合)
};

图的创建有下面几种方式:

  1. IO输入 ------ (自己写不方便测试,oj中更适合)
  2. 图的结构关系写到文件,读取文件
  3. 手动添加边 (我们采用的方式)
Graph(const V* a, size_t n)
{_vertexs.reserve(n);for (size_t i = 0; i < n; ++i){_vertexs.push_back(a[i]);_IndexMap[a[i]] = i;}_matrix.resize(n);for (size_t i = 0; i < n; ++i){_matrix[i].resize(n, MAX_W);}	
}

添加边

首先我们要找到边对应两个顶点的下标,然后才在矩阵添加边的信息,注意区分有向图还是无向图。

size_t GetVertexindex(const V& v)
{//不能直接用[]去查,万一不在就成插入了auto it = _IndexMap.find(v);if (it != _IndexMap.end()){return it->second;}else{throw invalid_argument("不存在的顶点");return -1;}
}void _AddEdge(const size_t& srci, const size_t& dsti, const W& w)
{_matrix[srci][dsti] = w;if (Direction == false) // 无向图{_matrix[dsti][srci] = w;}
}void AddEdge(const V& src, const V& dst, const W& w)
{size_t srci = GetVertexindex(src);size_t dsti = GetVertexindex(dst);_AddEdge(srci, dsti, w);
}

打印

void Print()
{// 顶点for (size_t i = 0; i < _vertexs.size(); ++i){cout << "[" << i << "]" << "->" << _vertexs[i] << endl;}cout << endl;// 矩阵// 横下标cout << "  ";for (size_t i = 0; i < _vertexs.size(); ++i){//cout << i << " ";printf("%4d", i);}cout << endl;for (size_t i = 0; i < _matrix.size(); ++i){cout << i << " "; // 竖下标for (size_t j = 0; j < _matrix[i].size(); ++j){//cout << _matrix[i][j] << " ";if (_matrix[i][j] == MAX_W){//cout << "* ";printf("%4c", '*');}else{//cout << _matrix[i][j] << " ";printf("%4d", _matrix[i][j]);}}cout << endl;}cout << endl;
}

下面我们测试一下

void TestGraph()
{Graph<char, int, INT_MAX, true> g("0123", 4);g.AddEdge('0', '1', 1);g.AddEdge('0', '3', 4);g.AddEdge('1', '3', 2);g.AddEdge('1', '2', 9);g.AddEdge('2', '3', 8);g.AddEdge('2', '1', 5);g.AddEdge('2', '0', 3);g.AddEdge('3', '2', 6);g.Print();
}

在这里插入图片描述
完整代码

//类型模板参数: V 顶点类型(int,char...),  W 权值类型(int,double...), Direction  表示有向/无向(默认无向)
//非类型模板参数: MAX_W  默认权值
template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
class Graph
{
public:Graph(const V* a, size_t n){_vertexs.reserve(n);for (size_t i = 0; i < n; ++i){_vertexs.push_back(a[i]);_IndexMap[a[i]] = i;}_matrix.resize(n);for (size_t i = 0; i < n; ++i){_matrix[i].resize(n, MAX_W);}	}size_t GetVertexindex(const V& v){//不能直接用[]去查,万一不在就成插入了auto it = _IndexMap.find(v);if (it != _IndexMap.end()){return it->second;}else{throw invalid_argument("不存在的顶点");return -1;}}void _AddEdge(const size_t& srci, const size_t& dsti, const W& w){_matrix[srci][dsti] = w;if (Direction == false) // 无向图{_matrix[dsti][srci] = w;}}void AddEdge(const V& src, const V& dst, const W& w){size_t srci = GetVertexindex(src);size_t dsti = GetVertexindex(dst);_AddEdge(srci, dsti, w);}void Print(){// 顶点for (size_t i = 0; i < _vertexs.size(); ++i){cout << "[" << i << "]" << "->" << _vertexs[i] << endl;}cout << endl;// 矩阵// 横下标cout << "  ";for (size_t i = 0; i < _vertexs.size(); ++i){//cout << i << " ";printf("%4d", i);}cout << endl;for (size_t i = 0; i < _matrix.size(); ++i){cout << i << " "; // 竖下标for (size_t j = 0; j < _matrix[i].size(); ++j){//cout << _matrix[i][j] << " ";if (_matrix[i][j] == MAX_W){//cout << "* ";printf("%4c", '*');}else{//cout << _matrix[i][j] << " ";printf("%4d", _matrix[i][j]);}}cout << endl;}cout << endl;}private:vector<V> _vertexs;        //顶点集合map<V, int> _IndexMap;    //顶点与下标映射vector<vector<W>> _matrix; //邻接矩阵(边的集合)
};

2.4 邻接表的实现

邻接表实际也是一个哈希桶,这里实现很简单

//存储边的信息
template<class W>
struct Edge
{size_t _srci;//起始点size_t _dsti;//目标点的下标W _w;//权值Edge<W>* _next;Edge(const size_t& srci,const size_t& dsti,const W& w):_srci(srci),_dsti(dsti),_w(w),_next(nullptr){}
};template<class V,class W,bool Direction = false>
class Graph
{typedef Edge<W> Edge;
public:Graph(const V* a, size_t n){_vertexs.reserve(n);for (size_t i = 0; i < n; ++i){_vertexs.push_back(a[i]);_IndexMap[a[i]] = i;}_tables.resize(n, nullptr);}size_t GetVertexindex(const V& v){auto it = _IndexMap.find(v);if (it != _IndexMap.end()){return it->second;}else{throw invalid_argument("不存在的顶点");return -1;}}void _AddEdge(const size_t& srci, const size_t& dsti,const W& w){//头插Edge* edge = new Edge(srci, dsti, w);edge->_next = _tables[srci];_tables[srci] = edge;if (Direction == false)  // 无向图{Edge* new_edge = new Edge(dsti, srci, w);new_edge->_next = _tables[dsti];_tables[dsti] = new_edge;}}void AddEdge(const V& src, const V& dst, const W& w){size_t srci = GetVertexindex(src);size_t dsti = GetVertexindex(dst);_AddEdge(srci, dsti, w);}void Print(){// 顶点for (size_t i = 0; i < _vertexs.size(); ++i){cout << "[" << i << "]" << "->" << _vertexs[i] << endl;}cout << endl;for (size_t i = 0; i < _tables.size(); ++i){cout << _vertexs[i] << "[" << i << "]->";Edge* cur = _tables[i];while (cur){cout << "[" << _vertexs[cur->_dsti] << ":" << cur->_dsti << ":" << cur->_w << "]->";cur = cur->_next;}cout << "nullptr" << endl;}}private:vector<V> _vertexs;     //顶点集合map<V, int> _IndexMap;  //顶点与下标映射vector<Edge*> _tables; //邻接表(哈希桶)
};

接下来图的遍历,最小生成树,最短路径我们都以邻接矩阵构的图去实现。

3. 图的遍历

给定一个图G和其中任意一个顶点v0,从v0出发,沿着图中各边访问图中的所有顶点,且每个顶点仅被遍历一次。"遍历"即对顶点进行某种操作的意思。

3.1 图的广度优先遍历

有树的基础就知道广度优先遍历必然要借助队列来实现。广度优先遍历就是以某个点为起点,当这个顶点出队列就把和这个顶点的邻接顶点都入队列,然后一层一层往外走。但是要注意的是已经入过队列的顶点下一次不能在入队列,否则就会死循环,因此还要来一个标记bool类型数组。当一个顶点入队列后就标记一下。

在这里插入图片描述

void BFS(const V& src)
{size_t srci = GetVertexindex(src);size_t n = _vertexs.size();//队列和标记数组queue<int> q;vector<bool> vis(n, false);q.push(srci);vis[srci] = true;while (!q.empty()){size_t front = q.front();q.pop();cout << front << ":" << _vertexs[front] << endl;//把和front顶点的临界顶点入队列for (size_t i = 0; i < n; ++i){if (_matrix[front][i] != MAX_W && !vis[i]){q.push(i);vis[i] = true;}}}
}void TestBDFS()
{string a[] = { "张三", "李四", "王五", "赵六", "周七" };Graph<string, int> g1(a, sizeof(a) / sizeof(string));g1.AddEdge("张三", "李四", 100);g1.AddEdge("张三", "王五", 200);g1.AddEdge("王五", "赵六", 30);g1.AddEdge("王五", "周七", 30);g1.Print();g1.BFS("张三");//g1.DFS("张三");
}

在这里插入图片描述

接下来看一道题图的BFS应用题

在这里插入图片描述

举一个例子,告诉我们一度好友、二度好友。。。是什么样的,让我们找到小点的六度好友。这就是一个典型的图BFS应用。回想一下刚才我们的BFS顶点出队列是怎么出的?是一个一个出的。对于这道题我们出队列的就要求一层一层出。那怎么一层一层出呢?很简单出队列之前计算一下当前队列内元素的个数。每次出队列内元素个数次。

void BFS(const V& src)
{size_t srci = GetVertexindex(src);size_t n = _vertexs.size();queue<int> q;vector<bool> vis(n, false);q.push(srci);vis[srci] = true;while (!q.empty()){//出队列之前计算队列内元素个数,一层一层出size_t k = q.size();while (k--){size_t front = q.front();q.pop();cout << front << ":" << _vertexs[front] << endl;for (size_t i = 0; i < n; ++i){if (_matrix[front][i] != MAX_W && !vis[i]){q.push(i);vis[i] = true;}}}}
}

3.2 图的深度优先遍历

图的深度优先遍历和树的前序遍历一样。先往深走,当走到不能走的就回溯换条路走,最终直到所有顶点遍历完然后返回。因此我们用递归来实现,这里我们还是需要一个标记bool类型数组,已经访问过的不能在访问否则就会死递归。

在这里插入图片描述

void _DFS(size_t srci, const size_t& n, vector<bool>& vis)
{cout << srci << ":" << _vertexs[srci] << endl;vis[srci] = true;//找一个srci相邻的且没有被访问过的顶点,去往深度遍历for (size_t i = 0; i < n; ++i){if (_matrix[srci][i] != MAX_W && !vis[i]){_DFS(i, n, vis);}}
}void DFS(const V& src)
{size_t srci = GetVertexindex(src);size_t n = _vertexs.size();vector<bool> vis(n, false);_DFS(srci, n, vis);
}

其实这里还有一个遗漏的问题,如果无向图是一个连通图或者有向图是一个强连通图,一次BFS和DFS就可以把所有顶点遍历一遍。但是如果图不是连通的。那以某个点为起点就没有办法一次BFS或者DSF把所有顶点遍历一遍,那如何把图中所有顶点都访问到呢?

在这里插入图片描述

其实很简单,不是有标记数组吗。把标记数组在遍历一遍,如果还有顶点没有被遍历到那就以这个顶点在做一次BFS或DFS。

4. 最小生成树

首先生成树对应的一定是连通图。连通图中找生成树。

连通图(连通图是针对无向图来说的):在无向图中,若从顶点v1到顶点v2有路径,则称顶点v1与顶点v2是连通的。如果图中任意一对顶点都是连通的,则称此图为连通图

生成树:一个连通图的最小连通子图称作该图的生成树。有n个顶点的连通图的生成树有n个顶点和n-1条边。(最少的边连通起来)

最小生成树:构成生成树的边的权值加起来是最小的。(最小的成本让n个顶点连通)

连通图中的每一棵生成树,都是原图的一个极大无环子图,即:从其中删去任何一条边,生成树就不在连通;反之,在其中引入任何一条新边,都会形成一条回路

连通图由n个顶点组成,则其生成树必含n个顶点和n-1条边。因此构造最小生成树的准则有三条:

  1. 只能使用图中的权值最小的边来构造最小生成树
  2. 只能使用恰好n-1条边来连接图中的n个顶点
  3. 选用的n-1条边不能构成回路

构造最小生成树的方法:Kruskal算法Prim算法。这两个算法都采用了逐步求解的贪心策略。

贪心算法:是指在问题求解时,总是做出当前看起来最好的选择。也就是说贪心算法做出的不是整体

最优的的选择,而是某种意义上的局部最优解。贪心算法不是对所有的问题都能得到整体最优解。

最小生成树不唯一,但是权值是唯一的。

4.1 Kruskal算法

给一个有n个顶点的连通网络N={V,E}

首先构造一个由这n个顶点组成、不含任何边的图G={V,NULL},其中每个顶点自成一个连通分量,

其次不断从E中取出权值最小的一条边(若有多条任取其一),若该边的两个顶点来自不同的连通分量,则将此边加入到G中。如此重复,直到所有顶点在同一个连通分量上为止。

核心:每次迭代时,选出一条具有最小权值,且两端点不在同一连通分量上的边,加入生成树。

其实上面说了这么多,Kruskal算法核心思想:就是每次都从边中选权值最小的边(全局选最小)。

那怎么去选最小的边呢?可以把所有的边拿出来排序。但是这不是好方法。更好的方法就是用优先级队列建一个小堆。每次拿堆顶元素,然后pop堆顶元素,再拿次小的边。

但是这里还有一个问题,可能选的边会造成回路

比如选择 i - g 权值为6的这条边,构成了回路!

如何判断选择的边构成了回路呢?

在这里插入图片描述
使用并查集 -> 判环

刚开始每一个顶点都是一个独立的集合,选边的时候判断一下构成这个边的两个顶点是否是一个集合,如果不是一个集合就可以选这个边。然后把对应的两个顶点合并成一个集合。

在这里插入图片描述

struct Edge
{size_t _srci;size_t _dsti;W _w;Edge(size_t& srci, size_t& dsti, W& w):_srci(srci),_dsti(dsti),_w(w){}bool operator>(const Edge& e) const{return _w > e._w;}
};//把最小生成树权值和返回去
W Kruskal(Self& minTree)
{size_t n = _vertexs.size();//最小生成树是连通图的一个子图,信息是一样的,先初始化minTree._vertexs = _vertexs;minTree._IndexMap = _IndexMap;minTree._matrix.resize(n);for (size_t i = 0; i < n; ++i){minTree._matrix[i].resize(n, MAX_W);}//建小堆,因为Edge是自定义类型,库中的greater不支持自定义类型比较,所以写一个对应的仿函数priority_queue<Edge, vector<Edge>, greater<Edge>> heap;//并查集UnionFindSet ufs(n);//将所有边入堆for (size_t i = 0; i < n; ++i){for (size_t j = 0; j < n; ++j){//无向图的邻接矩阵是一个对称矩阵//因此只用存矩阵上半部分或者下半部分就行了if (i < j && _matrix[i][j] != MAX_W){heap.push(Edge(i, j, _matrix[i][j]));}}}//选出n-1条边size_t sz = 0;W total = W();while (!heap.empty()){Edge minedge = heap.top();heap.pop();//构成边的两个顶点不在一个集合,说明不构成环,可以选if (!ufs.IsSet(minedge._srci, minedge._dsti)){//可以打印一下看选了那条边cout << _vertexs[minedge._srci] << "->" << _vertexs[minedge._dsti] << ":" << minedge._w << endl;minTree._AddEdge(minedge._srci, minedge._dsti, minedge._w);ufs.Union(minedge._srci, minedge._dsti);sz++;total += minedge._w;}else{cout << "构成环:";cout << _vertexs[minedge._srci] << "->" << _vertexs[minedge._dsti] << ":" << minedge._w << endl;}}if (sz == n - 1){return total;}else{return -1;}
}void TestGraphMinTree()
{const char* str = "abcdefghi";Graph<char, int> g(str, strlen(str));g.AddEdge('a', 'b', 4);g.AddEdge('a', 'h', 8);//g.AddEdge('a', 'h', 9);g.AddEdge('b', 'c', 8);g.AddEdge('b', 'h', 11);g.AddEdge('c', 'i', 2);g.AddEdge('c', 'f', 4);g.AddEdge('c', 'd', 7);g.AddEdge('d', 'f', 14);g.AddEdge('d', 'e', 9);g.AddEdge('e', 'f', 10);g.AddEdge('f', 'g', 2);g.AddEdge('g', 'h', 1);g.AddEdge('g', 'i', 6);g.AddEdge('h', 'i', 7);Graph<char, int> kminTree;cout << "Kruskal:" << g.Kruskal(kminTree) << endl;kminTree.Print();//Graph<char, int> pminTree;//cout << "Prim:" << g.Prim(pminTree, 'a') << endl;//pminTree.Print();//cout << endl;//for (size_t i = 0; i < strlen(str); ++i)//{//	cout << "Prim:" << g.Prim(pminTree, str[i]) << endl;//}
}

4.2 Prim算法

Prim算法也是用的贪心,但是它跟Kruskal算法不一样,Kruskal的核心思想是每次都在全局选最小,Prim是给一个起点,然后从这个起点开始去找最小边(局部选最小)。它选边是先把所有顶点归成两个集合,一个集合是已经被选择的顶点(已经加入到最小生成树的顶点),剩下顶点是一个集合。它是在两个集合之间去选最小边。每次都从两个集合各选一个顶点构成的最小边。

在这里插入图片描述

为什么会这样选边的呢?也就说这个地方贪心不是一个全局的贪心,是一个局部的贪心。以某个点为起点去找周围最小的边。而之前是全局贪心。那局部贪心的优势是什么?

它的优势就是选边不会构成环。

它是在两个集合之间去选最小边。每次都从两个集合各选一个顶点构成的最小边。天然避环。

那怎么去区分已经加入到最小生成树的顶点集合和剩余顶点的集合呢?
我们可以搞两个vector,一个X集合,一个Y集合。
X表示已经加入最小生成树顶点的结合
Y表示剩余顶点的集合

刚开始所以顶点都没加入到最小生成树也就是都在Y集合,因此Y集合的所有顶点都标记成true,如果某个顶点加入到最小生成树就把对应顶点从Y中变成false,而在X中变为true。

那如何从X->Y集合连接的边里面选出最小的边?
搞一个优先级队列(小堆)把已经加入最小生成树顶点相连的边加入到队列中,这样去选最小边可不可以?其实不行!

在这里插入图片描述

加入h到X集合的时候 a - h 就已经在一个集合了,这条边就不该在这个队列里面了,但是你又不能控制把它删除。

所以直接用优先级队列也不太好。

第二种方式就是每次去遍历,因为我们这里是矩阵很方便,每次去遍历去找X集合的顶点与Y集合的顶点构成最小的边。但是时间复杂度挺高的。

其实我们还是用优先级队列,不过选边的时候要判一下环,如果选出来最小的边的两个顶点在一个集合是构成环的,不能选!

W Prim(Self& minTree, const V& src)
{size_t n = _vertexs.size();minTree._vertexs = _vertexs;minTree._IndexMap = _IndexMap;minTree._matrix.resize(n);for (size_t i = 0; i < n; ++i)minTree._matrix[i].resize(n, MAX_W);//从X->Y集合中连接的边里面选出最小的边vector<bool> X(n,false);vector<bool> Y(n, true);priority_queue<Edge, vector<Edge>, greater<Edge>> heap;size_t srci = GetVertexindex(src);//先把srci连接的边添加到队列中for (size_t i = 0; i < n; ++i){if(_matrix[srci][i] != MAX_W)heap.push(Edge(srci, i, _matrix[srci][i]));}X[srci] = true;Y[srci] = false;size_t sz = 0;W total = W();while (!heap.empty()){Edge minedge = heap.top();heap.pop();if (!X[minedge._dsti])//每次从两个集合中各选一个顶点构成的最小边,防止成环{cout << _vertexs[minedge._srci] << "->" << _vertexs[minedge._dsti] << ":" << minedge._w << endl;minTree._AddEdge(minedge._srci, minedge._dsti, minedge._w);sz++;total += minedge._w;X[minedge._dsti] = true;Y[minedge._dsti] = false;for (size_t i = 0; i < n; ++i){//已经选过的最小边,不要重复添加if (_matrix[minedge._dsti][i] != MAX_W && Y[i])heap.push(Edge(minedge._dsti, i, _matrix[minedge._dsti][i]));}}else{cout << "构成环:";cout << _vertexs[minedge._srci] << "->" << _vertexs[minedge._dsti] << ":" << minedge._w << endl;}}if (sz == n - 1){return total;}else{return -1;}}

5. 最短路径

最短路径问题:从在带权有向图G中的某一顶点出发,找出一条通往另一顶点的最短路径,最短也就是沿路径各边的权值总和达到最小。

一般而言:
最小生成树 -> 无向图
最短路径 -> 有向图

5.1 单源最短路径–Dijkstra算法

单源最短路径:一个起点到其他所有点,最短路径。

单源最短路径问题:给定一个图G = ( V , E ) G=(V,E)G=(V,E),求源结点s ∈ V s∈Vs∈V到图中每个结点v ∈ V v∈Vv∈V的最短路径。Dijkstra算法就适用于解决带权重的有向图上的单源最短路径问题,同时算法要求图中所有边的权重非负。一般在求解最短路径的时候都是已知一个起点和一个终点,所以使用Dijkstra算法求解过后也就得到了所需起点到终点的最短路径。

针对一个带权有向图G,将所有结点分为两组S和QS是已经确定最短路径的结点集合,在初始时为空(初始时就可以将源节点s放入,毕竟源节点到自己的代价是0)Q 为其余未确定最短路径的结点集合每次从Q 中找出一个起点到该结点代价最小的结点u ,将u 从Q 中移出,并放入S 中,对u 的每一个相邻结点v 进行松弛操作。松弛即对每一个相邻结点v ,判断源节点s到结点u 的代价与u 到v 的代价之和是否比原来s 到v 的代价更小,若代价比原来小则要将s 到v 的代价更新为s 到u 与u 到v 的代价之和,否则维持原样。如此一直循环直至集合Q 为空,即所有节点都已经查找过一遍并确定了最短路径,至于一些起点到达不了的结点在算法循环后其代价仍为初始设定的值,不发生变化。Dijkstra算法每次都是选择V-S中最小的路径节点来进行更新,并加入S中,所以该算法使用的是贪心策略。

Dijkstra算法存在的问题是不支持图中带负权路径,如果带有负权路径,则可能会找不到一些路径的最短路径

比如这里是以s为源点去找和其他点的最短路径。
刚开始S集合里面只有起点s,s到其他起点初始都是∞指的是还没有最短路径。s是自己到自己可以初始为0,初始选择a这个起点,以这个点去做松弛操作,去遍历与s相连的顶点去更新s到其他顶点的最短路径,然后从未被加入到S里面的点里面去选一个从源点到这些顶点的最短路径。以这个顶点开始在去做松弛操作。

Dijkstra算法贪心策略:每次去选从起点->还没加入到最短路径的顶点中去选最短路径那个顶点,去更新其连接的路径(做松弛操作)

用最小的边在去更新其他边也是相对很小。
在这里插入图片描述

如何存储起点到其他顶点最短路径的权值和存储最短路径呢?

它这里用的是抽象表示,用了两个数组,本来是二维的。但是压缩成了一维。
每个顶点都确定过一个下标。根据下标搞了一个数组,把起点到其他顶点的最短路径的权值存储到这个dist数组里。在根据下标搞一个Ppath记录起点到其他顶点的路径,数组里面存的是路径前一个顶点下标。

在这里插入图片描述

//顶点个数是 N, 时间复杂度 O(N^2)  空间复杂度 O(N)
void Dijkstra(const V& src, vector<W>& dist, vector<int>& Ppath)
{size_t n = _vertexs.size();size_t srci = GetVertexindex(src);//dist,记录srci-其他顶点最短路径权值数组dist.resize(n, MAX_W);//Ppath 记录srci-其他顶点最短路径父顶点数组Ppath.resize(n, -1);//初始化dist[srci] = 0;Ppath[srci] = srci;//已经确定最短路径的顶点集合vector<bool> S(n, false);// n个顶点更新N次for (size_t i = 0; i < n; ++i){//每次去选从起点->还未加入到最短路径的顶点中去选最短路径的那个顶点,去更新其连接的路径(松弛操作)W min = MAX_W;size_t u = -1;for (size_t j = 0; j < n; ++j){if (!S[j] && dist[j] < min){u = j;min = dist[j];}}S[u] = true;//选到的顶点加入到最短路径for (size_t v = 0; v < n; ++v){//松弛操作,已经加入到最短路径的顶点路径已经是最小不用更新,其他顶点如果 s -> u + u -> v < s -> v 更新if (!S[v] && _matrix[u][v] != MAX_W && dist[u] + _matrix[u][v] < dist[v]){dist[v] = dist[u] + _matrix[u][v];Ppath[v] = u;}}}		
}

打印最短路径,各个顶点的最短路径是倒着存的,存的是前一个顶点的下标。我们把路径算出来之后还要逆置一下才能把路径找到。

在这里插入图片描述

void PrintShortPath(const V& src,const vector<W>& dist,const vector<int>& Ppath)
{size_t srci = GetVertexindex(src);size_t n = _vertexs.size();for (size_t i = 0; i < n; ++i){if (i != srci)//自己到自己不算{//找出i顶点的路径,和并查集类似vector<int> path;int parent = i;while (parent != srci){path.push_back(parent);parent = Ppath[parent];//前一个顶点的下标}path.push_back(srci);reverse(path.begin(), path.end());for (auto& e : path){cout << _vertexs[e] << "->";}cout << "权值和 " << dist[i] << endl;}}
}

在这里插入图片描述

Dijkstra算法存在的问题是不支持图中带负权路径,如果带有负权路径,则可能会找不到一些路径的最短路径

可以看到 s -> y 并不是最短路径。Dijkstra算法本身用了一个贪心,如果对已加入到最小路径的顶点更新这个贪心就失效了。如果边的权值都是正的,以其他边去更新已经加入最小路径的顶点就比之前更大没有必要更新,但是有负数就不一样了,贪心就失效了。

在这里插入图片描述

5.2 单源最短路径–Bellman-Ford算法

Dijkstra算法只能用来解决正权图的单源最短路径问题,但有些题目会出现负权图。这时这个算法就不能帮助我们解决问题了,而bellman—ford算法可以解决负权图的单源最短路径问题它的优点是可以解决有负权边的单源最短路径问题,而且可以用来判断是否有负权回路。它也有明显的缺点,它的时间复杂度 O(N*E) (N是点数,E是边数)普遍是要高于Dijkstra算法O(N²)的。像这里如果我们使用邻接矩阵实现,那么遍历所有边的数量的时间复杂度就是O(N^3),这里也可以看出来Bellman-Ford就是一种暴力求解更新

在这里插入图片描述

s -> { j } 其他顶点的集合,要么是直接相连,要么是找中间边边去更新,Dijkstra是去找s->某个顶点特别短那就优先拿这条边去作为起始边去更新,它是我到这个顶点边是最短的,那我从这个顶点到其他顶点也是最短的。Bellman-Ford是去找终止边暴力去更新。

Dijkstra 最小起始边,贪心
Bellman-Ford 终止边,暴力

终止边就是以 i -> j 去进行更新,i -> j 就是图中所有边

s -> { j } 其他顶点,要么直接相连,要么 s -> i i -> j,这个时候仅需要探测s -> i 是通的 i -> j 也是通的 它们加起来比 s -> j 更小,就松弛更新一下。i -> j 代表图中所有边,拿图中所有边去暴力更新一遍。

Bellman-Ford算法借助终止边(i -> j ,图中所有边)暴力更新起点 -> { j } 所有顶点。要么直接相连,要么借助终止边。

但是拿所有边走一遍并不是说就一定能更新出来!

void BellmanFord(const V& src, vector<W>& dist, vector<int>& Ppath)
{size_t srci = GetVertexindex(src);size_t n = _vertexs.size();// vector<W> dist, 记录srci -> 其他顶点最短路径权值数组dist.resize(n, MAX_W);// vector<int> pPath 记录srci -> 其他顶点最短路径父顶点数组Ppath.resize(n, -1);// 先更新srci->srci为缺省值dist[srci] = W();Ppath[srci] = srci;// i -> j 更新一轮// 借助终止边i->j(图中所有顶点之间的边),更新srci到所有顶点的最小路径(做松弛操作)for (size_t i = 0; i < n; ++i){for (size_t j = 0; j < n; ++j){//srci -> i + i -> j < srci -> j if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j]){dist[j] = dist[i] + _matrix[i][j];Ppath[j] = i;}}}}

最短路径似乎更新出来了, 但是为什么s->z的权值不对呢?

在这里插入图片描述

接下来画图分析一下

第一次更新是 s->s s->y,因为更新规则是 dist[i] + _matrix[i][j] < dist[j],我们初始的时候是把 dist[srci] = W()给了初始值。先先更新与s直连的边。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

s->x x->t,这一句的更新就导致了问题。4 + (-2)< 6, s->t 最短路径更新成2,t的前一个顶点变成x。也就是从s到t的路径变成了 s -> y -> x -> t, s -> t 最短路径更新成2。

但是注意到 s -> t 的最短路径更新到2 ,而从 s -> z 要经过t,s -> z 路径因为我们更新了 s -> t 的路径,而变成了 s -> y -> x -> t -> z,但是s -> z 最短路径可没有更新,依旧是上次 s ->(直连) t -> z的最短路径2。所以 s -> t 有了路径更新,但是 s -> t 最短路径没有更新。权值和路径对不上。

在这里插入图片描述

只要你更新出了一条更短路径,可能就会影响其他路径。 如何解决?

s -> z, 在更新一次就变成了 s -> y -> x -> t -> z 的权值 -2了。

在更新一次就修正了,但是新更新路径又可能会影响其他路径,所以还要继续更新,最多更新n轮(极端情况下最多用n条边去更新某一个顶点)。

这里还有一个优化,可能某一轮就不会在更新了也不会影响其他路径。因此可以增加一个标记位,某一轮没有更新就结束更新。

//时间复杂度 O(N^3), 空间复杂度 O(N)
void BellmanFord(const V& src, vector<W>& dist, vector<int>& Ppath)
{size_t srci = GetVertexindex(src);size_t n = _vertexs.size();// vector<W> dist, 记录srci -> 其他顶点最短路径权值数组dist.resize(n, MAX_W);// vector<int> pPath 记录srci -> 其他顶点最短路径父顶点数组Ppath.resize(n, -1);// 先更新srci->srci为缺省值dist[srci] = W();Ppath[srci] = srci;// 总体最多更新n轮for (size_t k = 0; k < n; ++k){//优化bool update = false;// i -> j 更新一轮// 借助终止边i->j(图中所有顶点之间的边),更新srci到所有顶点的最小路径(做松弛操作)for (size_t i = 0; i < n; ++i){for (size_t j = 0; j < n; ++j){//srci -> i + i -> j < srci -> j if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j]){//只要更新出一条更短路径,可能会影响其他路径,在更新一次就修正了//但是新更新的路径又可能会影响其他路径,所以还要继续更新,最多更新n轮dist[j] = dist[i] + _matrix[i][j];Ppath[j] = i;update = true;}}}// 如果这个轮次中没有更新出更短路径,那么后续轮次就不需要再走了if (update == false)break;}
}

在这里插入图片描述

还有一个优化思路,第一个轮次所有边都会参与更新,但是第二个轮次并一定所有边都参与更新,只有那些第一个轮次更新的最短路径的会影响其他路径的,然后第二轮去更新就好了。具体可以搞一个队列优化。

第一轮更新:所有边入队列
后面的轮次:更新出最短路径的边入队列

在这里插入图片描述

在这里插入图片描述

Bellman-Ford算法它的优点是可以解决有负权边的单源最短路径问题,但是解决不了带负权回路的的单源最短路径问题。因此可以用来判断是否有负权回路

s->s 每次都会更新。

在这里插入图片描述

bool BellmanFord(const V& src, vector<W>& dist, vector<int>& Ppath)
{size_t srci = GetVertexindex(src);size_t n = _vertexs.size();// vector<W> dist, 记录srci -> 其他顶点最短路径权值数组dist.resize(n, MAX_W);// vector<int> pPath 记录srci -> 其他顶点最短路径父顶点数组Ppath.resize(n, -1);// 先更新srci->srci为缺省值dist[srci] = W();Ppath[srci] = srci;// 总体最多更新n轮for (size_t k = 0; k < n; ++k){//优化bool update = false;// i -> j 更新一轮// 借助终止边i->j(图中所有顶点之间的边),更新srci到所有顶点的最小路径(做松弛操作)for (size_t i = 0; i < n; ++i){for (size_t j = 0; j < n; ++j){//srci -> i + i -> j < srci -> j if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j]){//只要更新出一条更短路径,可能会影响其他路径,在更新一次就修正了//但是新更新的路径又可能会影响其他路径,所以还要继续更新,最多更新n轮dist[j] = dist[i] + _matrix[i][j];Ppath[j] = i;update = true;}}}// 如果这个轮次中没有更新出更短路径,那么后续轮次就不需要再走了if (update == false)break;}//更新n轮后还能更新就是带负权回路for (size_t i = 0; i < n; ++i){for (size_t j = 0; j < n; ++j){//srci -> i + i -> j < srci -> j if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j]){return false;}}}return true;
}

在这里插入图片描述

5.3 多源最短路径–Floyd-Warshall算法

Floyd-Warshall算法是解决图中任意两点间的最短路径的一种算法,也可以解决带负权路径

Dijkstra算法和BellmanFord算法也可以以所有点为起点也可以求出任意两点之间的最短距离。但是Dijkstra算法不能带负权,BellmanFord算法效率低一点。

Floyd-Warshall算法真正优势在于同时更新多源,既然要记录多源的权值和数组那就意味着一维已经不行了,那这个时候就要搞成一个二维的。二维就能记录任意两个点。它和一维的区别就是以前算 srci -> i i -> j 要去矩阵里面取,现在就去dist这个矩阵里面去取。

在这里插入图片描述

Floyd算法考虑的是一条最短路径的中间节点,即简单路径p={v1,v2,…,vn}上除v1和vn的任意节点。

任意两点之间要么直接相连,要么最多经过其它点(n - 2个顶点)。

Dijkstra算法是用最小起始边来算
BellmanFord算法是用终止边来算
Floyd-Warshall算法使用中间点来算

设k是p的一个中间节点,那么从i到j的最短路径p就被分成i到k和k到j的两段最短路径p1,p2。p1是从i到k且中间节点属于{1,2,…,k-1}取得的一条最短路径。p2是从k到j且中间节点属于{1,2,…,k-1}取得的一条最短路径。

在这里插入图片描述

Floyd-Warshall算法本质还是用了动态规划。距离都在dsti里面去取,i -> j 要么直接相连,要么经过 ((1…k)集合中的顶点(n-2个顶点)) i -> k,k - > j。取两种情况中的最小值为最短路径。

具体做法如下:

  1. 先将直接相连的 i -> j 的 dist ,Ppath初始化
  2. 最短路径更新,i -> 中间点 -> j,k作为中间点尝试更新 i -> j 的路径, 如果 i -> k,k -> j < i -> j 更新 dist[i][j]和Ppath[i][j],注意如果是i -> k,k -> j < i -> j,Ppath[i][j] 更新要注意,Ppath要的是跟 j 相连的上一个邻接顶点,如果 k 与 j 直接相连 Ppath[k][j]存的就是 k ,如果 k -> j 没有直接相连,k -> … -> x - > j,Ppath[k][j] 存的就是 x。所以Ppath[i][j] = Ppath[k][j] ,而不是直接等于 k。
void FloydWarshall(vector<vector<W>>& vvDist, vector<vector<int>>& vvPpath)
{size_t n = _vertexs.size();//初始化权值和路径矩阵vvDist.resize(n, vector<W>(n, MAX_W));vvPpath.resize(n, vector<int>(n, -1));//先将之间相连的 i->j 更新一下for (size_t i = 0; i < n; ++i){for (size_t j = 0; j < n; ++j){if (_matrix[i][j] != MAX_W){vvDist[i][j] = _matrix[i][j];vvPpath[i][j] = i;}if (i == j){vvDist[i][j] = W();}}}// 最短路径的更新i-> {其他顶点} ->j// K严格来说最多是n-2个,但是不能循环n-2次,要循环n次,因为 i -> j 一直在变,要把所有点作为中间点// abcdef  a->f k这次是a或者f 对于a->f也没有影响,  a->a a->f,  a->f f->f,for (size_t k = 0; k < n; ++k){for (size_t i = 0; i < n; ++i){for (size_t j = 0; j < n; ++j){// k 作为的中间点尝试去更新 i->j 的路径if (vvDist[i][k] != MAX_W && vvDist[k][j] != MAX_W&& vvDist[i][k] + vvDist[k][j] < vvDist[i][j]){vvDist[i][j] = vvDist[i][k] + vvDist[k][j];// 找跟j相连的上一个邻接顶点// 如果k->j 直接相连,上一个点就k,vvpPath[k][j]存就是k// 如果k->j 没有直接相连,k->...->x->j,vvpPath[k][j]存就是xvvPpath[i][j] = vvPpath[k][j];}}// 打印权值和路径矩阵观察数据for (size_t i = 0; i < n; ++i){for (size_t j = 0; j < n; ++j){if (vvDist[i][j] == MAX_W){printf("%3c", '*');}else{printf("%3d", vvDist[i][j]);}}cout << endl;}cout << endl;for (size_t i = 0; i < n; ++i){for (size_t j = 0; j < n; ++j){printf("%3d", vvPpath[i][j]);}cout << endl;}cout << "=================================" << endl;}}
}void TestFloydWarShall()
{const char* str = "12345";Graph<char, int, INT_MAX, true> g(str, strlen(str));g.AddEdge('1', '2', 3);g.AddEdge('1', '3', 8);g.AddEdge('1', '5', -4);g.AddEdge('2', '4', 1);g.AddEdge('2', '5', 7);g.AddEdge('3', '2', 4);g.AddEdge('4', '1', 2);g.AddEdge('4', '3', -5);g.AddEdge('5', '4', 6);vector<vector<int>> vvDist;vector<vector<int>> vvParentPath;g.FloydWarshall(vvDist, vvParentPath);//打印任意两点之间的最短路径for (size_t i = 0; i < strlen(str); ++i){g.PrintShortPath(str[i], vvDist[i], vvParentPath[i]);cout << endl;}
}

在这里插入图片描述

相关文章:

【高阶数据结构】图

图 1. 图的基本概念2. 图的存储结构2.1 邻接矩阵2.2 邻接表2.3 邻接矩阵的实现2.4 邻接表的实现 3. 图的遍历3.1 图的广度优先遍历3.2 图的深度优先遍历 4. 最小生成树4.1 Kruskal算法4.2 Prim算法 5. 最短路径5.1 单源最短路径--Dijkstra算法5.2 单源最短路径--Bellman-Ford算…...

调研-音视频

音视频 基础概念主要内容音频基础概念音频量化过程音频压缩技术视频基础概念视频bug视频编码H264视频像素格式YUVRGB参考文献基础概念 ● 实时音视频应用环节 ○ 采集、编码、前后处理、传输、解码、缓冲、渲染等很多环节。 主要内容 音频 基础概念 三要素:音调(音频)、…...

【数据结构】链式结构实现:二叉树

二叉树 一.快速创建一颗二叉树二.二叉树的遍历1.前序、中序、后序遍历&#xff08;深度优先遍历DFS&#xff09;2.层序遍历&#xff08;广度优先遍历BFS&#xff09; 三.二叉树节点的个数四.二叉树叶子节点的个数五.二叉树的高度六.二叉树第k层节点个数七.二叉树查找值为x的节点…...

20221元组

在Python语言中, (7)是一种可变的、有序的序列结构,其中元素可以重复。 A.元组(tuple) B. 字符串(str) C. 列表(list) D.集合(set) ChatGPT 说&#xff1a; ChatGPT 在Python中&#xff0c;选项 C 列表(list) 符合题目描述。 解释&#xff1a; 列表 (list) 是一种可变的、有…...

艾瑞白皮书解读(三)丨剖析制造业、工程设计、创投数据治理痛点与典型方案

2024年7月 艾瑞咨询公司对国内数据治理行业进行了研究&#xff0c;访问了国内多位大中型企业数据治理相关负责人&#xff0c;深度剖析中国企业在数字化转型过程中面临到的核心数据问题后&#xff0c;重磅发布《2024中国企业数据治理白皮书》&#xff08;以下简称“白皮书”&…...

如何在 Odoo 16 Studio 模块中自定义视图和报告

为了有效地运营公司&#xff0c;需要定制的软件系统。Odoo 平台提供针对单个应用程序量身定制的管理解决方案和用户友好的界面&#xff0c;以便开发应用程序&#xff0c;而无需更复杂的后端功能。该平台支持使用简单的拖放功能和内置工具创建和修改更多定制的 Odoo 应用程序。企…...

Redis的十大数据类型的常用命令(上)

目录 1.key的操作命令2.String的常用命令案例一&#xff1a;dy点赞案例二&#xff1a;文章的喜欢数 3. List的常用命令案例&#xff1a;公众号订阅的消息 4. Hash的常用命令案例&#xff1a;早期购物车设计 5. Set的常用命令案例一&#xff1a;抽奖小程序案例二&#xff1a;朋友…...

智慧服务管理平台小程序开发方案

智慧服务管理平台小程序系统为用户提供一站式、个性化的服务管理解决方案&#xff0c;帮助用户优化服务流程、提升服务效率、增强客户满意度。适用于智慧校园、食堂、养老、智慧停车、智慧园区、智慧医院、智慧农业、康养、智慧社区、智慧农场等行业场景。一、目标用户 企业客户…...

【轻松拿捏】Java中ArrayList 和 LinkedList 的区别是什么?

ArrayList 和 LinkedList 的区别是什么&#xff1f; 1. ArrayList 2. LinkedList 3.总结 &#x1f388;边走、边悟&#x1f388;迟早会好 ArrayList 和 LinkedList 都是 Java 中常用的 List 接口的实现类&#xff0c;但它们在内部结构和操作性能上有所不同。 1. ArrayLis…...

【排序篇】快速排序的非递归实现与归并排序的实现

&#x1f308;个人主页&#xff1a;Yui_ &#x1f308;Linux专栏&#xff1a;Linux &#x1f308;C语言笔记专栏&#xff1a;C语言笔记 &#x1f308;数据结构专栏&#xff1a;数据结构 文章目录 1 快速排序非递归2. 归并排序3.排序算法复杂度及稳定性分析 1 快速排序非递归 利…...

Java垃圾收集器工作原理

在Java编程中&#xff0c;对象的内存分配主要发生在堆&#xff08;Heap&#xff09;上。堆是Java虚拟机&#xff08;JVM&#xff09;中的一块运行时数据区&#xff0c;用于存放由new关键字创建的对象和数组。与栈&#xff08;Stack&#xff09;内存分配相比&#xff0c;堆内存分…...

STM32CubeMX stm32不限长度使用DMA收发串口数据

STM32CubeMX 配置 代码 stm32h7xx_it.c /*** brief This function handles UART7 global interrupt.*/ void UART7_IRQHandler(void) {/* USER CODE BEGIN UART7_IRQn 0 */if (UART7 huart7.Instance) // 判断是否是空闲中断{if (__HAL_UART_GET_FLAG(&huart7, UART_FLA…...

Jmeter系列之作用域、执行顺序

这一节主要解释元件作用域和执行顺序&#xff0c;以及整理之前说过的参数化的方式。 作用域 之前也留下了一个问题。怎么给不同的请求设置不同的Header&#xff1f;后续也透露了可以使用Sample Controller&#xff0c;结合元件的作用域来实现 在Jmeter中&#xff0c;元件的作…...

舜宇光学科技社招校招入职测评:商业推理测验真题汇总、答题要求、高分技巧

舜宇光学科技&#xff08;集团&#xff09;有限公司&#xff0c;成立于1984年&#xff0c;是全球领先的综合光学零件及产品制造商。2007年在香港联交所主板上市&#xff0c;股票代码2382.HK。公司专注于光学产品的设计、研发、生产及销售&#xff0c;产品广泛应用于手机、汽车、…...

C语言——构造(结构体)

指针——内存操作 我们对于内存的操作借助于 <string.h>这个库提供的内存操作函数。 内存填充 头文件: #include<string.h> 函数原型: void*memset(void *s,int c,size_t n); 函数功能&#xff1a; 填充s开始的堆内存空间前n个字节&#xff0c;使得每个字节值为c…...

京东2025届秋招 算法开发工程师 第2批笔试

目录 1. 第一题2. 第二题3. 第三题 ⏰ 时间&#xff1a;2024/08/17 &#x1f504; 输入输出&#xff1a;ACM格式 ⏳ 时长&#xff1a;2h 本试卷还有选择题部分&#xff0c;但这部分比较简单就不再展示。 1. 第一题 村子里有一些桩子&#xff0c;从左到右高度依次为 1 , 1 2…...

模具监视器的技术参数有哪些

模具监视器的技术参数涵盖了多个方面&#xff0c;这些参数对于确保模具监视器的性能、稳定性和检测精度至关重要。以下是一些主要的技术参数&#xff1a; 一、显示器参数 屏幕尺寸&#xff1a;常见的模具监视器显示器尺寸为12.5英寸至13.5英寸&#xff0c;具体尺寸可能因不同…...

使用QGIS配置管线流向地图

一、需求概述 在管网项目中,需要进行地图配置使用QGIS显示管网的流向。 二、目标 配置一副管网地图,可以在地图上显示出每个管段的流向。 三、数据结构 管网数据: id[管线编码]source[起始节点ID]target[终点节点ID]dir[方向]1100101FT2101102FT……………………节点数据…...

白骑士的C#教学附加篇 5.1 C#开发工具

系列目录 上一篇&#xff1a;白骑士的C#教学实战项目篇 4.4 游戏开发 在这一部分&#xff0c;我们将介绍一些额外的内容和工具&#xff0c;以帮助您提高 C# 开发的效率和质量。掌握合适的开发工具和调试技巧&#xff0c;可以让您在编写和维护代码时更加高效和从容。 开发工具对…...

C++中的多线程编程和锁机制

二、多线程、锁 2.1 C语言线程库pthread&#xff08;POSIX threads&#xff09; 2.2.1 线程创建 pthread_create #include <pthread.h>pthread_t thread; ThreadData args {1, "Hello from parameterized thread"}; int result pthread_create(&threa…...

零门槛NAS搭建:WinNAS如何让普通电脑秒变私有云?

一、核心优势&#xff1a;专为Windows用户设计的极简NAS WinNAS由深圳耘想存储科技开发&#xff0c;是一款收费低廉但功能全面的Windows NAS工具&#xff0c;主打“无学习成本部署” 。与其他NAS软件相比&#xff0c;其优势在于&#xff1a; 无需硬件改造&#xff1a;将任意W…...

线程同步:确保多线程程序的安全与高效!

全文目录&#xff1a; 开篇语前序前言第一部分&#xff1a;线程同步的概念与问题1.1 线程同步的概念1.2 线程同步的问题1.3 线程同步的解决方案 第二部分&#xff1a;synchronized关键字的使用2.1 使用 synchronized修饰方法2.2 使用 synchronized修饰代码块 第三部分&#xff…...

大语言模型如何处理长文本?常用文本分割技术详解

为什么需要文本分割? 引言:为什么需要文本分割?一、基础文本分割方法1. 按段落分割(Paragraph Splitting)2. 按句子分割(Sentence Splitting)二、高级文本分割策略3. 重叠分割(Sliding Window)4. 递归分割(Recursive Splitting)三、生产级工具推荐5. 使用LangChain的…...

转转集团旗下首家二手多品类循环仓店“超级转转”开业

6月9日&#xff0c;国内领先的循环经济企业转转集团旗下首家二手多品类循环仓店“超级转转”正式开业。 转转集团创始人兼CEO黄炜、转转循环时尚发起人朱珠、转转集团COO兼红布林CEO胡伟琨、王府井集团副总裁祝捷等出席了开业剪彩仪式。 据「TMT星球」了解&#xff0c;“超级…...

linux 下常用变更-8

1、删除普通用户 查询用户初始UID和GIDls -l /home/ ###家目录中查看UID cat /etc/group ###此文件查看GID删除用户1.编辑文件 /etc/passwd 找到对应的行&#xff0c;YW343:x:0:0::/home/YW343:/bin/bash 2.将标红的位置修改为用户对应初始UID和GID&#xff1a; YW3…...

【JavaSE】绘图与事件入门学习笔记

-Java绘图坐标体系 坐标体系-介绍 坐标原点位于左上角&#xff0c;以像素为单位。 在Java坐标系中,第一个是x坐标,表示当前位置为水平方向&#xff0c;距离坐标原点x个像素;第二个是y坐标&#xff0c;表示当前位置为垂直方向&#xff0c;距离坐标原点y个像素。 坐标体系-像素 …...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

Vue 模板语句的数据来源

&#x1f9e9; Vue 模板语句的数据来源&#xff1a;全方位解析 Vue 模板&#xff08;<template> 部分&#xff09;中的表达式、指令绑定&#xff08;如 v-bind, v-on&#xff09;和插值&#xff08;{{ }}&#xff09;都在一个特定的作用域内求值。这个作用域由当前 组件…...

拟合问题处理

在机器学习中&#xff0c;核心任务通常围绕模型训练和性能提升展开&#xff0c;但你提到的 “优化训练数据解决过拟合” 和 “提升泛化性能解决欠拟合” 需要结合更准确的概念进行梳理。以下是对机器学习核心任务的系统复习和修正&#xff1a; 一、机器学习的核心任务框架 机…...

js 设置3秒后执行

如何在JavaScript中延迟3秒执行操作 在JavaScript中&#xff0c;要设置一个操作在指定延迟后&#xff08;例如3秒&#xff09;执行&#xff0c;可以使用 setTimeout 函数。setTimeout 是JavaScript的核心计时器方法&#xff0c;它接受两个参数&#xff1a; 要执行的函数&…...