数据结构——图(遍历,最小生成树,最短路径)

数据结构——图(遍历,最小生成树,最短路径)

目录

一.图的基本概念

二.图的存储结构

1.邻接矩阵

2.邻接表

三.图的遍历

1.图的广度优先遍历

2.图的深度优先遍历

四.最小生成树

1.Kruskal算法

2.Prim算法

五.最短路径

1.单源最短路径--Dijkstra算法

2.单源最短路径--Bellman-Ford算法

3.多源最短路径--Floyd-Warshall算法

六.整体实现

1.UnionFindSet.h

2.Graph.h

3.test.cpp


一.图的基本概念

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

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

        E = {(x,y)|x,y属于V}或者E = {|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的一条边(弧),和是两条不同的边,比如下图G3和G4为有向图。在无向图中,顶点对(x, y) 是无序的,顶点对(x,y)称为顶点x和顶点y相关联的一条边,这条边没有特定方向,(x, y)和(y,x) 是同一条边,比如下图G1和G2为无向图。注意:无向边(x, y)等于有向边和

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

        邻接顶点:在无向图中G中,若(u, v)是E(G)中的一条边,则称u和v互为邻接顶点,并称边(u,v)依附于顶点u和v;在有向图G中,若是E(G)中的一条边,则称顶点u邻接到v,顶点v邻接自顶 点u,并称边与顶点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个顶点和n1条边

二.图的存储结构

1.邻接矩阵

        因为节点与节点之间的关系就是连通与否,即为0或者1,因此邻接矩阵(二维数组)即是:先用一 个数组将定点保存,然后采用矩阵来表示节点与节点之间的关系

注意:

  1. 无向图的邻接矩阵是对称的,第i行(列)元素之和,就是顶点i的度。有向图的邻接矩阵则不一 定是对称的,第i行(列)元素之后就是顶点i的出(入)度
  2. 如果边带有权值,并且两个节点之间是连通的,上图中的边的关系就用权值代替,如果两个 顶点不通,则使用无穷大代替
  3. 用邻接矩阵存储图的有点是能够快速知道两个顶点是否连通,缺陷是如果顶点比较多,边比 较少时,矩阵中存储了大量的0成为系数矩阵,比较浪费空间,并且要求两个节点之间的路 径不是很好求
namespace matrix { template<class V, class W, W MAX_W = INT_MAX, bool Direction = false> class Graph { typedef Graph<V, W, MAX_W, Direction> Self; public: //图的创建 //1.IO输入->不方便测试,oj中更适合 //2.图结构关系写到文件,读取文件 //3.手动添加边 Graph() = default; 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 < _matrix.size(); ++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 { //assert(false); throw invalid_argument("顶点不存在"); return -1; } } void _AddEdge(size_t srci, 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 < _matrix.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; //邻接矩阵 }; }
void TestGraph1() { Graph<char, int> g("0123", 4); //Graph<char, int, 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(); }

邻接矩阵总结:

  1. 邻接矩阵存储方式非常适合稠密图
  2. 邻接矩阵O(1)判断两个顶点的连接关系并取到权值
  3. 相对而言不适合查找一个顶点连接所有边----O(N)

2.邻接表

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

1.无向图邻接表存储        

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

2.有向图邻接表存储

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

namespace link_table { template<class W> struct Edge { //int _srci; int _dsti; //目标点的下标 W _w; //权值 Edge<W>* _next; Edge(int dsti, const W& w) :_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 { //assert(false); throw invalid_argument("顶点不存在"); return -1; } } void AddEdge(const V& src, const V& dst, const W& w) { size_t srci = GetVertexIndex(src); size_t dsti = GetVertexIndex(dst); Edge* eg = new Edge(dsti, w); eg->_next = _tables[srci]; _tables[srci] = eg; if (Direction == false) { Edge* eg = new Edge(srci, w); eg->_next = _tables[dsti]; _tables[dsti] = eg; } } 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; //邻接表 }; }
void TestGraph2() { string a[] = { "张三", "李四", "王五", "赵六" }; //Graph<string, int, true> g1(a, 4); Graph<string, int> g1(a, 4); g1.AddEdge("张三", "李四", 100); g1.AddEdge("张三", "王五", 200); g1.AddEdge("王五", "赵六", 30); g1.Print(); }

邻接表总结:

  1. 适合存储稀疏图
  2. 适合查找一个顶点连接出去的边
  3. 不适合确定两个顶点是否相连及权值

三.图的遍历

1.图的广度优先遍历

void BFS(const V& src) { size_t srci = GetVertexIndex(src); //队列和标记数组 queue<int> q; vector<bool> visited(_vertexs.size(), false); q.push(srci); visited[srci] = true; int levelSize = 1; size_t n = _vertexs.size(); while (!q.empty()) { //一层一层出 for (int i = 0; i < levelSize; ++i) { int front = q.front(); q.pop(); cout << front << ":" << _vertexs[front] << " "; //把front顶点的邻接顶点入队列 for (size_t i = 0; i < n; ++i) { if (_matrix[front][i] != MAX_W) { if (visited[i] == false) { q.push(i); visited[i] = true; } } } } cout << endl; levelSize = q.size(); } cout << endl; }
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("张三"); }

2.图的深度优先遍历

void _DFS(size_t srci, vector<bool>& visited) { cout << srci << ":" << _vertexs[srci] << endl; visited[srci] = true; //找一个和srci相邻的没有访问过的点,去深度遍历 for (size_t i = 0; i < _vertexs.size(); ++i) { if (_matrix[srci][i] != MAX_W && visited[i] == false) { _DFS(i, visited); } } } void DFS(const V& src) { size_t srci = GetVertexIndex(src); vector<bool> visited(_vertexs.size(), false); _DFS(srci, visited); }
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.DFS("张三"); }

四.最小生成树

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

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

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

1.Kruskal算法

        Kruskal算法(克鲁斯卡尔算法)任给一个有n个顶点的连通网络N={V,E}, 首先构造一个由这n个顶点组成、不含任何边的图G={V,NULL},其中每个顶点自成一个连通分量, 其次不断从E中取出权值最小的一条边(若有多条任取其一),若该边的两个顶点来自不同的连通分量,则将此边加入到G中。如此重复,直到所有顶点在同一个连通分量上为止

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

struct Edge { size_t _srci; size_t _dsti; W _w; Edge(size_t srci, size_t dsti, const 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); } priority_queue<Edge, vector<Edge>, greater<Edge>> minque; for (size_t i = 0; i < n; ++i) { for (size_t j = 0; j < n; ++j) { if (i < j && _matrix[i][j] != MAX_W) { minque.push(Edge(i, j, _matrix[i][j])); } } } cout << "Kruskal开始选边:" << endl; //选出n-1条边 int size = 0; W totalW = W(); UnionFindSet ufs(n); while (!minque.empty()) { Edge min = minque.top(); minque.pop(); if (!ufs.InSet(min._srci, min._dsti)) { cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl; minTree._AddEdge(min._srci, min._dsti, min._w); ufs.Union(min._srci, min._dsti); ++size; totalW += min._w; } else { cout << "构成环"; cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl; } } cout << endl; if (size == n - 1) { return totalW; } else { return W(); } } 
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(); cout << endl; }

2.Prim算法

        Prim算法(普里姆算法)

W Prim(Self& minTree, const W& src) { size_t srci = GetVertexIndex(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); } vector<bool> X(n, false); vector<bool> Y(n, true); X[srci] = true; Y[srci] = false; //从X到Y集合相连接的边中选出值最小的边 priority_queue<Edge, vector<Edge>, greater<Edge>> minq; //将srci连接的边添加到队列中 for (size_t i = 0; i < n; ++i) { if (_matrix[srci][i] != MAX_W) { minq.push(Edge(srci, i, _matrix[srci][i])); } } cout << "Prim开始选边:" << endl; int size = 0; W totalW = W(); while (!minq.empty()) { Edge min = minq.top(); minq.pop(); if (X[min._dsti]) { cout << "构成环"; cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl; } else { minTree._AddEdge(min._srci, min._dsti, min._w); cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl; X[min._dsti] = true; Y[min._dsti] = false; ++size; totalW += min._w; if (size == n - 1) break; for (size_t i = 0; i < n; ++i) { if (_matrix[min._dsti][i] != MAX_W && Y[i]) { minq.push(Edge(min._dsti, i, _matrix[min._dsti][i])); } } } } cout << endl; if (size == n - 1) { return totalW; } else { return W(); } }
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> pminTree; cout << "Prim:" << g.Prim(pminTree, 'a') << endl; pminTree.Print(); } 

五.最短路径

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

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和Q,S是已经确定最短路径的结点集合,在初始时为空(初始时就可以将源节点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算法(迪杰斯特拉算法)存在的问题是不支持图中带负权路径,如果带有负权路径,则可能会找不到一些路径的最短路径

//时间复杂度:O(N^2),空间复杂度:O(N) void Dijkstra(const V& src, vector<W>& dist, vector<int>& pPath) { size_t srci = GetVertexIndex(src); size_t n = _vertexs.size(); dist.resize(n, MAX_W); pPath.resize(n, -1); dist[srci] = 0; pPath[srci] = srci; //已经确定最短路径的顶点集合 vector<bool> S(n, false); for (size_t j = 0; j < n; ++j) { //选出最短路径顶点且不在S更新其他路径 int u = 0; W min = MAX_W; for (size_t i = 0; i < n; ++i) { if (S[i] == false && dist[i] < min) { u = i; min = dist[i]; } } S[u] = true; //松弛更新 for (size_t v = 0; v < n; ++v) { if (S[v] == false && _matrix[u][v] != MAX_W && dist[u] + _matrix[u][v] < dist[v]) { dist[v] = dist[u] + _matrix[u][v]; pPath[v] = u; } } } }
void TestGraphDijkstra() { const char* str = "syztx"; Graph<char, int, INT_MAX, true> g(str, strlen(str)); g.AddEdge('s', 't', 10); g.AddEdge('s', 'y', 5); g.AddEdge('y', 't', 3); g.AddEdge('y', 'x', 9); g.AddEdge('y', 'z', 2); g.AddEdge('z', 's', 7); g.AddEdge('z', 'x', 6); g.AddEdge('t', 'y', 2); g.AddEdge('t', 'x', 1); g.AddEdge('x', 'z', 4); vector<int> dist; vector<int> parentPath; g.Dijkstra('s', dist, parentPath); g.PrintShortPath('s', dist, parentPath); }

2.单源最短路径--Bellman-Ford算法

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

//时间复杂度:O(N^3),空间复杂度:O(N) 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(); for (size_t k = 0; k < n; ++k) { bool updata = false; cout << "更新第" << k << "轮:" << endl; for (size_t i = 0; i < n; ++i) { for (size_t j = 0; j < n; ++j) { if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j]) { updata = true; cout << _vertexs[i] << "->" << _vertexs[j] << ":" << _matrix[i][j] << endl; dist[j] = dist[i] + _matrix[i][j]; pPath[j] = i; } } } //如果这个轮次没有更新出最短路径,后续轮次就不需要再走 if (updata == false) break; } //如果还能更新就是带负权回路 for (size_t i = 0; i < n; ++i) { for (size_t j = 0; j < n; ++j) { if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j]) { return false; } } } return true; }
void TestGraphBellmanFord() { const char* str = "syztx"; Graph<char, int, INT_MAX, true> g(str, strlen(str)); g.AddEdge('s', 't', 6); g.AddEdge('s', 'y', 7); g.AddEdge('y', 'z', 9); g.AddEdge('y', 'x', -3); //g.AddEdge('y', 's', 1);//新增 g.AddEdge('z', 's', 2); g.AddEdge('z', 'x', 7); g.AddEdge('t', 'x', 5); g.AddEdge('t', 'y', 8); //g.AddEdge('t', 'y', -8);//更改 g.AddEdge('t', 'z', -4); g.AddEdge('x', 't', -2); vector<int> dist; vector<int> parentPath; if (g.BellmanFord('s', dist, parentPath)) { g.PrintShortPath('s', dist, parentPath); } else { cout << "存在负权回路" << endl; } }

3.多源最短路径--Floyd-Warshall算法

        Floyd-Warshall算法(弗洛伊德算法)是解决任意两点间的最短路径的一种算法

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

        设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算法本质是三维动态规划,D[i][j][k]表示从点i到点j只经过0到k个点最短路径,然后建立 起转移方程,然后通过空间优化,优化掉最后一维度,变成一个最短路径的迭代算法,最后即得到所有点的最短路径

void FloydWarShall(vector<vector<W>>& vvDist, vector<vector<int>>& vvpPath) { size_t n = _vertexs.size(); vvDist.resize(n); vvpPath.resize(n); for (size_t i = 0; i < n; ++i) { vvDist[i].resize(n, MAX_W); vvpPath[i].resize(n, -1); } 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] = 0; } } } //最短路径的更新 for (size_t k = 0; k < n; ++k) { for (size_t i = 0; i < n; ++i) { for (size_t j = 0; j < n; ++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]; vvpPath[i][j] = vvpPath[k][j]; } } } } }
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.UnionFindSet.h

#pragma once #include<vector> class UnionFindSet { public: UnionFindSet(size_t n) :_ufs(n, -1) { } int FindRoot(int x) { int root = x; while (_ufs[root] >= 0) root = _ufs[root]; //路径压缩 while (_ufs[x] >= 0) { int parent = _ufs[x]; _ufs[x] = root; x = parent; } return root; } bool Union(int x1, int x2) { int root1 = FindRoot(x1); int root2 = FindRoot(x2); if (root1 == root2)//x1和x2本来就在一个集合中 return false; //数据量小的向大的合并 if (abs(_ufs[root1]) < abs(_ufs[root2])) swap(root1, root2); _ufs[root1] += _ufs[root2]; _ufs[root2] = root1; return true; } bool InSet(int x1, int x2) { return FindRoot(x1) == FindRoot(x2); } size_t SetSize() { size_t n = 0; for (auto& e : _ufs) { if (e < 0) ++n; } return n; } private: vector<int> _ufs; }; void TestUoionFindSet() { UnionFindSet ufs(10); ufs.Union(8, 9); ufs.Union(7, 8); ufs.Union(6, 7); ufs.Union(5, 6); ufs.Union(4, 5); }

2.Graph.h

#pragma once #include<vector> #include<string> #include<map> #include<queue> #include<set> #include<functional> namespace matrix { template<class V, class W, W MAX_W = INT_MAX, bool Direction = false> class Graph { typedef Graph<V, W, MAX_W, Direction> Self; public: //图的创建 //1.IO输入->不方便测试,oj中更适合 //2.图结构关系写到文件,读取文件 //3.手动添加边 Graph() = default; 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 < _matrix.size(); ++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 { //assert(false); throw invalid_argument("顶点不存在"); return -1; } } void _AddEdge(size_t srci, 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 < _matrix.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 BFS(const V& src) { size_t srci = GetVertexIndex(src); //队列和标记数组 queue<int> q; vector<bool> visited(_vertexs.size(), false); q.push(srci); visited[srci] = true; int levelSize = 1; size_t n = _vertexs.size(); while (!q.empty()) { //一层一层出 for (int i = 0; i < levelSize; ++i) { int front = q.front(); q.pop(); cout << front << ":" << _vertexs[front] << " "; //把front顶点的邻接顶点入队列 for (size_t i = 0; i < n; ++i) { if (_matrix[front][i] != MAX_W) { if (visited[i] == false) { q.push(i); visited[i] = true; } } } } cout << endl; levelSize = q.size(); } cout << endl; } void _DFS(size_t srci, vector<bool>& visited) { cout << srci << ":" << _vertexs[srci] << endl; visited[srci] = true; //找一个和srci相邻的没有访问过的点,去深度遍历 for (size_t i = 0; i < _vertexs.size(); ++i) { if (_matrix[srci][i] != MAX_W && visited[i] == false) { _DFS(i, visited); } } } void DFS(const V& src) { size_t srci = GetVertexIndex(src); vector<bool> visited(_vertexs.size(), false); _DFS(srci, visited); } struct Edge { size_t _srci; size_t _dsti; W _w; Edge(size_t srci, size_t dsti, const 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); } priority_queue<Edge, vector<Edge>, greater<Edge>> minque; for (size_t i = 0; i < n; ++i) { for (size_t j = 0; j < n; ++j) { if (i < j && _matrix[i][j] != MAX_W) { minque.push(Edge(i, j, _matrix[i][j])); } } } cout << "Kruskal开始选边:" << endl; //选出n-1条边 int size = 0; W totalW = W(); UnionFindSet ufs(n); while (!minque.empty()) { Edge min = minque.top(); minque.pop(); if (!ufs.InSet(min._srci, min._dsti)) { cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl; minTree._AddEdge(min._srci, min._dsti, min._w); ufs.Union(min._srci, min._dsti); ++size; totalW += min._w; } else { cout << "构成环"; cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl; } } cout << endl; if (size == n - 1) { return totalW; } else { return W(); } } W Prim(Self& minTree, const W& src) { size_t srci = GetVertexIndex(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); } vector<bool> X(n, false); vector<bool> Y(n, true); X[srci] = true; Y[srci] = false; //从X到Y集合相连接的边中选出值最小的边 priority_queue<Edge, vector<Edge>, greater<Edge>> minq; //将srci连接的边添加到队列中 for (size_t i = 0; i < n; ++i) { if (_matrix[srci][i] != MAX_W) { minq.push(Edge(srci, i, _matrix[srci][i])); } } cout << "Prim开始选边:" << endl; int size = 0; W totalW = W(); while (!minq.empty()) { Edge min = minq.top(); minq.pop(); if (X[min._dsti]) { cout << "构成环"; cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl; } else { minTree._AddEdge(min._srci, min._dsti, min._w); cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl; X[min._dsti] = true; Y[min._dsti] = false; ++size; totalW += min._w; if (size == n - 1) break; for (size_t i = 0; i < n; ++i) { if (_matrix[min._dsti][i] != MAX_W && Y[i]) { minq.push(Edge(min._dsti, i, _matrix[min._dsti][i])); } } } } cout << endl; if (size == n - 1) { return totalW; } else { return W(); } } 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; size_t parenti = i; while (parenti != srci) { path.push_back(parenti); parenti = pPath[parenti]; } path.push_back(srci); reverse(path.begin(), path.end()); for (auto index : path) { cout << _vertexs[index] << "->"; } cout << "权值和: " << dist[i] << endl; } } } //时间复杂度:O(N^2),空间复杂度:O(N) void Dijkstra(const V& src, vector<W>& dist, vector<int>& pPath) { size_t srci = GetVertexIndex(src); size_t n = _vertexs.size(); dist.resize(n, MAX_W); pPath.resize(n, -1); dist[srci] = 0; pPath[srci] = srci; //已经确定最短路径的顶点集合 vector<bool> S(n, false); for (size_t j = 0; j < n; ++j) { //选出最短路径顶点且不在S更新其他路径 int u = 0; W min = MAX_W; for (size_t i = 0; i < n; ++i) { if (S[i] == false && dist[i] < min) { u = i; min = dist[i]; } } S[u] = true; //松弛更新 for (size_t v = 0; v < n; ++v) { if (S[v] == false && _matrix[u][v] != MAX_W && dist[u] + _matrix[u][v] < dist[v]) { dist[v] = dist[u] + _matrix[u][v]; pPath[v] = u; } } } } //时间复杂度:O(N^3),空间复杂度:O(N) 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(); for (size_t k = 0; k < n; ++k) { bool updata = false; cout << "更新第" << k << "轮:" << endl; for (size_t i = 0; i < n; ++i) { for (size_t j = 0; j < n; ++j) { if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j]) { updata = true; cout << _vertexs[i] << "->" << _vertexs[j] << ":" << _matrix[i][j] << endl; dist[j] = dist[i] + _matrix[i][j]; pPath[j] = i; } } } //如果这个轮次没有更新出最短路径,后续轮次就不需要再走 if (updata == false) break; } //如果还能更新就是带负权回路 for (size_t i = 0; i < n; ++i) { for (size_t j = 0; j < n; ++j) { if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j]) { return false; } } } return true; } void FloydWarShall(vector<vector<W>>& vvDist, vector<vector<int>>& vvpPath) { size_t n = _vertexs.size(); vvDist.resize(n); vvpPath.resize(n); for (size_t i = 0; i < n; ++i) { vvDist[i].resize(n, MAX_W); vvpPath[i].resize(n, -1); } 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] = 0; } } } //最短路径的更新 for (size_t k = 0; k < n; ++k) { for (size_t i = 0; i < n; ++i) { for (size_t j = 0; j < n; ++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]; vvpPath[i][j] = vvpPath[k][j]; } } } } } private: vector<V> _vertexs; //顶点集合 map<V, int> _indexMap; //顶点映射下标 vector<vector<W>> _matrix; //邻接矩阵 }; void TestGraph1() { Graph<char, int> g("0123", 4); //Graph<char, int, 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(); } 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("张三"); } 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(); cout << endl; Graph<char, int> pminTree; cout << "Prim:" << g.Prim(pminTree, 'a') << endl; pminTree.Print(); } void TestGraphDijkstra() { const char* str = "syztx"; Graph<char, int, INT_MAX, true> g(str, strlen(str)); g.AddEdge('s', 't', 10); g.AddEdge('s', 'y', 5); g.AddEdge('y', 't', 3); g.AddEdge('y', 'x', 9); g.AddEdge('y', 'z', 2); g.AddEdge('z', 's', 7); g.AddEdge('z', 'x', 6); g.AddEdge('t', 'y', 2); g.AddEdge('t', 'x', 1); g.AddEdge('x', 'z', 4); vector<int> dist; vector<int> parentPath; g.Dijkstra('s', dist, parentPath); g.PrintShortPath('s', dist, parentPath); } void TestGraphBellmanFord() { const char* str = "syztx"; Graph<char, int, INT_MAX, true> g(str, strlen(str)); g.AddEdge('s', 't', 6); g.AddEdge('s', 'y', 7); g.AddEdge('y', 'z', 9); g.AddEdge('y', 'x', -3); //g.AddEdge('y', 's', 1);//新增 g.AddEdge('z', 's', 2); g.AddEdge('z', 'x', 7); g.AddEdge('t', 'x', 5); g.AddEdge('t', 'y', 8); //g.AddEdge('t', 'y', -8);//更改 g.AddEdge('t', 'z', -4); g.AddEdge('x', 't', -2); vector<int> dist; vector<int> parentPath; if (g.BellmanFord('s', dist, parentPath)) { g.PrintShortPath('s', dist, parentPath); } else { 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; } } } namespace link_table { template<class W> struct Edge { //int _srci; int _dsti; //目标点的下标 W _w; //权值 Edge<W>* _next; Edge(int dsti, const W& w) :_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 { //assert(false); throw invalid_argument("顶点不存在"); return -1; } } void AddEdge(const V& src, const V& dst, const W& w) { size_t srci = GetVertexIndex(src); size_t dsti = GetVertexIndex(dst); Edge* eg = new Edge(dsti, w); eg->_next = _tables[srci]; _tables[srci] = eg; if (Direction == false) { Edge* eg = new Edge(srci, w); eg->_next = _tables[dsti]; _tables[dsti] = eg; } } 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; //邻接表 }; void TestGraph2() { string a[] = { "张三", "李四", "王五", "赵六" }; //Graph<string, int, true> g1(a, 4); Graph<string, int> g1(a, 4); g1.AddEdge("张三", "李四", 100); g1.AddEdge("张三", "王五", 200); g1.AddEdge("王五", "赵六", 30); g1.Print(); } }

3.test.cpp

#include<iostream> using namespace std; #include"UnionFindSet.h" #include"Graph.h" int main() { //TestUoionFindSet(); //matrix::TestGraph1(); //matrix::TestBDFS(); //matrix::TestGraphMinTree(); //matrix::TestGraphDijkstra(); //matrix::TestGraphBellmanFord(); matrix::TestFloydWarShall(); //link_table::TestGraph2(); return 0; }

Read more

毕业设计源码:Python音乐推荐系统 Django+Echarts+协同过滤算法+前端三剑客 课程设计 毕业设计(建议收藏)✅

毕业设计源码:Python音乐推荐系统 Django+Echarts+协同过滤算法+前端三剑客 课程设计 毕业设计(建议收藏)✅

博主介绍:✌全网粉丝10W+,前互联网大厂软件研发、集结硕博英豪成立工作室。专注于计算机相关专业项目实战6年之久,选择我们就是选择放心、选择安心毕业✌ > 🍅想要获取完整文章或者源码,或者代做,拉到文章底部即可与我联系了。🍅 点击查看作者主页,了解更多项目! 🍅感兴趣的可以先收藏起来,点赞、关注不迷路,大家在毕设选题,项目以及论文编写等相关问题都可以给我留言咨询,希望帮助同学们顺利毕业 。🍅 1、毕业设计:2026年计算机专业毕业设计选题汇总(建议收藏)✅ 2、大数据毕业设计:2026年选题大全 深度学习 python语言 JAVA语言 hadoop和spark(建议收藏)✅ 1、项目介绍 技术栈 以Python为开发语言,基于Django框架搭建系统整体架构,集成基于用户的协同过滤推荐算法实现核心推荐功能,运用Echarts完成数据可视化展示,前端通过HTML、CSS、JavaScript构建交互页面,采用MySQL或PostgreSQL数据库存储各类业务数据。 功能模块 * 可视化界面 * 首页 * 音乐播放与信息展示 * 音乐详情页 * 音乐推

By Ne0inhk
Python爬虫(47)Python异步爬虫与K8S弹性伸缩:构建百万级并发数据采集引擎

Python爬虫(47)Python异步爬虫与K8S弹性伸缩:构建百万级并发数据采集引擎

目录 * 一、背景与行业痛点 * 二、核心技术架构解析 * 2.1 异步爬虫引擎设计 * 2.2 K8S弹性伸缩架构 * 三、生产环境实践数据 * 3.1 性能基准测试 * 3.2 成本优化效果 * 四、高级优化技巧 * 4.1 协程级熔断降级 * 4.2 预测式扩容 * 五、总结 * 🌈Python爬虫相关文章(推荐) 一、背景与行业痛点 在数字经济时代,企业每天需要处理TB级结构化数据。某头部金融风控平台曾面临以下挑战: 数据时效性:需实时采集10万+新闻源,传统爬虫系统延迟超12小时 反爬对抗:目标站点采用IP轮询+设备指纹识别,单IP请求被限速至10RPM 成本困境:固定资源池模式导致闲时资源浪费,月均成本超支40% 基于此背景,我们设计并实现了基于Python异步爬虫+K8S弹性伸缩的解决方案,

By Ne0inhk

小米钱包签到领积分兑换视频会员(pythone脚本,适用于青龙面板)

一.青龙面板配置环境变量 1.配置通知渠道,企业微信应用或者飞书 2.名称为xmqb,值为抓包的两个值,中间用#号隔开 二.添加脚本 import os import time import requests import urllib3 import json from datetime import datetime from typing import Optional, Dict, Any, Union urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) class Notifier: @staticmethod def send(title: str, content: str): ""

By Ne0inhk
机器学习:数据清洗与预处理 | Python

机器学习:数据清洗与预处理 | Python

个人主页-爱因斯晨 文章专栏-Python学习 文章目录 * 个人主页-爱因斯晨 * 文章专栏-Python学习 * 前言 * 了解数据清洗 * 数据清洗的步骤 * 1. 环境准备与库导入 * 2. 数据加载 * 3. 数据初探与理解 * 4. 缺失值处理 * 5. 重复值处理 * 6. 异常值处理 * 7. 数据类型转换 * 8. 数据标准化 / 归一化(预处理) * 实例实践 * 总结 前言 我们不论在学习机器学习还是数据分析中,都会涉及很多数据。但原数据不可避免有很多杂志,为了确保结果的准确性,我们需要首先进行数据清洗和预处理。 了解数据清洗 数据清洗就像是一场数据的“大扫除”。它是从原始数据中找出并修正那些错误、不完整、重复或不一致的数据。通过数据清洗,能显著提升数据质量,为后续数据分析、挖掘和建模等工作提供准确、可靠、干净的数据基础,从而让基于数据得出的结论更具可信度和价值。 数据清洗的步骤 1. 环境准备与库导入

By Ne0inhk