二叉树进阶 --- 上

目录

1. 二叉搜索树的概念及结构

1.1. 二叉搜索树的概念

1.2. 二叉搜索树的结构样例

2. 二叉搜索树的实现

2.1. insert 的非递归实现

2.2. find 的非递归实现

2.3. erase 的非递归实现

2.3.1. 第一种情况:所删除的节点的左孩子为空

2.3.1.1. 错误的代码

2.3.1.2. 正确的代码

2.3.2. 第二种情况:所删除的节点的右孩子为空

2.3.2.1. 正确的代码

2.3.3. 第三种情况:所删除的节点有两个非空节点 && 找右子树的最左节点

2.3.3.1. 有错误的代码

2.3.3.2. 正确的代码

2.3.4. erase的完整实现如下


1. 二叉搜索树的概念及结构

学习二叉搜索树的一些原因:

  • map 和 set 特性需要先铺垫二叉搜索树,而二叉搜索树也是一种树形结构;
  • 二叉搜索树的特性了解,有助于更好的理解 map 和 set 的特性。

1.1. 二叉搜索树的概念

二叉搜索树(Binary Search Tree,简称BST) 又名为二叉排序树或者是二叉查找树。它可能是一棵空树,或者是满足下面性质的二叉树:

  • 如果它的左子树不为空,那么左子树上的所有节点的值都要小于根节点的值;
  • 如果它的右子树不为空,那么右子树上的所有节点的值都要大于根节点的值;
  • 它的左右子树也是一颗二叉搜索树。

对于一颗二叉搜索树,它的中序遍历可以得到有序的数据;

需要注意的是,二叉搜索树要求每个节点的值都唯一,如果存在重复的值,可以在节点中添加计数器来解决。

1.2. 二叉搜索树的结构样例

一棵树是否是一颗二叉搜索树,必须要符合二叉搜索树的性质。

 为了更好地理解二叉搜索树,我们需要其进行模拟实现:

2. 二叉搜索树的实现

2.1. insert 的非递归实现

对于二叉搜索树的插入,我们需要满足插入后的二叉树仍旧是一颗二叉搜索树,也就是说,插入的元素必须要被插入到特定的位置,以维持二叉搜索树的结构。如上图所示,如果要插入14,那么它的位置是确定的,如下图所示: 

因此 insert 的具体实现我们可以分解为两个过程:

  • 第一步:找到要插入元素的位置;
  • 第二步:插入元素,完成连接关系。 

注意:在这里实现的二叉搜索树的每个值具有唯一性,相同值不插入。

bool insert(const T& key)
{
	// 1. 如果是空树,直接对_root赋值即可,插入成功并返回true
	if (_root == nullptr)
	{
		_root = new Node(key);
		return true;
	}
	else
	{
		// step 1: 先找目标位置
		Node* cur = _root;
		// 为了更好的连接新节点, 因此记录父节点
		Node* parent = nullptr;

		while (cur)
		{
			// 如果当前节点的Key大于目标Key
			// 当前节点应该向左子树走
			if (cur->_key > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			// 如果当前节点的Key小于目标Key
			// 当前节点应该向右子树走
			else if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				// 找到了相同的 key, 在这里不插入
				return false;
			}
		}

		// cur 走到了空, 即 cur 就是合适的位置
		cur = new Node(key);
		// 我们需要判断cur是parent的左节点还是右节点
		// 如果key小于parent的key,那么插入左节点
		if (key < parent->_key)
			parent->_left = cur;
		// 反之连接到右节点
		else
			parent->_right = cur;
		return true;
	}
}

2.2. find 的非递归实现

find 就很简单了,没什么要说的,根据传递的 key 进行判断,大于当前节点,那么当前节点向左走,反之向右走,如果相等,返回true,循环结束,则说明没有这个key,实现如下:

bool find(const T& key)
{
	// 1. 从根节点开始
	Node* cur = _root;
	while (cur)
	{
		// 2. 如果当前关键字大于目标关键字,那么向左子树走
		if (cur->_key > key)
			cur = cur->_left;
		// 3. 如果小于目标关键字,那么向右子树走
		else if (cur->_key < key)
			cur = cur->_right;
		// 4. 相等,就返回true
		else
			return true;
	}
	// 5. 循环结束,说明没找到, 返回false
	return false;
}

2.3. erase 的非递归实现

对于搜索二叉树来说,真正有一些难度的是删除,对于删除我们可以分解为不同的情况,根据对应的情况,以特点方式解决。

在这里我们分为三种情况:

  • 所删除的节点的左孩子为空:托孤法删除;
  • 所删除的节点的右孩子为空:托孤法删除;
  • 所删除的节点的有两个非空孩子:替代法删除。

注意:对于叶子结点的处理可以归为第一类情况或者第二类情况。

为了可以更好的理解上面的三种情况,我们用图来说话:

2.3.1. 第一种情况:所删除的节点的左孩子为空

如图所示:假如现在我们要删除的节点是15节点,可以发现它的左孩子为空,那么如何删除呢?

 我们的方法是托孤法删除,什么叫托孤法删除呢?

就是将15的非空孩子(在这里就是19)交给它的父亲节点(在这里就是8),如图所示:

注意:在这里一定是父亲节点的右孩子指向被删除的节点的非空孩子吗?

答案是,不一定,我们需要根据被删除节点和父亲节点的关系判断:

  • 如果被删除节点是父亲节点的右孩子,那么在这里就是父亲节点的右孩子指向被删除节点的非空节点;
  • 如果被删除节点是父亲节点的左孩子,那么在这里就是父亲节点的左孩子指向被删除节点的非空节点。

代码如下:

2.3.1.1. 错误的代码
// 第一种情况: 所删除的节点的左孩子为空
if (del->_left == nullptr)
{
	if (del_parent->_left == del)
	{
		del_parent->_left = del->_right;
	}
	else
	{
		del_parent->_right = del->_right;
	}
	delete del;
    del = nullptr;
}

可能我们认为这段代码没问题,但是如果是下面这种情况呢?  

如果我此时要删除8,而8是这棵树的根节点,它是没有父节点的,那么此时上面的代码就会崩溃;

为了解决这个隐患,我们的方案就是,如果被删除节点是根,且它的左子树为空树,那么我们更新根节点即可,在这里就是让15做根节点。

2.3.1.2. 正确的代码
//第一种情况:所删除的节点的左孩子为空
if (del->_left == nullptr)
{
	// 如果被删除节点是根,那么更新根即可
	if (del == _root)
	{
		Node* newroot = del->_right;
		delete _root;
		_root = newroot;
	}
	// 被删除节点是非根节点
	else
	{
		if (del_parent->_left == del)
		{
			del_parent->_left = del->_right;
		}
		else
		{
			del_parent->_right = del->_right;
		}
		delete del;
	}
}

2.3.2. 第二种情况:所删除的节点的右孩子为空

如图所示:假如现在我们要删除的节点是6节点,可以发现它的右孩子为空,那么如何删除呢?

方案依旧是托孤法删除,在这里就是将6(被删除节点)的5(非空孩子节点)交给4(父亲节点) ,如下:

 处理细节,和第一种情况大同小异。

需要注意的就是:最后父亲节点连接非空孩子节点的时候,要根据被删除节点是父亲节点的左孩子还是右孩子来判断。

第二种情况和第一种情况大同小异,也需要对根节点进行特殊处理:

代码如下:

2.3.2.1. 正确的代码
//第二种情况:所删除的节点的右孩子为空
else if (del->_right == nullptr)
{
    // 当被删除节点为根节点
	if (del == _root)
	{
		Node* newroot = del->_left;
		delete del;
		_root = newroot;
	}
    //当被删除节点为非根节点
	else
	{
		if (del_parent->_left == del)
		{
			del_parent->_left = del->_left;
		}
		else
		{
			del_parent->_right = del->_left;
		}
		delete del;
        del = nullptr;
	}
}

2.3.3. 第三种情况:所删除的节点有两个非空节点 && 找右子树的最左节点

较为复杂的就是第三种情况了,由于被删除的节点有两个孩子,因此无法托孤,因为父亲节点至多只能管理两个孩子,所以我们又提出了新的解决方案:替代法删除

如图所示:

假如现在我们要删除4所在的节点,可以发现,4所在的节点有两个孩子,因此无法托孤,那么我们需要采用替代法删除,替代法删除就是在左子树或者右子树找一个"合适节点",将4所在的节点的key进行覆盖,将删除4所在的节点转化为删除我们找的这个"合适节点"。

而这个"合适节点"通常只有两个:

  • 其一:以被删除的节点所在的二叉树开始,左子树的最大节点,即左子树的最右(大)节点;
  • 其二:以被删除的节点所在的二叉树开始,右子树的最小节点,即右子树的最左(小)节点。

而我们在这里就找右子树的最左(小)节点,注意,要从被删除的节点开始,在这里就是5;

当找到这个 "合适节点" 后,交换它与要删除节点的 Key;

此时就将删除目标节点转换为删除这个 "合适节点" 了;

因为此时这个 "合适节点" 只会有两种情况:

  • 第一种:它没有孩子,即左右子树为空;
  • 第二种:它只有一个孩子,且只可能是右孩子,因为这个 "合适节点" 是右子树的最左节点;

如果是第一种,即没有孩子,我们也可以将其归类为第二种情况,即右孩子不为空 && 左孩子为空,因此,可以用统一的方式处理,即托孤法删除 (所删除的节点的左孩子为空)。

故我们的大致步骤如下:

  1. 找合适节点;
  2. 交换合适节点和删除节点的 Key;
  3. 托孤发删除合适节点。

如图所示:

2.3.3.1. 有错误的代码
// 第三种情况:所删除的节点有两个非空节点
else
{
	// 从被删除结点开始, 找右子树的最小(左)节点
	Node* right_min = del->_right;
	// 并记录这个节点的父亲节点
    // 有可能这里我们会习惯的从nullptr开始,但是对于某些特殊情况会崩溃
	Node* right_min_parent = nullptr;
	while (right_min->_left)
	{
		right_min_parent = right_min;
		right_min = right_min->_left;
	}
	// 交换这个节点和要删除节点的 key
	std::swap(del->_key, right_min->_key);

	// 将删除 del 转化为删除 right_min (托孤法删除)
	if (right_min_parent->_left == right_min)
		right_min_parent->_left = right_min->_right;
	else
		right_min_parent->_right = right_min->_right;
	delete right_min;
	right_min = nullptr;
}

如果我们将"合适节点"的父节点初始值设为nullptr,那么在下面的场景会发生崩溃:

由于此时,这个 "合适节点" 正好是 del->_right,不会进入循环,那么 right_min_parent 就是空,那么后面的操作就会对空指针进行解引用,非法操作,进程崩溃。

因此这里的 right_min_parent 的初始值可以从 del 开始,不可以将初始值设为空。

同时,我们发现,最后进行托孤法删除时,我们也进行了判断,这样的原因是因为这个"合适节点"既可能是父节点的左孩子,也可能是父节点的右孩子,因此必须判断。

2.3.3.2. 正确的代码
// 第三种情况:所删除的节点有两个非空节点
else
{
	// 从被删除结点开始, 找右子树的最小(左)节点
	Node* right_min = del->_right;
	// 并记录这个节点的父亲节点, 让其从del开始
	Node* right_min_parent = del;
	while (right_min->_left)
	{
		right_min_parent = right_min;
		right_min = right_min->_left;
	}
	// 交换这个节点和要删除节点的 key
	std::swap(del->_key, right_min->_key);

	// 将删除 del 转化为删除 right_min (托孤法删除)
	if (right_min_parent->_left == right_min)
		right_min_parent->_left = right_min->_right;
	else
		right_min_parent->_right = right_min->_right;
	delete right_min;
	right_min = nullptr;
}

以此类推,我们也可以找左子树的最大(右)节点,代码如下: 

else
{
    // 从被删除节点开始, 找左子树的最大(右)节点
    Node* left_max = del->_left;
    // 并记录这个节点的父亲节点, 让其从del开始
    Node* left_max_parent = del;
    while (left_max->_right)
    {
    	left_max_parent = left_max;
    	left_max = left_max->_right;
    }
    // 交换这个节点和要删除节点的 key
    std::swap(del->_key, left_max->_key);

    // 将删除 del 转化为删除 left_max (托孤法删除)
    if (left_max_parent->_left == left_max)
    	left_max_parent->_left = left_max->_left;
    else
    	left_max_parent->_right = left_max->_left;
    delete left_max;
    left_max = nullptr;
}

最后,我们将第三种情况汇总,第一种是找右子树的最左节点,第二种是找左子树的最右节点,如下:

else 
{
	// 从被删除节点开始, 找右子树的最小(左)节点 || 找左子树的最大(右)节点
	if (del->_right)
		_erase_right_min_node(del);
	else
		_erase_left_max_node(del);
}

void _erase_right_min_node(Node* del) 
{
	// 从被删除结点开始, 找右子树的最小(左)节点
	Node* right_min = del->_right;
	// 并记录这个节点的父亲节点, 让其从del开始
	Node* right_min_parent = del;
	while (right_min->_left)
	{
		right_min_parent = right_min;
		right_min = right_min->_left;
	}
	// 交换这个节点和要删除节点的 key
	std::swap(del->_key, right_min->_key);

	// 将删除 del 转化为删除 right_min (托孤法删除)
	if (right_min_parent->_left == right_min)
		right_min_parent->_left = right_min->_right;
	else
		right_min_parent->_right = right_min->_right;
	delete right_min;
	right_min = nullptr;
}

void _erase_left_max_node(Node* del)
{
	// 从被删除节点开始, 找左子树的最大(右)节点
	Node* left_max = del->_left;
	// 并记录这个节点的父亲节点, 让其从del开始
	Node* left_max_parent = del;
	while (left_max->_right)
	{
		left_max_parent = left_max;
		left_max = left_max->_right;
	}
	// 交换这个节点和要删除节点的 key
	std::swap(del->_key, left_max->_key);

	// 将删除 del 转化为删除 left_max (托孤法删除)
	if (left_max_parent->_left == left_max)
		left_max_parent->_left = left_max->_left;
	else
		left_max_parent->_right = left_max->_left;
	delete left_max;
	left_max = nullptr;
}

2.3.4. erase的完整实现如下

bool erase(const T& key)
{
	// 先找要删除的节点
	Node* del = _root;
	Node* del_parent = nullptr;

	while (del)
	{
		if (del->_key < key)
		{
			del_parent = del;
			del = del->_right;
		}
		else if (del->_key > key)
		{
			del_parent = del;
			del = del->_left;
		}
		else
		{
			// 锁定了要删除的节点
			// 分三种情况:
			// case 1: 左子树为空
			if (del->_left == nullptr)
			{
				// 如果要删除的节点是根
				if (del == _root)
				{
					Node* newroot = del->_right;
					delete _root;
					_root = newroot;
				}
				else
				{
					// 托孤法删除
					if (del_parent->_left == del)
						del_parent->_left = del->_right;
					else
						del_parent->_right = del->_right;
					delete del;
					del = nullptr;
				}
			}
			// case 2: 右子树为空
			else if (del->_right == nullptr)
			{
				if (_root == del)
				{
					Node* newroot = del->_left;
					delete _root;
					_root = newroot;
				}
				else
				{
					if (del_parent->_left == del)
						del_parent->_left = del->_left;
					else
						del_parent->_right = del->_left;
					delete del;
					del = nullptr;
				}
			}
			// case 3: 左右子树都不为空
			else 
			{
				// 从被删除节点开始, 找右子树的最小(左)节点 || 找左子树的最大(右)节点
				if (del->_right)
					_erase_right_min_node(del);
				else
					_erase_left_max_node(del);
			}
			return true;
		}
	}

	return false;
}

void _erase_right_min_node(Node* del) 
{
	// 从被删除结点开始, 找右子树的最小(左)节点
	Node* right_min = del->_right;
	// 并记录这个节点的父亲节点, 让其从del开始
	Node* right_min_parent = del;
	while (right_min->_left)
	{
		right_min_parent = right_min;
		right_min = right_min->_left;
	}
	// 交换这个节点和要删除节点的 key
	std::swap(del->_key, right_min->_key);

	// 将删除 del 转化为删除 right_min (托孤法删除)
	if (right_min_parent->_left == right_min)
		right_min_parent->_left = right_min->_right;
	else
		right_min_parent->_right = right_min->_right;
	delete right_min;
	right_min = nullptr;
}
void _erase_left_max_node(Node* del)
{
	// 从被删除节点开始, 找左子树的最大(右)节点
	Node* left_max = del->_left;
	// 并记录这个节点的父亲节点, 让其从del开始
	Node* left_max_parent = del;
	while (left_max->_right)
	{
		left_max_parent = left_max;
		left_max = left_max->_right;
	}
	// 交换这个节点和要删除节点的 key
	std::swap(del->_key, left_max->_key);

	// 将删除 del 转化为删除 left_max (托孤法删除)
	if (left_max_parent->_left == left_max)
		left_max_parent->_left = left_max->_left;
	else
		left_max_parent->_right = left_max->_left;
	delete left_max;
	left_max = nullptr;
}

二叉树进阶 --- 上,至此结束。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【工具篇】-什么是.NET

“.NET"&#xff1a;.NET Core是由Microsoft开发&#xff0c;目前在.NET Foundation(一个非营利的开源组织)下进行管理。.NET Core是用C#和C编写的&#xff0c;并采用MIT协议作为开源协议。 简单来说&#xff1a;就是开发框架。 .NET 又称 .NET 平台或 .NET 框架&#xf…

Container exited with a non-zero exit code 1

最近遇到运行yarn pi的时候遇到如下问题。 很明显是container出错了&#xff0c;但是错误没有提示的很清楚。然后去看nodemanager日志也是如此。这时候笔者第一个想到要去看container的执行日志。container具体的日志目录位置是通过YARN的配置文件&#xff08;如yarn-site.xml&…

JavaSE——集合框架一(1/7)-集合体系概述(集合体系结构,Collection集合体系)、Collection的常用方法(介绍,实例演示,代码)

目录 集合体系概述 集合体系结构 Collection集合体系 Collection的常用方法 介绍 实例演示 完整代码 集合体系概述 集合体系结构 集合是一种容器&#xff0c;用来装数据的&#xff0c;类似于数组&#xff0c;但集合的大小可变&#xff0c;开发中也非常常用。 为了满足…

sql注入之bool盲注

目录 盲注步骤 1、进入靶场 2、如下图所示输入&#xff1f;id1‘ 判断此时存在注入点 3、判断列数 ​编辑 4、开始盲注 普通的python脚本 代码思想 结果 二分查找python脚本 二分查找算法思想简介 二分查找与普通查找的主要差距 代码思想 代码 结果​编辑 下面以…

Fcos源码训练编译问题

训练fcos代码时出现问题 ImportError: cannot import name ‘_C’ 原因是没有对代码进行编译 运行python setup.py develop --no-deps进行代码编译 编译过程中出现报错&#xff1a; fcos_core/csrc/cuda/ROIAlign_cuda.cu:5:10: fatal error: THC/THC.h: No such file or dire…

iphone进入恢复模式怎么退出?分享2种退出办法!

iPhone手机莫名其妙的进入到了恢复模式&#xff0c;或者是某些原因需要手机进入恢复模式&#xff0c;但是之后我们不知道如何退出恢复模式怎么办&#xff1f; 通常iPhone进入恢复模式的常见原因主要是软件问题、系统升级失败、误操作问题等导致。那iphone进入恢复模式怎么退出&…

Python-VBA函数之旅-staticmethod函数

目录 一、staticmethod函数的常见应用场景 二、staticmethod函数使用注意事项 三、如何用好staticmethod函数&#xff1f; 1、staticmethod函数&#xff1a; 1-1、Python&#xff1a; 1-2、VBA&#xff1a; 2、推荐阅读&#xff1a; 个人主页&#xff1a; https://blog…

【EMQX实践】如何感知设备上下线?

序言 在智能物联时代&#xff0c;存设备需要联网上云场景。EMQX在此场景中属于设备连接网关关键节点&#xff0c;EMQX不紧紧只是消息中间件的作用&#xff0c;我们更需要监控哪些设备什么时候连接上线&#xff0c;又在什么时候断开下线。 为什么需要感知设备上下线 感知设备…

二叉树介绍

引入 定义 区别 定义不同 形态不同 基本形态

vscode默认终端设置为cmd的方法

vscode默认终端是powershell,执行某些命令时会提示权限等问题&#xff0c;如果更习惯使用cmd终端的话&#xff0c;可以将默认终端配置为cmd。 方法一&#xff1a; 方法二&#xff1a; 如果你想更改默认的终端&#xff0c;可以通过以下步骤操作&#xff1a; 打开 VSCode。使用…

Applied Spatial Statistics(五)线性回归 I

Applied Spatial Statistics&#xff08;五&#xff09;线性回归 I 该笔记本演示了&#xff1a; 线性回归系数估计在假设下是无偏的如何围绕系数估计构建 bootstrap 置信区间残差图Q-Q图 1. 线性回归系数估计在假设下是无偏的 import numpy as np import matplotlib.pyplot…

大数据比赛-环境搭建(一)

1、安装VMware Workstation 链接&#xff1a;https://pan.baidu.com/s/1IvSFzpnQFl3svWyCGRtEmg 提取码&#xff1a;ukpo 内有安装包及破解方式&#xff0c;安装教程。 2、下载Ubuntu系统 阿里巴巴开源镜像站-OPSX镜像站-阿里云开发者社区 (aliyun.com) 点击下载&#xff…

掌握未来搜索的钥匙:深入解析 Milvus 向量搜索引擎的终极指南!

在大数据时代&#xff0c;向量搜索技术愈发重要。作为一个开源的向量相似性搜索引擎&#xff0c;Milvus 提供了基于向量的相似性搜索功能&#xff0c;广泛应用于机器学习、人工智能等领域。本文将深入介绍 Milvus 的基本概念&#xff0c;包括其介绍、主要作用、使用方法及注意事…

命令行工具部署达梦数据库 DMDPC(BP 多副本架构)

解达梦数据库DPC集群的主要使用场景&#xff1a; DMDPC 关注和解决的是大数据、计算与存储分离、高可用、支持全部的 SQL 标准、拥有完整的事务处理能力和集群规模能够动态伸缩的业务场景&#xff1a; 大量的复杂查询操作要求优化器能够生成优良的执行计划&#xff0c;并且执…

【北京迅为】《iTOP-3588从零搭建ubuntu环境手册》-第7章 安装VMwareTools

RK3588是一款低功耗、高性能的处理器&#xff0c;适用于基于arm的PC和Edge计算设备、个人移动互联网设备等数字多媒体应用&#xff0c;RK3588支持8K视频编解码&#xff0c;内置GPU可以完全兼容OpenGLES 1.1、2.0和3.2。RK3588引入了新一代完全基于硬件的最大4800万像素ISP&…

编程代码的舞者--Python循环语句

循环语句是编程中不可或缺的要素之一&#xff0c;它们能够让程序反复执行特定的任务&#xff0c;提高代码的重复利用性和效率。在本篇博客中&#xff0c;我们将深入探讨Python中常用的循环语句&#xff0c;包括for循环和while循环&#xff0c;以及控制循环流程的关键字break和c…

OBS插件--复合模糊

复合模糊 复合是一款滤镜插件&#xff0c;支持多种模糊类型和多种蒙版效果。支持模糊源的部分显示区域&#xff0c;可以反选区域进行模糊&#xff0c;这个功能对于场景部分区域需要遮盖非常实用。 下面截图演示下操作步骤&#xff1a; 首先&#xff0c;打开 OBS直播助手 在…

draw.io 网页版二次开发(3):打包和部署(war包)

目录 一 说明 二 环境配置 1. 下载并安装 Apache Ant 2. 下载并安装JDK和JRE 3. 下载tomcat 4. Ant、JDK和JRE 环境变量的配置 三 draw.io打包 四 部署 五 最后 一 说明 应公司项目要求&#xff0c;需要对draw.io进行二次开发&#xff0c;并将html界面通过iframe 嵌…

数据结构之——队列详解

目录 前言&#xff1a; 一、什么是队列 二、队列的实现 2.1 队列结构 2.2 队列初始化 2.3 队列销毁 2.4 入队列 2.5 出队列 2.6 获取队列头部元素 2.7 获取队列尾部元素 2.8 获取队列中有效元素个数 2.9 检测队列是否为空 三、 代码总览 Queue.h test.c 四、例题 前言…

SpringSecurity集成第三方登录

SpringSecurity 集成第三方登录 认证及自定义流程 首先我们提供一个实现了AbstractAuthenticationProcessingFilter抽象类的过滤器&#xff0c;用来代替UsernamePasswordAuthenticationFilter逻辑&#xff0c;然后提供一个AuthenticationProvider实现类代替AbstractUserDetail…