C++ | 数据结构与算法 | 最小生成树算法讲解 | Kruskal Prim

news2025/1/13 17:29:02

文章目录

    • 前言
    • Kruskal算法
    • Prim算法

前言

讲解之前,我们需要先明白连通图是指什么?连通图具有以一个顶点为起点可以到达该图中的任意一个顶点的特性,就算它们不直接相连,但是它们之间至少有一条可以递达的路径。并且连通图是针对无向图的,对于有向图,有个对应的概念——强连通图。而一张连通图的最小连通子图被称为生成树,因此生成树这个概念是对于无向图的,再细看这个概念,什么叫最小连通子图,这个最小体现在哪?一张连通图的连通子图不唯一,只要一张子图满足任意两顶点间连通的条件,它就是连通图的连通子图,从某种意义上说,连通图和连通子图拥有相同的顶点数量,这是一个默认前提,而最小就体现在:用最少的边数将图中的所有顶点连接,若图有n个顶点,那么最少可以用n-1个顶点连通这些顶点在这里插入图片描述
图片来自百度百科,上面这张图中,左上是一张连通图,该图有6个顶点,其他两张图是它的连通子图,它们用5条边将原图的6个顶点连接,使得任意两顶点间都存在一条路径能够递达,满足了连通的特性,所以它们是原图的连通子图,并且用了最少的边连接所有顶点,所以它们又是原图的最小连通子图,也叫生成树。继续补充生成树的概念:连通图的每一棵生成树都是原图的一个极大无环子图,也就是说生成树只要再加一条边,都将形成一条环路。但是只要任意删除一条边,生成树就不再满足连通的特性

这篇博客讲解的是最小生成树,那么这个最小又体现在哪呢?因为生成树也不唯一,从上面的例子就能看出来,我们可以用相同数量的边以不同的方式连接连通图所有的顶点,它们都是最小连通子图,也就是生成树。生成树的最小体现在边的数量最少,而最小生成树的最小体现在边的权值上,即数量相同的边,连接所有的顶点,但是这些边的权值加起来要最小。总结一下最小生成树的特征:

1,用n-1条边连接原图的n个顶点
2.这n-1条边的权值相加得到的值是所有可能中最小的
3.这n-1条边不能构成回路

Kruskal算法

理解了最小生成树的来由,现在的问题是怎样从不唯一的生成树中,找到一棵最小生成树?当然了,最小生成树也是不唯一的,数量相同的边,权值相加难免也会有相同的时候。

Kruskal算法使用了贪心思想,即每次从原图中挑一条权值最小的边,然后挑一条次小的,直到挑了n-1条边。此时最小生成树也就构建完成,这时就有一个问题,如果这n-1条边不能连接n个顶点怎么办?刚才说过了,对于生成树来说,少一条边就无法满足连接所有边的条件,所以我们要挑出n-1条边,不多不少,其次多一条边还会导致图中构成环路。反着理解,我们只要在挑选边的过程中保证每次添加的边不会使图构成环路,那么最后挑出的n-1条边就不会构成环路,并且保证这些边的权值最小,这不就构成了最小生成树吗?所以现在的问题就变成了要怎么保证每次选择的边不会构成环路?我们可以将连通的顶点放在一个集合中,每次选择边时,如果边的两个顶点都在一个集合中,说明这两个顶点已经连通,再选择一条边连接它们,不就构成了环路吗?所以此时不能选这条边,一开始每个顶点都是一个集合,随着边的添加,顶点就逐渐的汇成一个集合,直到最后一条边的添加,两个集合合并成为了一个集合,也就是说该集合中的顶点都是连通的。对于选择不同集合的顶点的子算法,我们使用并查集结构实现,由于上篇博客已经实现了并查集,这里就不再讲解这个结构,但是因为我实现的并查集是针对泛型的,而这里我们只需要针对整数(用数组下标做映射),使用泛型反而有点复杂,所以我把简化后的并查集贴出来

#pragma once
#include <iostream>
#include <unordered_map>
#include <vector>
#include <utility>
using namespace std;

class UnionFindSet
{
public:
	UnionFindSet(size_t n) 
	{
		_ufs.resize(n, -1);
	}

	size_t find_root(size_t index)   // 查找index下标的根元素,返回其下标
	{
		size_t root = index;
		while (_ufs[root] >= 0)     // 根元素在并查集中的值是负数
		{
			root = _ufs[root];
		}

		while (_ufs[index] >= 0)
		{
			size_t parent = _ufs[index];  // 先保存其双亲,以对其双亲也进行路径压缩
			_ufs[index] = root;           // 路径压缩
			index = parent;
		}

		return root;
	}


	bool set_test(size_t data1, size_t data2)         // 判断两个元素是否在同一集合中
	{
		return find_root(data1) == find_root(data2); // 返回两元素的根元素下标是否相等
	}

	// 连接两个集合
	void set_union(size_t data1, size_t data2)
	{
		// 找到它们的根元素下标
		size_t root1 = find_root(data1);
		size_t root2 = find_root(data2);

		if (root1 != root2) // 不同集合才能合并
		{
			// 假设root1为根元素的集合元素个数更多,如果它的元素更少,交换
			// 注意根元素在并查集中存储的是负数
			if (_ufs[root1] > _ufs[root2])
			{
				swap(root1, root2);
			}

			_ufs[root1] += _ufs[root2]; // 数值的累加,维护集合的个数
			_ufs[root2] = root1;        // 将小集合作为大集合的子集,保存大集合根元素的下标
		}
	}

	size_t set_size() // 返回并查集中树的个数
	{
		// 遍历数组,只有有值小于0就说明它是一个根元素,树的个数加1
		size_t ret = 0;
		for (size_t i = 0; i < _ufs.size(); ++i)
		{
			if (_ufs[i] < 0)
			{
				ret++;
			}
		}
		return ret;
	}
private:
	vector<int> _ufs;				// 保存元素之间关系的数组
};

有了并查集,我们只需要在选择边时,判断边的两个顶点是否处于同一集合中,如果处于同一集合,不选择这条边。如果不处于,选择这条边并将这两个顶点放入同一集合,关于集合的操作我们通过并查集提供的接口实现。

而对于每次的选择,我们要怎么快速的选到最小权值的边呢?由于顶点之间的关系都是用邻接矩阵或者邻接表保存的(具体实现可以看这篇博客),这样的结构只适合用来查询,想要找出其中最小权值的边就会比较复杂,所以这里得额外地再使用一个结构,保存边的信息(起点和终点,权值),用来排序的数组也好,直接使用优先级队列创建小堆也行。这里我就使用优先级队列,排序其实也行,这个看具体使用场景。

struct edge
{
	size_t _srci;
	size_t _deti;
	W _w;

	edge(size_t srci, size_t deti, W w)
		:_srci(srci)
		, _deti(deti)
		, _w(w)
	{}

	bool operator>(const edge& x)
	{
		return _w > x._w;
	}
};

typedef graph<V, W, MAX_W, Direction> self;
const W& kruskal(self& min_tree) // 接收一个graph类型的对象引用,将其修改为最小生成树
{
	// 先初始化优先级队列
	priority_queue<edge, vector<W>, greater<W>> minque;
	// 遍历邻接矩阵,将图的边保存到优先级队列中
	// 但是要注意,由于生成树是无向图,所以只需要遍历一半的矩阵,另一半是重复的
	for (size_t i = 0; i < _matrix.size(); ++i)
	{
		// 注意j < i,控制只遍历一半的矩阵
		for (size_t j = 0; j < i; ++j)
		{
			if (_matrix[i][j] != MAX_W)
			{
				minque.push(edge(i, j, _matrix[i][j]));
			}
		}
	}
}

先给出一部分实现,因为优先级队列需要传仿函数,缺省的仿函数是less,建立的是大堆,其仿函数的实现大概是这样的

class <template T>
struct less
{
	bool operator()(const T& left, const T& right)
	{
		return left < right;
		// 如果是greater类
		// return left > right;
	}
};

可以看到它重载了()操作符,priority_queue使用时会用less类创建一个对象,比如

less compare;

遇到需要比较的情况,比如要比较left和right两个数,就使用compare对象

compare(left, right);

由于compare对象实现了()的重载,所以程序会调用这个重载函数,返回left < right的比较情况,如果这两个操作数是内置类型,int,char,那么left < right这个表达式是成立的,但是两个操作数是自定义类型呢?这个比较计算就无法进行,因此我们需要在自定义类型中对<操作符进行重载,以支持这个比较计算。所以我在edge中实现了operator>以支持edge类型的比较计算。以下是Kruskal的全部实现

struct edge
{
	size_t _srci;
	size_t _deti;
	W _w;

	edge(size_t srci, size_t deti, W w)
		:_srci(srci)
		, _deti(deti)
		, _w(w)
	{}

	bool operator>(const edge& x) const
	{
		return _w > x._w;
	}
};

typedef graph<V, W, MAX_W, Direction> self;
W kruskal(self& min_tree) // 接收一个graph类型的对象引用,将其修改为最小生成树
{
	// 先初始化优先级队列
	priority_queue<edge, vector<edge>, greater<edge>> minque;
	// 遍历邻接矩阵,将图的边保存到优先级队列中
	// 但是要注意,由于生成树是无向图,所以只需要遍历一半的矩阵,另一半是重复的
	for (size_t i = 0; i < _matrix.size(); ++i)
	{
		// 注意j < i,控制只遍历一半的矩阵
		for (size_t j = 0; j < i; ++j)
		{
			if (_matrix[i][j] != MAX_W)
			{
				minque.push(edge(i, j, _matrix[i][j]));
			}
		}
	}

	// 最小生成树的初始化,初始化矩阵,顶点集合与映射表
	min_tree._vertex = _vertex;
	min_tree._index_map = _index_map;
	min_tree._matrix.resize(_vertex.size());
	for (size_t i = 0; i < _vertex.size(); ++i)
	{
		min_tree._matrix[i].resize(_vertex.size(), MAX_W);
	}

	// 并查集的初始化
	UnionFindSet ufs(_vertex.size()); 
	// 返回值创建
	W ret = W();

	// 当并查集中只有一个集合,说明所有的顶点都连接在了一起
	// 此时结束循环,最小生成树构建完成
	while (ufs.set_size() != 1)
	{
		// 取出最小的边
		edge min_edge = minque.top();
		minque.pop();
		// 判断边的顶点是否处于同一集合中
		if (ufs.set_test(min_edge._srci, min_edge._deti) == false)
		{
			// 若不在一个集合中,添加这条边然后将两顶点放入同一集合
			min_tree._add_edge(min_edge._srci, min_edge._deti, min_edge._w);
			ufs.set_union(min_edge._srci, min_edge._deti);
			ret += min_edge._w;
		
			// for test
			cout << _vertex[min_edge._srci] << "->" << _vertex[min_edge._deti] << ':' << _matrix[min_edge._srci][min_edge._deti] << endl;
		}
	}
	return ret;
}

其中使用到了上篇博客模拟实现的图结构,但是有一个接口发生了变化add_edge,该接口接收两个顶点的值,与边的权值,将顶点的值转换成下标后将两点连接,由于Kruskal算法中已经算出了顶点映射的下标,所以这里不用调该接口,我们将其结构进一步封装,得到一个接收顶点下标的接口_add_edge,使程序调用该接口就好了

void _add_edge(size_t srci, size_t deti, const W& w)
{
	// 检查邻接矩阵是否初始化,因为默认构造函数并不会初始化矩阵
	if (_matrix.size() != _vertex.size())
	{
		_matrix.resize(_vertex.size());
		for (size_t i = 0; i < _matrix.size(); ++i)
		{
			// 用最大值初始化矩阵
			_matrix[i].resize(_vertex.size(), MAX_W);
		}
	}

	_matrix[srci][deti] = w;
	// 如果是无向图,镜像也要添加边
	if (Direction == false)
	{
		_matrix[deti][srci] = w;
	}
}

// 边的添加
void add_edge(const V& src, const V& det, const W& w)
{
	// 需要进入邻接表,将顶点转换成下标
	size_t src_index = get_index(src);
	size_t det_index = get_index(det);

	// 顶点存在的判断
	if (src_index == -1 || det_index == -1)
	{
		throw invalid_argument("顶点不存在");
	}

	_add_edge(src_index, det_index, w);
}

在这里插入图片描述
最后是测试程序,先给出一个连通图,我们要找出该连通图的最小生成树
在这里插入图片描述

#include "Graph.hpp"
#include "UnionFindset.hpp"

int main()
{
	matrix::graph<char, int, INT_MAX, false> g;
	g.add_tex('a');
	g.add_tex('b');
	g.add_tex('c');
	g.add_tex('d');
	g.add_tex('e');
	g.add_tex('f');
	g.add_tex('g');
	g.add_tex('h');
	g.add_tex('i');

	g.add_edge('a', 'b', 4);
	g.add_edge('a', 'h', 8);
	g.add_edge('b', 'h', 11);
	g.add_edge('b', 'c', 8);
	g.add_edge('h', 'i', 7);
	g.add_edge('i', 'c', 2);
	g.add_edge('h', 'g', 1);
	g.add_edge('i', 'g', 6);
	g.add_edge('c', 'f', 4);
	g.add_edge('g', 'f', 2);
	g.add_edge('c', 'd', 7);
	g.add_edge('d', 'f', 14);
	g.add_edge('d', 'e', 9);
	g.add_edge('e', 'f', 10);

	matrix::graph<char, int, INT_MAX, false> min_tree;
	size_t ret = g.kruskal(min_tree);
	cout << "最小生成树的权值:" << ret << endl;
	return 0;
}

在这里插入图片描述
(图片来自《算法导论》)我在添加边时将这条边的起点,终点以及权值进行了打印,读者可以通过打印的顺序与图片进行对照,看看两者构建的最小生成树是否相同

可以看到最小生成树不是唯一的,《算法导论》给出的结果选择了a->h这条边,而我们的算法选择了b->c这条边,虽然边不一样,但是权值是相同的,不影响程序最后的结果。经过测试Kruskal算法实现完成

Prim算法

与Kruskal的思想类似,它们都是贪心思想,不同的是两者贪心的范围不同,Kruskal的贪心是从全局入手,每次选择的都是整张图中权值最小的边,而Prim呢?Prim从局部入手,这个局部就体现在Prim需要接收一个顶点值,作为算法的起点,从这个顶点入手,从依附于该顶点的边中选择权值最小的那个。所以说Prim的贪是着眼于当前,从当前拥有的顶点出发,从依附于它们的边中选择权值最小的那个,而Kruskal的贪则是着眼于全局,从所有的未选择的边中选择权值最小的那个,这就是两个算法的本质区别。

因为Prim是从依附于当前顶点的边中选择权值最小的那个,那么Prim所拥有的顶点与依附于该顶点的边就需要被记录,这里可以使用一个bool数组记录已经拥有的顶点,可以用优先级队列存储依附于当前顶点的边。一开始,以调用者传入的顶点为起点,将顶点值转换成数组下标,修改bool数组中该下标的bool值为true,表示当前连接了这个顶点,然后遍历邻接矩阵(邻接表)找出依附于该顶点的边并放入优先级队列中,与Kruskal一样,优先级队列是一个小堆,此时Prim的初始化工作完成。要注意的是:虽然无向图的边没有方向,但是我们可以人为的指定方向(注意这只是为了方便算法的实现),以拥有的顶点为起点,向外的顶点(未拥有的顶点)为终点。

初始化完成后,我们选择堆顶的边(局部权值最小),添加到最小生成树上,但是要注意保证权值最小的同时,这条边的终点不能是我们已经拥有的顶点,否则将构成环路。边的起点是我们已经拥有的顶点,边的终点是我们未拥有的顶点,在这个前提下保证边的权值是最小的。就这样我们从一个顶点开始一步步的向外连接其他顶点,直到所有顶点被连接,此时的边的数量为n-1,根据这个条件我们结束循环,最小生成树的构建也随之完成了

W Prim(self& min_tree, const V& start)
{
	// 先将顶点值转换为下标
	size_t starti = get_index(start);
	if (starti == -1)
	{
		throw invalid_argument("顶点不存在");
	}

	// 最小生成树的初始化,初始化矩阵,顶点集合与映射表
	min_tree._vertex = _vertex;
	min_tree._index_map = _index_map;
	min_tree._matrix.resize(_vertex.size());
	for (size_t i = 0; i < _vertex.size(); ++i)
	{
		min_tree._matrix[i].resize(_vertex.size(), MAX_W);
	}

	// bool数组的构建与初始化
	size_t vertex_size = _vertex.size();
	vector<bool> vertex_had(vertex_size, false);
	// 起点的标记
	vertex_had[starti] = true;

	// 创建优先级队列,存储当前可以选择的边
	priority_queue<edge, vector<edge>, greater<edge>> min_que;
	// 优先级队列初始化
	for (size_t i = 0; i < vertex_size; ++i)
	{
		// 将依附于顶点的边存储到优先级队列
		if (_matrix[starti][i] != MAX_W)
		{
			min_que.push(edge(starti, i, _matrix[starti][i]));
		}
	}
	// 至此初始化完成,要开始循环以构建最小生成树了
	
	// 一些关键变量的创建
	size_t finish = 0;
	W ret = W();
	// 当还未连接所有顶点时,重复循环
	while (finish != vertex_size - 1)
	{
		// 拿到最小权值边
		edge min_edge = min_que.top();
		min_que.pop();
		// 判断可以选择的最小权值边是否构成了环路
		if (!vertex_had[min_edge._deti])
		{
			// 未构成环路,为最小生成树添加这条边
			min_tree._add_edge(min_edge._srci, min_edge._deti, min_edge._w);
			vertex_had[min_edge._deti] = true;
			// 循环变量的维护
			finish++;
			// 返回值的维护,权值的记录
			ret += min_edge._w;

			// for test
			cout << _vertex[min_edge._srci] << "->" << _vertex[min_edge._deti] << ':' << _matrix[min_edge._srci][min_edge._deti] << endl;
		}
		// 若构成环路,直接进行下次最小权值边的选择
		else
		{
			continue;
		}
		// 添加依附于新拥有顶点的边
		// 注意添加的边的起点是新拥有的顶点,终点是未拥有的顶点
		for (size_t i = 0; i < vertex_size; ++i)
		{
			if (_matrix[min_edge._deti][i] != MAX_W && !vertex_had[i])
			{
				min_que.push(edge(min_edge._deti, i, _matrix[min_edge._deti][i]));
			}
		}
	} // end of while
	return ret;
}

以上是Prim算法的实现,由于之前已经讲解过了大致的逻辑,并且代码也有详细的注释,这里就不再赘述其中的细节,直接进行程序的测试

#include "Graph.hpp"
#include "UnionFindset.hpp"

int main()
{
	matrix::graph<char, int, INT_MAX, false> g;
	g.add_tex('a');
	g.add_tex('b');
	g.add_tex('c');
	g.add_tex('d');
	g.add_tex('e');
	g.add_tex('f');
	g.add_tex('g');
	g.add_tex('h');
	g.add_tex('i');

	g.add_edge('a', 'b', 4);
	g.add_edge('a', 'h', 8);
	g.add_edge('b', 'h', 11);
	g.add_edge('b', 'c', 8);
	g.add_edge('h', 'i', 7);
	g.add_edge('i', 'c', 2);
	g.add_edge('h', 'g', 1);
	g.add_edge('i', 'g', 6);
	g.add_edge('c', 'f', 4);
	g.add_edge('g', 'f', 2);
	g.add_edge('c', 'd', 7);
	g.add_edge('d', 'f', 14);
	g.add_edge('d', 'e', 9);
	g.add_edge('e', 'f', 10);

	matrix::graph<char, int, INT_MAX, false> min_tree;
	size_t ret = g.Prim(min_tree, 'a');
	cout << "最小生成树的权值:" << ret << endl;
		
	return 0;
}

在这里插入图片描述

与Kruskal算法举的例子一样,这次我们用Prim算法求其最小生成树,读者可以通过打印的顺序体会Prim算法的实现过程。


至此最小生成树的两种算法讲解完成,虽然实现过程略有些复杂,但是两种的指导思想——贪心,却很好理解,所以在复杂算法的实现之前,我们需要考虑清楚算法的大概实现逻辑,将贪心的思想具体化,最后才是写代码完成细节的实现

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/187908.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

劳务派遣协议范本整理版模板范本

劳务派遣协议范本整理版 甲方&#xff08;用工单位&#xff09;&#xff1a;______ 法定代表人&#xff1a;____________ 地址&#xff1a;__________________ 电话&#xff1a;__________________ 传真&#xff1a;__________________ 乙方&#xff08;派遣单位&#xf…

【Java】一文彻底弄懂访问修饰符(public/protected/默认/private)--建议收藏

博主简介&#xff1a;努力学习的预备程序媛一枚~博主主页&#xff1a; 是瑶瑶子啦所属专栏: Java岛冒险记【从小白到大佬之路】 前言 OOP(Object Oriented Programing)&#xff0c;即面向对象编程&#xff0c;最重要的功能/特点之一就是封装&#xff0c;这点在该专栏开篇博客【…

WebPack面试题汇总

1&#xff0c;Webpack有什么作用&#xff0c;谈谈你对它的理解 现在的前端网页功能丰富&#xff0c;特别是SPA&#xff08;single page web application 单页应用&#xff09;技术流行后&#xff0c;JavaScript的复杂度增加和需要一大堆依赖包&#xff0c;还需要解决Scss、Les…

记录--这样封装列表 hooks,一天可以开发 20 个页面

这里给大家分享我在网上总结出来的一些知识&#xff0c;希望对大家有所帮助 这样封装列表 hooks,一天可以开发 20 个页面 前言 在做移动端的需求时&#xff0c;我们经常会开发一些列表页&#xff0c;这些列表页大多数有着相似的功能&#xff1a;分页获取列表、上拉加载、下拉刷…

【Java 面试题合集】ThreadPoolExecutor 线程池面试题

文章目录自定义的线程池的 7 个参数如何合理设置核心线程数 corePoolSize 的大小《JAVA 并发编程实战》中的方案java 开发手册中为什么不允许使用 Executors 默认的实现&#xff1f;一个线程池中的线程异常了&#xff0c;那么线程池会怎么处理这个线程?线程池被创建后里面有线…

OpenCV——总结《图像处理-1》

1.HSV H - 色调&#xff08;主波长&#xff09;。S - 饱和度&#xff08;纯度/颜色的阴影&#xff09;。V值&#xff08;强度&#xff09; hsvcv2.cvtColor(img,cv2.COLOR_BGR2HSV)2.图像阈值 函数介绍&#xff1a; ret, dst cv2.threshold(src, thresh, maxval, type) sr…

嵌入式Linux从入门到精通之第九节:系统编程

系统编程概述 在讲解系统编程之前,先了解几个概念: 操作系统的作用: 操作系统用来管理所有的资源,并将不同的设备和不同的程序关联起来。 什么是Linux系统编程? 在有操作系统的环境下编程,并使用操作系统提供的系统调用及各种库,对系统资源进行访问。 学会了C语言再知…

Grafana9.3.x在windows上的安装及使用

Grafana9.3.x的安装及使用1. Grafana install1.1 Download1.2 Install2. User Guide1.1 Document1.2 Table视图背景色渲染3.Awakening1. Grafana install 1.1 Download 下载地址 Grafana Website: https://grafana.com/. 1.2 Install 直接点击安装就好了 进入conf目录复制一…

python集合语法与应用

python集合语法与应用 文章目录python集合语法与应用一、实验目的二、实验原理三、实验环境四、实验内容五、实验步骤1.创建2.增加3.删除4.集合运算5.拓展知识一6.拓展知识二总结一、实验目的 掌握集合的用法 二、实验原理 集合中只能包含数字、字符串、元组等不可变的类型的…

规则引擎,实现业务低代码开发的重要工具

规则引擎&#xff0c;是将业务执行抽象化的配置&#xff0c;通过其定义的数据结构、算法和流程来实现应用程序功能的普适化。 规则引擎可以帮助企业提高业务开发效率&#xff0c;提高运营的灵活性&#xff0c;降低运营成本与开发成本&#xff0c;让系统更加智能化灵活化。这里以…

【Rust】5. 所有权

5. 所有权 5.1 什么是所有权 5.1.1 栈&#xff08;Stack&#xff09;与堆&#xff08;Heap&#xff09; 5.1.2 所有权规则 5.1.3 变量作用域 5.1.4 String 类型 String 类型可进行修改&#xff0c;而字符串字面值是不可以的&#xff01;&#xff08;区别在于二者对内存的处理…

OAuth2

目录一、什么是OAuth2.0二、OAuth2中的角色三、认证流程四、生活中的Oauth2思维5. 令牌的特点6.OAuth2授权方式6.1 授权码6.2 隐藏方式6.3 密码方式6.4 凭证方式一、什么是OAuth2.0 OAuth2.0是目前使用非常广泛的授权机制&#xff0c;用于授权第三方应用获取用户的数据。 举例…

PythonWeb Django框架学习笔记

文章目录Django一、初步了解Django1.1 创建项目1.2 文件介绍1.3 APP的创建和说明添加新的app注册app创建页面1.4 templates模板templates语法单一变量列表循环【列表】字典循环【字典】列表套字典条件判断templates小结1.5 请求和响应案例&#xff1a;用户管理二、数据库操作2.…

前言技术之mybatis-plus 01

目录 1.什么是mybatis-plus 2.初体验 3.日志 4.主键生成策略 5.更新 6.自动填充 1.什么是mybatis-plus 升级版的mybatis&#xff0c;目的是让mybatis更易于使用&#xff0c; 用官方的话说“为简化而生” 官网&#xff1a; MyBatis-Plus 2.初体验 1.准备数据库脚本 数据…

再获殊荣!天云数据入选第一批北京市级企业技术中心,Hubble数据库提供新一代信息技术科技服务

为助力北京国际科技创新中心建设&#xff0c;贯彻落实北京市“十四五”时期高精尖产业发展规划&#xff0c;引导和支持企业加强创新能力&#xff0c;培育和引导企业技术中心建设&#xff0c;北京市经济和信息化局组织开展了2022年度第一批北京市市级企业技术中心的创建工作&…

【Python学习笔记】5. Python3 基本数据类型(上)——数值型、字符串型

前言 Python 中的变量不需要声明。每个变量在使用前都必须赋值&#xff0c;变量赋值以后该变量才会被创建。 Python3 基本数据类型 Python 中的变量不需要声明。每个变量在使用前都必须赋值&#xff0c;变量赋值以后该变量才会被创建。 在 Python 中&#xff0c;变量就是变…

tomcat配置多个host,并且避免重复加载

目录 1.实验描述 2.实验环境 2.实验过程 2.1 创建order和user项目 2.2 打包项目 2.3 修改tomcat的server.xml的配置 2.4 启动tomcat 2.5 配置本地host 2.6 验证 1.实验描述 目前有两个域名&#xff0c;分别是&#xff1a; order.abc.com user.abc.com 两个项目&…

微信小程序学习第1天:微信小程序开发入门介绍

前言&#xff1a;微信小程序开发模式 1、申请小程序开发账号 2、安装小程序开发者工具 3、创建和配置小程序项目 一、申请小程序开发账号 1、体验小程序 2、注册小程序开发账号 使用浏览器打开https://mp.weixin.qq.com网址&#xff0c;按照提示注册 注册承购后&#xff0c…

微服务 初始 分布式搜索引擎 Elastic Search

文章目录⛄引言一、什么是Elastic Search&#xff1f;二、Elastic Search 倒排索引⛅正向索引⚡倒排索引⛄正向和倒排三、ES的一些概念⛅文档和字段⚡索引和映射四、MySQL 与 Elasticsearch⛵小结⛄引言 本文参考黑马 分布式Elastic search Elasticsearch是一款非常强大的开源…

OBB的计算python实现

OBB的计算python实现1. 实现步骤步骤① 分解点集的xyz分量步骤② 对x、y、z这三个随机变量&#xff08;一维数组&#xff09;求协方差矩阵步骤③ 对步骤②中的协方差矩阵求解特征值与特征向量&#xff0c;特征向量构造列向量矩阵M步骤④ 将点集的几何中心平移至坐标系原点&…