C++ 不知图系列之基于链接表的无向图最短路径搜索

news2025/1/17 21:38:57

1. 前言

图的常用存储方式有 2 种:

  • 邻接炬阵。

  • 链接表。

邻接炬阵的优点和缺点都很明显。优点是简单、易理解,但是对于大部分图结构而言,都是稀疏的,使用矩阵存储,空间浪费就较大。

链接表相比较邻接矩阵存储方案,使用起来更方便,对于空间的使用是刚好够用原则,不会产生太多空间浪费。理解起来可能会有点难度。

本文将以链接表方式存储图结构,在此基础上实现无向无权图最短路径搜索。

2. 链接表

2.1 存储思路

使用链接表实现图的存储时,有主表子表概念。

  • 主表: 用来存储图对象中的所有顶点数据。
  • 子表: 每一个顶点自身会维护一个子表,用来存储与其相邻的所有顶点数据。

如下图结构中有 5 个顶点,使用链接表保存时,需要主表 1 张,子表 5 张。链接表的优点是能够紧凑地表示稀疏图。

图1.png

邻接表01.png

2.2 图的存储实现

2.2.1 项点类

因顶点本身是具有特定的数据含义(如,可能是城市、公交车站、网址、路由器……),需要一个顶点类承载顶点的有效数据。并在顶点类中提供维护自身信息的函数。

#include <iostream>
#include <queue>
#include <stack>
#include <vector>
using namespace std;
template<typename T>
struct Vertex {
	// 顶点的编号
	int vid;
	// 顶点有效负荷
	T value;
	// 是否被访问过:False 没有 True:有
	bool visited ;
    
    //相邻顶点类型
	template<typename T1>
	struct NeighborVertex {
		Vertex<T1> *vertex;
		//和相邻顶点的权重
		int weigth;
		//兄弟顶点
		NeighborVertex<T1> *next;
		NeighborVertex(	Vertex<T1> *ver,int weigth) {
			this->vertex=ver;
			this->weigth=weigth;
			this->next=NULL;
		}
	};
	// 相邻顶点链表
	NeighborVertex<T> *  head;
	//搜索时的前驱结点
	Vertex* preVertex;
    //无参构造
	Vertex() {
		this->vid=0;
		this->visited=false;
		this->head=NULL;
	}
    //有参构造
	Vertex(int vid,T value) {
		this->vid=vid;
		this->visited=false;
		this->head=NULL;
		this->value=value;
	}
	/*
	*在链表中使用头部插入方案添加邻接顶点
	*nbrVer:相邻顶点
	*weight:无向无权重图,权重默认设置为 1
	*/
	void addNeighbor(Vertex<T> *nbrVer,int weigth) {
		NeighborVertex<T> * newNbrVer=new NeighborVertex<T>(nbrVer,weigth);
		//头部添加
		if(this->head==NULL) {
			this ->head=newNbrVer;
		} else {
			newNbrVer->next=this->head;
			this->head=newNbrVer;
		}
	}
	
    /*
	*   显示与当前顶点相邻的顶点
	*/
	vector<Vertex<T> *> showAllNeighbor() {
		NeighborVertex<T> *move=this->head;
		vector<Vertex<T> *> allNeighbor;
		while(move!=NULL) {
			allNeighbor.push_back(move->vertex);
			move->vertex->desc();
			cout<<"_权重"<<move->weigth<<"\t";
			move=move->next;
		}
		return allNeighbor;
	}
	/*
	*判断与某顶点是否相邻,并返回权重  0 表示与此顶点不相邻
	*/
	int  isNeighbor(NeighborVertex<T> *ver) {
		NeighborVertex<T> *move=this->head;
		while(move!=NULL && move->vertex->value!=ver->value) {
			move=move->next;
		}
		if (move==NULL)
			return 0;
		else
			return move->weigth;
	}

	//顶点的自我显示
	void desc() {
		cout<<"顶点["<<this->vid<<"_"<<this->value<<"]";
	}
};

顶点类结构需要说明地方:

  • visited:用于搜索路径算法中,检查节点是否已经被搜索过。
  • head:存储与顶点相邻的顶点信息,也称为相邻顶点,相邻顶点需要包括 2 方面信息,一是顶点Vertex信息,二是权重。这里提供了NeighborVertex类型,在Vertex类型的基础之上封装了权重。

父子表.png

2.2.2 图类

图类用于维护l图中的所有顶点以及顶点之间的关系,以及针对于图的相关算法。

template<typename T>
class Graph {
	private:
		// 一维数组,存储节点
		Vertex<T>** vertexs;
		// 顶点编号,从 0 开始
		int vnums;
		//一维数组大小
		int size;
	public:
		Graph(int size) {
			//初始一给数组大小
			this->vertexs=new Vertex<T>*[size];
			this->vnums=0;
			this->size=size;
		}
		//添加新顶点
		Vertex<T> * addVertex(T value);
		//为顶点添加相邻顶点
		void addNeighbor(T parentValue,T nbrValue,int weight);
		//按值查找顶点
		Vertex<T> * findVertexByValue(T value);
		//按编号查找顶点
		Vertex<T> * findVertexById(int id);
		//显示所有顶点
		void showAllVers();
};
查找函数:

查找算法有 2 种方案:

  • 按值查找。
/*
*按值查找顶点是否存在
*/
template<typename T>
Vertex<T> * Graph<T>::findVertexByValue(T value) {
	for(int i=0; i<Graph<T>::vnums; i++) {
		if(Graph<T>::vertexs[i]==NULL)
			continue;
		if(Graph<T>::vertexs[i]->value==value) {
			return Graph<T>::vertexs[i];
		}
	}
	return NULL;
}
  • 按编号查找。
/*
*按编号查找顶点是否存在
*/
template<typename T>
Vertex<T> * Graph<T>::findVertexById(int id) {
	for(int i=0; i<Graph<T>::vnums; i++) {
		if(Graph::vertexs[i]->vid==id) {
			return Graph<T>::vertexs[i];
		}
	}
	return NULL;
}

添加新顶点函数: 先查找是否存在顶点,没有就添加。

顶点的编号由图对象内部指定,便于统一管理。

/*
*添加新的顶点
*/
template<typename T>
Vertex<T> * Graph<T>::addVertex(T value) {
	//检查此顶点是否存在
	Vertex<T> * ver= Graph<T>::findVertexByValue(value);
	if(ver==NULL) {
		//创建新的顶点
		ver=new  Vertex<T>(Graph<T>::vnums,value);
		Graph::vertexs[Graph<T>::vnums]=ver;
		Graph<T>::vnums++;
	}
	return ver;
}

添加顶点之间的关系:

//为顶点添加相邻顶点
template<typename T>
void Graph<T>::addNeighbor(T parentValue,T nbrValue,int weight) {
	Vertex<T> * parentVer= Graph<T>::findVertexByValue(parentValue);
	if(parentVer==NULL)
		parentVer=Graph<T>::addVertex(parentValue);
	Vertex<T> * nbrVer= Graph<T>::findVertexByValue(nbrValue);
	if(nbrVer==NULL)
		nbrVer=Graph<T>::addVertex(nbrValue);
	//调用顶点的函数
	parentVer->addNeighbor(nbrVer,weight);
}

显示所有顶点:

template<typename T>
void Graph<T>::showAllVers() {
	for(int i=0; i<Graph<T>::vnums; i++) {
		Graph::vertexs[i]->desc();
		cout<<endl<<"\t相邻顶点:";
		//输出相邻顶点
		Graph::vertexs[i]->showAllNeighbor();
		cout<<endl;
	}
}

测试图中的函数:

存储如下图结构中顶点以及顶点之间的信息:

图1.png

int main(int argc, char** argv) {
	//实例化图
	Graph<char> *graph=new Graph<char>(10);
	//添加几个顶点 A,B C D E
	char vers[5]= {'A','B','C','D','E'};
	for(int i=0; i<5; i++) {
		graph->addVertex(vers[i]);
	}
	//添加顶点之间的关系(A -(3)-》B )
	graph->addNeighbor('A','B',3);
	//添加顶点之间的关系(A -(5)-》D )
	graph->addNeighbor('A','D',5);
	//添加顶点之间的关系(B -(4)-》c )
	graph->addNeighbor('B','C',4);
	//添加顶点之间的关系(C -(6)-》D)
	graph->addNeighbor('C','D',6);
	//添加顶点之间的关系(C -(1)-》E)
	graph->addNeighbor('C','E',1);
	//添加顶点之间的关系(D -(2)-》E)
	graph->addNeighbor('D','E',2);
	//添加顶点之间的关系(E -(7)-》B)
	graph->addNeighbor('E','B',7);
	//输出所的顶点
	graph->showAllVers();
	return 0;
}

输出结果:

res.png

3. 最短路径算法

从图结构可知,从一个顶点到达另一个顶点,不止一条可行路径,在众多路径我们总是试图选择一条最短路径。当然,需求不同,衡量一个路径是不是最短路径的标准也会不同。

如打开导航系统后,最短路径可能是费用最少的那条、可能是速度最快的那条、也可能是量程数最少的或者是红绿灯最少的……

无权无向图中,以经过的边数最少的路径为最短路径。在无权无向图中找到最短路径相对简单。

在有向加权图中,会以附加在每条边上的权重的数据含义来衡量。权重可以是时间、速度、量程数……

3.1 算法思路

查找无向图中任意两个顶点间的最短路径长度,可以直接使用广度搜索算法。如下图求解 A0 ~ F5 的最短路径。

Tips: 无向图中任意 2 个顶点间的最短路径长度由边数决定。

无向无权重图.png

广度优先搜索算法流程:

广度优先搜索算法的基本原则:以某一顶点为参考点,先搜索离此顶点最近的顶点,再搜索离最近顶点的最近顶点……以此类推,一层一层向目标顶点推进。

如从顶点 A0 找到顶点 F5。先从离 A0 最近的顶点 B1D3 找起,如果没找到,再找离 B1D3 最近的顶点 C2E4,如果还是没有找到,再找离 C2E4 最近的顶点 F5

Tips: 因为每一次搜索都是采用最近原则,最后搜索到的目标也一定是最近的路径。

也因为采用最近原则,在搜索过程中所经历到的每一个顶点的路径都是最短路径。最近+最近,结果必然还是最近

无向无权重图00.png

显然,广度优先搜索的最近搜索原则是符合先进先出思想的,具体算法实施时可以借助队列实现整个过程。

算法流程:

  • 先确定起始点 A0

  • 找到 A02 个后序顶点 B1D3 (或者说 B1、D3的前序顶点是 A0),压入队列中。除去起点 A0B1D3 顶点属于第一近压入队列的节点。

    B1D3 压入队列的顺序并不影响 A0 ~B1A0 ~ D3 的路径距离(都是 1)。

    A0~B1 的最短路径长度为 1。

    A0~D3 的最短路径长度为 1。

  • 从队列中搜索 B1 时,找到 B1 的后序顶点 C2 并压入队列。B1C2 的前序顶点。

    B1 ~ C2 的最短路径长度为 1,而又因为 A0~B1 的最短路径长度为 1 ,所以 A0 ~ C2 的最短路径为 2

  • B1 搜索完毕后,在队列中搜索 B3 时,找到 B3 的后序顶点 E4 ,压入队列。因 B1D3 属于第一近顶点,所以这 2 个顶点的后序顶点 C2E4 属于第二近压入队列,或说 A0-B1-C2A0-D3-E4 的路径距离是相同的(都为 2)。

  • 当搜索到 C2 时,此时队列没有压入操作。

  • 当 搜索到 E4 时,E42 个相邻顶点 C2F5,因 C2 已经压入过,所以仅压入 F5。因 F5 是由第二近顶点压入,所以 F5 是属于第三近压入顶点。

    A0-D3-E4-F5 的路径为 3。

无向无权重图01.png

3.2 编码实现

在图类添加广度搜索函数:

在图类添加如下函数:使用广度优先搜索算法查找顶点与顶点之间的路径。

广度优先搜索算法有一个核心点,当搜索到某一个顶点后,需要找到与此顶点相邻的其它顶点,并压入队列中。pushQueue 方法就是做这件事情的。如果某一个顶点曾经进过队列,就不要再重复压入队列了。

 template<typename T>
class Graph {
	private:
         //省略……
         //保存所有使用广度算法搜索到的路径
		vector<vector<Vertex<T> *> >  allPaths;
	public:
         //省略……
		//把某一顶点的相邻顶点压入队列
		void pushQueue(queue<Vertex<T> *> & myQueue,Vertex<T> * vertex);
		//广度搜索路径
		void bfsNearestPath(T from,T to);
		//输出广度搜索到的所有路径
		void showAllPaths();
	};

pushQueue的函数实现:

//把某一顶点的相邻顶点压入队列
template<typename T>
void  Graph<T>::pushQueue(queue<Vertex<T> *> & myQueue,Vertex<T> *  vertex) {
	//查找  vertex 的相邻顶点
	cout<<vertex->value<<"相邻顶点:"<<endl;
	vector<Vertex<T> *> allVers=vertex->showAllNeighbor();	
	for(int i=0; i<allVers.size(); i++) {
		if(allVers[i]->visited==false) {
			//设置前驱顶点
			allVers[i]->preVertex=vertex;
			//压入队列中
			myQueue.push(allVers[i]);
			//设置为已经压入
			allVers[i]->visited=true;
		}
	}
	cout<<endl;
}

bfsNearestPath广度搜索算法实现:

//广度搜索路径
template<typename T>
void Graph<T>::bfsNearestPath(T from,T to) {
	//队列:广度搜索要使用队列
	queue<Vertex<T> *>  myQueue;
	//临时路径
	vector<Vertex<T> *> tmpPath;
	//检查顶点是否存在
	Vertex<T> * fromVer= Graph<T>::findVertexByValue(from);
	Vertex<T> * toVer= Graph<T>::findVertexByValue(to);
	if(fromVer==NULL || toVer==NULL)
		return;
	tmpPath.push_back(fromVer);
	//把 fromVer 顶点的相邻顶点压入队列中,fromVer 本身可以不用压入
	Graph<T>::pushQueue(myQueue,fromVer);

	while(!myQueue.empty()) {
		//出队列
		Vertex<T> * ver=  myQueue.front();
		myQueue.pop();
		if(ver->preVertex==fromVer) {
			//如果前驱是 fromVer
			tmpPath.push_back(ver);
			//添加新路径
			Graph<T>::allPaths.push_back(tmpPath);
			//把临时路径的最后一个顶点删除
			tmpPath.pop_back();
		} else {
			//扫描所有路径
			for(int i=0; i<Graph<T>::allPaths.size(); i++) {
				tmpPath = Graph<T>::allPaths[i];
				//得到路径中的最后一个顶点
				Vertex<T> * tmpVer =tmpPath.back();
				if (ver->preVertex==tmpVer) {
					tmpPath.push_back(ver);
					//allPaths[i]=tmpPath;
					Graph<T>::allPaths.push_back(tmpPath);
				}
			}
		}
		if(ver->value==toVer->value) {
			break;
		} else {
			Graph<T>::pushQueue(myQueue,ver);
		}
	}
}

显示顶点之间的路径信息:

/*
*显示搜索到的路径
*/
template<typename T>
void Graph<T>::showAllPaths() {
	for(int i=0; i<Graph<T>::allPaths.size(); i++) {
		vector<Vertex<T> *> vec=Graph<T>::allPaths[i];
		int pathWeight=0;
		for(int j=0; j<vec.size(); j++) {
			pathWeight++;
			vec[j]->desc();
			cout<<"-";
		}
		cout<<"路径长度:"<<pathWeight-1<<endl;
	}
}

测试代码: 因为测试的是无向无权重图,顶点之间的权重默认为 1

nt main(int argc, char** argv) {
	//实例化图
	Graph<char> *graph=new Graph<char>(10);
	//添加几个顶点 A,B C D E
	char vers[6]= {'A','B','C','D','E','F'};
	for(int i=0; i<6; i++) {
		graph->addVertex(vers[i]);
	}

	//添加顶点之间的关系(A -(3)-》B )
	graph->addNeighbor('A','B',1);
	//添加顶点之间的关系(A -(5)-》D )
	graph->addNeighbor('A','D',1);
	//添加顶点之间的关系(B -(4)-》c )
	graph->addNeighbor('B','C',1);
	//添加顶点之间的关系(C -(6)-》D)
	graph->addNeighbor('D','E',1);
	//添加顶点之间的关系(C -(1)-》E)
	graph->addNeighbor('C','E',1);
	//添加顶点之间的关系(D -(2)-》E)
	graph->addNeighbor('E','F',1);
	//输出所的顶点
	graph->showAllVers();
	//广度搜索A 到 F 的路径,中间会经过其它顶点
	graph->bfsNearestPath('A','F');
	cout<<"\n路径信息:"<<endl;
	graph->showAllPaths();
	return 0;
}

输出结果:

paths.png

无向无权重图中,查找起始点到目标点的最短路径,使用广度优先搜索算法便可实现。

但如果是有向加权图,可能不会称心如愿。因有向加权图中的边是有权重的。故对于有向加权图则需要另择方案。

4. 总结

本文讲解了如何使用链表存储图,以及使用广度搜索算法实现无向无权重图中顶点之间的路径搜索。

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

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

相关文章

团队的Code Review实践

高效地进行 Code Review 一直是我们想要做的事情&#xff0c;如何持续保持高效 Review 也是我们日常开发中所亟需解决的问题。 在疫情爆发之前&#xff0c;团队大多是线下一起办公。大家会聚在一起 Code Review&#xff0c;把讨论出的反馈记录在便利贴并贴在白板上。而当远程、…

Baklib|如何为你的营销计划制作Wiki页面

当你需要快速查找信息时&#xff0c;你会怎么做?很有可能&#xff0c;你会直接去谷歌——在输入你的查询之后&#xff0c;十有八九&#xff0c;搜索引擎会带你去wiki百科。wiki百科是一个巨大的在线百科全书。在这个数据库中&#xff0c;几乎所有的文章都有链接。 现在想象一…

Maven之POM介绍

POM介绍前言POM基础为什么要学习POM什么是POMSuper POMMinimal POM&#xff08;POM的最低配置&#xff09;POM特点Project Inheritance(项目继承性&#xff09;Project Aggregation(项目聚合&#xff09;Project Inheritance VS Project AggregationProject Interpolation and …

《FFmpeg Basics》中文版-00-简介

欢迎 亲爱的读者们&#xff0c; 欢迎来到这本书&#xff0c;它将使您熟悉FFmpeg项目的许多有趣的特性。下面的几个大公司都是FFmpeg使用者: Facebook&#xff0c;最大的社交网络&#xff0c;用FFmpeg技术处理用户的视频。Google Chrome&#xff0c;流行的web浏览器&#xff0…

EDI通信中常用的网络排查方法

在知行之桥EDI系统运维工作中不可避免会碰到各种网络问题&#xff0c;本文将分享一些我们运维同事常用的一些网络问题排查定位方法&#xff0c;帮助大家快速的定位原因&#xff0c;解决通信问题。 1.Ping&#xff1a; 作用&#xff1a;ping用于确定本地主机是否能与另一台主机…

shell脚本下用plot给iostat和CPU的采样画图的写法

目前大多的互联网客户,在导入SSD之前&#xff0c;基本会要求OEM或者SSD厂商提供一些性能数据图&#xff0c;所以 作图也基本成了测试人员的必备技能&#xff0c;单盘的性能可以用Excel表格导入&#xff0c;但是复杂的可能会比较麻烦。就需要我们借助工具来作图了。 本篇文章简单…

第9章 Apache-Dbutils实现CRUD操作

1. Apache-DBUtils简介 *commons-dbutils 是 Apache 组织提供的一个开源 JDBC工具类库&#xff0c;它是对JDBC的简单封装&#xff0c;学习成本极低&#xff0c;并且使用dbutils能极大简化jdbc编码的工作量&#xff0c;同时也不会影响程序的性能。 *API介绍&#xff1a; org.…

java和vue的大学生奖学金助学金系统奖学金系统助学金系统

简介 大学生奖学金助学金系统。学生申请自己需要的奖助学金&#xff0c;上传证明材料。该学院的辅导员可以下载学生的证明材料以及根据学生的综合成绩来审核是否通过&#xff0c;若不通过请输入不通过原因。管理员可以导入学生excel和辅导员excel以及学生综合成绩excel、分布公…

鲲鹏代码迁移工具介绍

鲲鹏代码迁移工具介绍 代码迁移工具介绍 代码迁移工具是什么&#xff1f; ✨我们为什么会需要用到代码迁移工具&#xff1f; ✨处理器所支持的指令集不同&#xff0c;意味着开发者可能需要对代码进行跨平台的迁移。 这里我们常见到便是x86平台的代码往arm平台进行迁移 通常…

主流组件库学习

主流组件库差异性对比 NutUI - 京东研发的移动端 UI 组件库&#xff0c;支持 Vue3、Taro 多端适配&#xff0c;面向电商业务场景Vant - 有赞研发的移动端 UI 组件库&#xff0c;支持 Vue3、微信小程序、支付宝小程序TDesign Mobile - 腾讯研发的移动端组件库&#xff0c;适合在…

Redis第二章_实战篇_短信登录+缓存策略+秒杀+分布式锁>>

Redis第二章_实战篇_短信登录缓存策略秒杀分布式锁>> 文章目录Redis第二章_实战篇_短信登录缓存策略秒杀分布式锁>>开篇导读1、短信登录1.1、导入黑马点评项目1.1.1 、导入SQL1.1.2、有关当前模型1.1.3、导入后端项目1.1.4、导入前端工程1.1.5 运行前端项目1.2 、基…

Spring(十一)- Spring Bean的依赖注入注解

文章目录一、Spring Bean的依赖注入注解1. 通过Value直接注入普通属性2. 通过Value注入properties文件中的属性3. Autowired注解&#xff0c;用于根据类型进行注入4. Qualifier配合Autowired可以完成根据名称注入Bean实例&#xff0c;使用Qualifier指定名称5. Resource注解既可…

三款Zookeeper可视化工具、ZooInspector、prettyZoo、ZooKeeperAssistant

三款Zookeeper可视化工具、ZooInspector、prettyZoo、ZooKeeperAssistant①Zookeeper图形化工具&#xff1a;ZooInspector1.下载完后&#xff0c;解压压缩包&#xff0c;进入zookeeper-dev-ZooInspector.jar目录后执行run jar包命令2.登录客户端&#xff1a;输入zk服务的ip和端…

XCTF1-web disabled_button weak_auth view_source cookie backup

一个不能按的按钮 题目描述 X老师今天上课讲了前端知识&#xff0c;然后给了大家一个不能按的按钮&#xff0c;小宁惊奇地发现这个按钮按不下去&#xff0c;到底怎么才能按下去呢&#xff1f; 进入场景 题目提示为前端设置的问题&#xff0c;查看网页源码&#xff0c;定位…

Django + Nginx https部署实战(第二辑)

如何使用Nginx把网站升级为Https&#xff1f; Https是Http协议的升级版&#xff0c;由于证书的引入&#xff0c;使得用户与网站之间的通讯变得更加安全。 在使用https之前&#xff0c;我们必须了解的事情 证书颁发机构 证书颁发机构是一家知名且受信任的组织&#xff0c;它对网…

基于MMDetection训练VOC格式数据集

一 环境说明 基于前述安装MMDetection&#xff0c;数据集为VOC格式&#xff0c;主要版本如下&#xff1a; Python&#xff1a;3.7.8 CUDA&#xff1a;11.3 cuDNN&#xff1a;8.4.0 torch&#xff1a;1.12.0 torchvision&#xff1a;0.13.0 mmcv-full&#xff1a;1.6.0 MMDetec…

机器学习极简入门笔记-5-无监督学习-K-means

目录 第17章 KNN算法&#xff08;有监督学习算法&#xff0c;放在此位置是为了与下一章的K-means做对比&#xff09; 17.1 KNN算法原理 17.2 KNN中的K 第18章 K-means——最简单的聚类算法 18.1 K-means算法步骤 18.2 K-means算法具体细节 18.3 启发式算法 18.4 K-mean…

介绍a股level2数据接口委托队列的作用

a股level2数据接口可以实现量化交易的准确性&#xff0c;那用户在交易的过程中会发现在“委托队列”中&#xff0c;如果出现一些有规律性的单子&#xff0c;往往是主力之间的盘口暗语。 例如排在靠前的都是大单&#xff0c;封单相当坚决&#xff0c;如果细心一点还会发现都是拖…

Spring Security 自定义拦截器Filter实现登录认证

前言 需求来源: 微信小程序获取授权码code, 通过授权码code, 获取微信用户信息(比如openid,unionId), 并记录登录状态(比如token信息的获取); 原本打算使用Spring Security中OAuth2.0的机制 实现用小程序登录&#xff0c;发现小程序再已经获取授权码code登录流程和Spring Secu…

Intellij插件之~图形界面Swing UI Designer

资料 Java Swing 介绍 JavaFX快速入门 Java Swing 图形界面开发简介 GUI Designer Basics scrcpy ScrcpyController Services&#xfeff; 创建一个Gui Form文件(类form文件) 创建一个Gui Form 此时生成两个类, 是自动关联的 鼠标可以拖入控件,每拖入一个组件,在TestForm…