C++ 浅谈之 AVL 树和红黑树

news2024/11/15 1:44:45

C++ 浅谈之 AVL 树和红黑树

HELLO,各位博友好,我是阿呆 🙈🙈🙈

这里是 C++ 浅谈系列,收录在专栏 C++ 语言中 😜😜😜

本系列阿呆将记录一些 C++ 语言重要的语法特性 🏃🏃🏃

OK,兄弟们,废话不多直接开冲 🌞🌞🌞


一 🏠 概述

AVL 树

上文提到对于二叉搜索树有单边树的缺陷,那么对于 STL 关联容器 map、set 等底层结构对其进行了平衡处理,采用平衡树实现

AVL 树,当向二叉搜索树插入新结点后,保证每个结点左右子树高度差绝对值不超过 1 ,降低树高度,减少平均搜索长度


概念

AVL 树是空树或具有如下性质的二叉搜索树

① 左右子树都是 AVL 树

② 左右子树高度之差 ( 简称平衡因子 : 右子树高 - 左子树高 ) 绝对值不超过 1

一棵二叉搜索树高度平衡,它就是 AVL 树。如果它有 n 个结点,其高度可保持在 O(log2N),搜索时间复杂度 O(log2N)


AVL 树节点定义

template<class K,class V>
struct AVLTreeNode
{
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;//定义成三叉链的形式
	int _bf;//balance factor平衡因子
	pair<K, V> _kv;//用pair同时存K和V两个数据
 
	AVLTreeNode(const pair<K,V>& kv)//节点构造函数
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)//平衡因子初始给0
		,_kv(kv)
	{}
};

AVL 树插入

AVL 树是在二叉搜索树基础上引入平衡因子,插入过程如下

① 按照二叉搜索树方式找到空位置,插入新节点

② 插入新节点后,需要调整节点的平衡因子

③ 插入元素后可能导致 AVL 树左右子树高度不符合条件,需要旋转


AVL 树旋转

AVL树的旋转分为多种,具体举出一种如下图所示(右单旋)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1k1kVipt-1676620255584)(E:\2022年MD文档\2023 年 MD文档\二月\浅谈系列\C++ 浅谈之 AVL 树和红黑树.assets\企业微信截图_16766174867883.png)]

Parent 平衡因子为 2 或 -2 ,分以下情况

① 平衡因子为 2,说明右子树高,需要往左边压高度,设 Parent 右子树根为 SubR

当 SubR 平衡因子为 1 时,执行左单旋

当 SubR 平衡因子为 -1 时,执行右左双旋

② 平衡因子为 -2,说明左子树高,需要往右边压高度,设 Parent 左子树根为 SubL

当 SubL 平衡因子为 -1 时,执行右单旋

当 SubL 平衡因子为 1 时,执行左右双旋

旋转完成后,原 Parent 为根子树个高度降低,已经平衡(无需往上更新,直接退出循环)


AVL 树模拟实现

#pragma once
#include<iostream>
#include<assert.h>
#include<string>
using namespace std;
 
 
template<class K,class V>
struct AVLTreeNode
{
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;//定义成三叉链的形式
	int _bf;//balance factor平衡因子
	pair<K, V> _kv;//用pair同时存K和V两个数据
 
	AVLTreeNode(const pair<K,V>& kv)//节点构造函数
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)//平衡因子初始给0
		,_kv(kv)
	{}
};
 
template<class K,class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	AVLTree()
		:_root(nullptr)
	{}
	//拷贝构造和赋值拷贝也需要自己实现
	AVLTree(const AVLTree<K,V>& kv)
	{
		_root = Copy(kv._root);
	}
	AVLTree<K, V>& operator=(AVLTree<K,V> kv)
	{
		swap(_root, kv._root);
		return *this;
	}
	~AVLTree()
	{
		Destroy(_root);
		_root = nullptr;
	}
	Node* Copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;
		Node* newroot = new Node(root->_key);//建立新节点
		newroot->_left = Copy(root->_left);//新节点的左右节点再去转换成子问题
		newroot->_right = Copy(root->_right);
		return newroot;//最后返回新节点
	}
	void Destroy(Node* root)
	{
		//利用后序遍历去释放节点
		if (root == nullptr)
		{
			return;
		}
		Destroy(root->_left);
		Destroy(root->_right);
		delete root;
	}
	V& operator[](const K& key)//重载operator[]
	{
		//operator[]的原则是:
		//如果插入成功返回插入都value的引用
		//如果插入失败则返回V类型默认缺省值
		pair<Node*, bool> ret = Insert(make_pair(key, V()));//V采用传匿名对象的方式
		return ret.first->_kv.second;
	}
	Node* Find(const pair<K, V>& kv)//查找函数
	{
		Node* cur = _root;
		while (cur)
		{
			if (kv.first > cur->_kv.first)
			{
				cur = cur->_right;
			}
			else if (kv.first < cur->_kv.first)
			{
				cur = cur->_left;
			}
			else
			{
				return cur;
			}
		}
		return nullptr;
	}
	void RotateR(Node* parent)//右单旋
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
 
		parent->_left = subLR;
		if (subLR != nullptr)//注意:这里一定要判断不为空的,因为下面可能会出现空指针的解引用
		{
			subLR->_parent = parent;
		}
		subL->_right = parent;
		Node* parentParent = parent->_parent;//一定要在改变链接关系之前把这个指针存下来
		parent->_parent = subL;
		
		//if (parentParent == nullptr)或者采用这个条件也是可以的
		if(parent==_root)
		{
			_root = subL;
			_root->_parent = nullptr;
		}
		else
		{
			//这里注意:parent还有父母时,链接之前需要注意判断到底是右孩子还是左孩子
			if (parentParent->_left == parent)
				parentParent->_left = subL;
			else
				parentParent->_right = subL;
 
			subL->_parent = parentParent;//最后还要把父指针关系链接上
		}
 
		parent->_bf = subL->_bf = 0;//最后右单旋完成后平衡因子都要修改成0
	}
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
 
		//先把subR的左孩子赋值给parent的右节点
		parent->_right = subRL;
		if (subRL != nullptr)//注意一定要判断是否为空的情况
		{
			subRL->_parent = parent;//然后链接parent指针
		}
 
		//然后subR的左节点链接上parent
		subR->_left = parent;
		Node* parentParent = parent->_parent;//提前记录
		parent->_parent = subR;
		//if (parentParent == nullptr)
		if (parent == _root)
		{
			_root = subR;
			_root->_parent = nullptr;
		}
		else
		{
			if (parentParent->_left == parent)
				parentParent->_left = subR;
			else
				parentParent->_right = subR;
 
			subR->_parent = parentParent;
		}
 
		parent->_bf = subR->_bf = 0;
	}
	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;//注意:需要提前存subRL的平衡因子,因为旋转可能引起改变
		//subRL的平衡因子是双旋的关键节点
 
		RotateR(parent->_right);//先进行右旋,并注意旋转点为父节点的右节点
		RotateL(parent);//再进行左旋,此时旋转点为父节点
 
		if (bf == 0)
		{
			parent->_bf = 0;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == 1)
		{
			parent->_bf = -1;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 0;
			subR->_bf = 1;
			subRL->_bf = 0;
		}//注意这里处理完成过后sunRL的平衡因子一定都是等于0的
		else
		{
			assert(false);
		}
	}
	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;
		RotateL(parent->_left);//先进行左旋,并注意旋转点为父节点的左节点
		RotateR(parent);//再进行右旋,此时旋转点为父节点
 
		if (bf == 0)
		{
			parent->_bf = 0;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else if (bf == 1)
		{
			parent->_bf = 0;
			subL->_bf = -1;
			subLR->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 1;
			subL->_bf = 0;
			subLR->_bf = 0;
		}//注意这里处理完成过后sunRL的平衡因子一定都是等于0的
		else
		{
			assert(false);
		}
	}
	pair<Node*,bool> Insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)//根节点为空时先new一个新节点
		{
			_root = new Node(kv);
			return make_pair(_root, true);
		}
 
		Node* cur = _root;
		Node* parent = nullptr;
		//先利用while循环去找cur的空位
		while (cur)
		{
			if (kv.first > cur->_kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (kv.first < cur->_kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return make_pair(cur, false);
			}
		}
		//将cur插入到相应位置
		cur = new Node(kv);
		Node* newnode = cur;//用一个newnode记录一下新节点用以返回
		if (kv.first > parent->_kv.first)
		{
			parent->_right = cur;//注意三叉链的链接逻辑顺序,等号左右方向不能反,先把cur链接到父节点的右边
			cur->_parent = parent;//然后再去把父指针知道父节点
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}
 
		//进行旋转调整
		//while(cur!=_root)
		while (parent)
		{
			//1.进入循环先对平衡因子进行调整
			if (cur == parent->_right)
			{
				parent->_bf++;
			}
			else
			{
				parent->_bf--;
			}
 
			//分三种情况向上走
			if (parent->_bf == 0)//平衡因子等于0不需要调整
			{
				//为什么不需调整
				//因为等于0的话,说明底层子树高度不平衡,添加进入新元素后平衡了,只要平衡了高度并没发生变化,不会影响上面的父节点
				break;
			}
			else if (parent->_bf == -1 || parent->_bf == 1)
			{
				//平衡因子等于-1,说明插入新节点后子树的高度不平衡了,需要继续往上迭代查看父节点是否还满足平衡节点
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				if (parent->_bf == -2)//父节点等于-2,说明左边高,触发右旋的情况
				{
					if (cur->_bf == -1)//cur节点等于-1,说明在cur的左边更高,触发右单旋的情况
					{
						RotateR(parent);
					}
					else//cur等于-1,说明在cur的右边更高,触发左右双旋
					{
						RotateLR(parent);
					}
				}
				else//父节点等于1,说明右边更高,触发左旋的情况
				{
					if (cur->_bf == 1)//cur节点等于1时,说明在cur的右边更高,触发右单旋的情况
					{
						RotateL(parent);
					}
					else//cur等于-1,说明在cur的左边更高,触发右左双旋
					{
						RotateRL(parent);
					}
				}
				//思考:为什么上面在传参数的时候,都是传parent的节点呢?这样的好处是什么呢
 
				break;//调整完成后break退出循环
				//这里为什么调整完成过后就可以退出,通过旋转调整平衡因子后,parent节点的平衡因子都为0了,调整过后不需要再向上继续查找了
			}
			else
			{
				assert(false);
			}
		}
		return make_pair(newnode,true);
	}
	void _Inorder(Node* root)//中序遍历打印每个节点
	{
		if (root == nullptr)
			return;
		_Inorder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;
		_Inorder(root->_right);
	}
	void Inorder()
	{
		_Inorder(_root);
		cout << endl;
	}
 
	//验证是否为平衡二叉树
	//1.左子树高度与右子树高度差必须小于1
	int _Height(Node* root)//求树的高度函数
	{
		if (root == nullptr)
		{
			return 0;
		}
 
		int leftHeight = _Height(root->_left);//递归去子问题求解
		int rightHeight = _Height(root->_right);
 
		return rightHeight > leftHeight ? rightHeight + 1 : leftHeight + 1;
	}
	bool _IsBalance(Node* root)
	{
		if (root == nullptr)
		{
			return true;
		}
 
		int leftHeight = _Height(root->_left);
		int rightHeight = _Height(root->_right);
 
		// 2.检查一下每颗树的平衡因子是否正确
		if (rightHeight - leftHeight != root->_bf)
		{
			cout << "平衡因子异常:" << root->_kv.first << endl;
			return false;
		}
 
		return abs(rightHeight - leftHeight) < 2
			&& _IsBalance(root->_left)
			&& _IsBalance(root->_right);//分别递归到各自的左右子树再去检查
	}
	bool IsAVLTree()
	{
		return _IsBalance(_root);
	}
private:
	Node* _root;
};

二 🏠 核心

红黑树

红黑树的概念

一种二叉搜索树,在每个结点上增加一个存储位表示结点颜色,Red 或 Black。确保没有一条路径会比其他路径长 2 倍,因而接近平衡


红黑树性质

红黑树的定义

  1. 每个结点不是红就是黑
  2. 根节点是黑色
  3. 一个节点是红,则两个孩子是黑 ( 没有连续红节点 )
  4. 每条路径上包含相同数量黑节点
  5. 每个叶子结点都是黑 ( 此处指的是空结点,即 NIL 节点 )

为什么红黑树能保证最长路径中节点个数不会超过最短路径的两倍 ?

假设黑节点数量 N 个,最短路径为 :logN,最长路径为 :2 * logN . 所以最长路径节点数不会超过最短路径的两倍


红黑树节点定义

enum Colour
{
	RED,
	BLACK
};//节点的颜色
template<class K, class V>
struct RBTreeNode
{
	RBTreeNode<K, V>* _left;//节点左孩子
	RBTreeNode<K, V>* _right;//节点右孩子
	RBTreeNode<K, V>* _parent;//节点的父亲
	pair<K, V> _kv;//节点中存放的键值对
	Colour _col;//节点的颜色
 
	RBTreeNode(const pair<K,V> kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_kv(kv)
		,_col(RED)
	{}
};

为什么节点默认颜色是红色 ?

因为默认颜色黑色,将破坏定义四,其它每条路径都会因该路径增加一个黑节点而不满足红黑树性质,这是一种全局的破坏;默认颜色红色,会破坏定义三,但只会影响当前路径,并不会对其它路径造成影响


红黑树插入操作

可分为两步

  1. 按照二叉搜索树规则插入新节点

  2. 检测新节点插入后,红黑树性质是否造到破坏

如果父节点是黑色(未违反红黑树任何性质),则不需要调整;

父节点为红色时,违反了定义三不能有连在一起的红色节点

此时对红黑树有多种情况,如下图为其中一种讨论

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Aai00yvJ-1676620255586)(E:\2022年MD文档\2023 年 MD文档\二月\浅谈系列\C++ 浅谈之 AVL 树和红黑树.assets\1676619846457.png)]


红黑树模拟实现

#pragma once
#include<iostream>
using namespace std;
 
enum Colour
{
	RED,
	BLACK
};//节点的颜色
template<class K,class V>
struct RBTreeNode
{
	RBTreeNode<K,V>* _left;//节点左孩子
	RBTreeNode<K,V>* _right;//节点右孩子
	RBTreeNode<K,V>* _parent;//节点的父亲
	pair<K,V> _kv;//节点中存放的T类型的数据
	Colour _col;//节点的颜色
 
	RBTreeNode(const pair<K,V> kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_kv(kv)
		,_col(RED)
	{}
};
 
template<class K,class V>
class RBTree
{
	typedef RBTreeNode<K, V> Node;
public:
	RBTree()
		:_root(nullptr)
	{}
	//拷贝构造和赋值拷贝也需要自行实现这里不做赘述
	~RBTree()
	{
		Destory(_root);
		_root = nullptr;
	}
	void Destory(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		//利用后序遍历释放节点
		Destory(root->_left);
		Destory(root->_right);
		delete root;
	}
	void RotateR(Node* parent)//右单旋
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
 
		parent->_left = subLR;
		if (subLR != nullptr)//注意:这里一定要判断不为空的,因为下面可能会出现空指针的解引用
		{
			subLR->_parent = parent;
		}
		subL->_right = parent;
		Node* parentParent = parent->_parent;//一定要在改变链接关系之前把这个指针存下来
		parent->_parent = subL;
 
		//if (parentParent == nullptr)或者采用这个条件也是可以的
		if (parent == _root)
		{
			_root = subL;
			_root->_parent = nullptr;
		}
		else
		{
			//这里注意:parent还有父母时,链接之前需要注意判断到底是右孩子还是左孩子
			if (parentParent->_left == parent)
				parentParent->_left = subL;
			else
				parentParent->_right = subL;
 
			subL->_parent = parentParent;//最后还要把父指针关系链接上
		}
	}
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
 
		//先把subR的左孩子赋值给parent的右节点
		parent->_right = subRL;
		if (subRL != nullptr)//注意一定要判断是否为空的情况
		{
			subRL->_parent = parent;//然后链接parent指针
		}
 
		//然后subR的左节点链接上parent
		subR->_left = parent;
		Node* parentParent = parent->_parent;//提前记录
		parent->_parent = subR;
		//if (parentParent == nullptr)
		if (parent == _root)
		{
			_root = subR;
			_root->_parent = nullptr;
		}
		else
		{
			if (parentParent->_left == parent)
				parentParent->_left = subR;
			else
				parentParent->_right = subR;
 
			subR->_parent = parentParent;
		}
	}
	pair<Node*, bool> Insert(const pair<K, V> kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->_col = BLACK;//根节点给黑色
			return make_pair(_root, true);
		}
 
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)//循环去找空位
		{
			if (kv.first > cur->_kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if(kv.first < cur->_kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return make_pair(cur, false);
			}
		}
 
		Node* newnode = new Node(kv);
		newnode->_col = RED;//链接上新节点
		if (kv.first > parent->_kv.first)
		{
			parent->_right = newnode;
			newnode->_parent = parent;
		}
		else
		{
			parent->_left = newnode;
			newnode->_parent = parent;
		}
		cur = newnode;
 
		//父节点存在且父节点为红色时需要继续处理
		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent;
			//关键看叔叔的脸色
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;
				if (uncle && uncle->_col == RED)
				{
					//具体变色的情况需要画图分析:
					grandfather->_col = RED;
					parent->_col = uncle->_col = BLACK;
 
					//注意需要继续向上处理,容易忘记
					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					if (cur == parent->_left)
					{
						RotateR(grandfather);//右单旋
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						RotateL(parent);//先以父节点为旋转点左旋
						RotateR(grandfather);//再以g为旋转点右旋
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;//旋转+变色过后一定是满足所有性质的直接退出循环
				}
			}
			else
			{
				Node* uncle = grandfather->_left;
				if (uncle && uncle->_col == RED)
				{
					uncle->_col = parent->_col = BLACK;
					grandfather->_col = RED;
 
					cur = grandfather;
					parent = cur->_parent;
				}
				else // 情况2:+ 情况3:
				{
					if (cur == parent->_right)
					{
						RotateL(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else // cur == parent->_left
					{
						RotateR(parent);
						RotateL(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
		}
		_root->_col = BLACK;
		return make_pair(newnode, true);
	}
	void _InOrder(Node* root)//中序遍历递归打印
	{
		if (root == nullptr)
		{
			return;
		}
 
		_InOrder(root->_left);
		cout << root->_kv.first << ":"<<root->_kv.second<<endl;
		_InOrder(root->_right);
	}
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
	bool _CheckBlance(Node* root, int blackNum, int count)//balckNum相当于一个标尺,count就是用来记录每条路径的黑节点数目
	{
		if (root == nullptr)//如果root走到空节点
		{
			if (count != blackNum)//count不等于最左路径的黑色节点数
			{
				cout << "黑色节点的数量不相等" << endl;
				return false;//返回假
			}
			return true;//否则返回真
		}
		if (root->_col == RED && root->_parent->_col == RED)
		{
			cout << "存在连续的红色节点" << endl;
			return false;
		}
		if (root->_col == BLACK)
		{
			count++;
		}
		return _CheckBlance(root->_left, blackNum, count)
			&& _CheckBlance(root->_right, blackNum, count);//再递归到各子树的子问题
	}
	bool CheckBlance()
	{
		if (_root == nullptr)
		{
			return true;
		}
		if (_root->_col == RED)
		{
			cout << "根节点是红色的" << endl;
			return false;
		}
		// 找最左路径做黑色节点数量参考值
		int blackNum = 0;
		Node* left = _root;
		while (left)
		{
			if (left->_col == BLACK)
			{
				blackNum++;
			}
			left = left->_left;
		}
		int count = 0;
		return _CheckBlance(_root, blackNum, count);
	}
private:
	Node* _root;
};

红黑树与 AVL 树比较

都是高效平衡二叉树,增删改查时间复杂度都是 O( log2N) ,红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径 2 倍,降低了插入和旋转次数,所以在经常进行增删结构中性能比 AVL 树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多


三 🏠 结语

身处于这个浮躁的社会,却有耐心看到这里,你一定是个很厉害的人吧 👍👍👍

各位博友觉得文章有帮助的话,别忘了点赞 + 关注哦,你们的鼓励就是我最大的动力

博主还会不断更新更优质的内容,加油吧!技术人! 💪💪💪

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

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

相关文章

可靠、安全、稳定,开源高质量项目 | 亚马逊的开源文化

亚马逊的领导力准则是亚马逊文化的核心&#xff0c;它如同亚马逊的 DNA 融入贯穿每一个重要决策&#xff0c;深深影响着每一位亚麻人、影响着每一位亚马逊的客户、合作伙伴以及每一位亚马逊云科技的构建者。同时&#xff0c;亚马逊的领导力准则对亚马逊与开源的互动方式也产生着…

(原创)不小心禁用或者卸载Kotlin插件的解决方法

问题 之前因为kotlin版本的一些问题&#xff0c;不小心禁用了kotlin插件 等到再重启Android Studio&#xff0c;就发现进不去了 后来在网上找到方法解决了&#xff0c;但是某一天 又脑子一热&#xff0c;直接把Kotlin插件给卸载了&#xff0c;这下直接玩大发了 花了一点时间才…

Springboot 使用quartz 定时任务 增删改查

前段时间公司项目用到了 定时任务 所以写了一篇定时任务的文章 &#xff0c;浏览量还不错 &#xff0c; Springboot 整合定时任务 ) 所以就准备写第二篇&#xff0c; 如果你是一名Java工程师&#xff0c;你也可以会看到如下的页面 &#xff0c;去添加定时任务 定时任务展示 :…

linux学习笔记 超详细 0基础(下)shell

shell是一个命令解释器&#xff0c;为我们提供了交互式的文本控制台界面&#xff0c;我们可以通过终端控制台来输入命令&#xff0c;由shell解释并交给linux内核执行。Shell是一个解释器&#xff0c;Unix下的Bourne Shell命令解释器的加强版Bourne Again Shell &#xff0c;bas…

甘特图:项目管理工具,轻松简化工作流程

项目规模越大&#xff0c;管理就越复杂&#xff0c;有时候甚至一个项目经理需要管理多个项目&#xff0c;当多个项目、多条任务同时进行&#xff0c;项目所涉及的范围广&#xff0c;内容越来越复杂&#xff0c;使得项目越难以把控&#xff0c;好的管理工具&#xff0c;可以提升…

2023美赛C题:Wordle筛选算法

Wordle 规则介绍 Wordle 每天会更新一个5个字母的单词&#xff0c;在6次尝试中猜出单词就算成功。每个猜测必须是一个有效的单词&#xff08;不能是不能组成单词的字母排列&#xff09;。 每次猜测后&#xff0c;字母块的颜色会改变&#xff0c;颜色含义如下&#xff1a; 程…

Unity导出WebGL工程,并部署本地web服务器

WebGL打包 设置修改 在Build Settings->PlayerSettings->Other Settings->Rendering 将Color Space 设置为Gamma 将Lightmap Encoding 设置为NormalQuality 在Build Settings->PlayerSettings->Publishing Settings 勾选Decompression Fallback 打包 完成配…

有这几个表现可能是认知障碍前兆

我国目前对于认知障碍的认知率、就诊率、诊断率很低&#xff0c;然而认知障碍如果能在早期发现&#xff0c;并及时治疗&#xff0c;生活质量会有效提高&#xff0c;缓解家属的精神和经济负担。所以&#xff0c;认知障碍的前兆一定要了解。1.记忆力减退&#xff0c;一周内的重要…

【Spring】@Value注入配置文件 application.yml 中的值失败怎么办

本期目录一、 问题背景二、 问题原因三、 解决方法一、 问题背景 今天碰到的问题是用 Value 注解无法注入配置文件 application.yml 中的配置值。 检查过该类已经交给 Spring 容器管理了&#xff0c;即已经在类上加了 Configuration 和 ConfigurationProperties(prefix &quo…

UnityEditor编辑器扩展自己实现了一遍SceneView的镜头移动

基本实现由于最近一个星期都比较魔怔《天际线》&#xff0c;突然开始要工作了&#xff0c;用Editor好像突然没了按键反而不习惯就是要实现一个点击AWSD&#xff0c;能方便编辑地图的功能其实大可不必自己写代码本身Unity自带的&#xff0c;飞跃模式已经包含&#xff08;按鼠标右…

抽象工厂模式(Abstract Factory Pattern)

1.抽象工厂模式定义: 抽象工厂模式提供了一个创建一系列相关或者相互依赖对象的接口&#xff0c;无需指定它们具体的类 2.抽象工厂模式适用场景: 客户端(应用层)不依赖于产品类实例如何被创建、实现等细节强调一系列相关的产品对象(属于同一产品族)一起使用创建对象需要大量…

ONLYOFFICE中的chatGPT是怎样提升工作效率的

几乎一夜之间chatGPT火遍国内外网络&#xff0c;作为一个总是努力提高工作效率并在一天内完成更多工作的人&#xff0c;我很高兴发现 ONLYOFFICE添加了ChatGPT — 一个人工智能驱动的聊天机器人&#xff0c;可以帮助您管理时间、设定目标并改善您的个人和职业生活。 ONLOYOFFIC…

Allegro172版本无法低亮颜色的原因和解决办法

Allegro172版本无法低亮颜色的原因和解决办法 用Allegro版本做PCB设计的时候,高亮是使用非常频繁的功能,低亮已经高亮的对象也是使用较为频繁的。 在用172版本时会出现无法低亮的情况,如下图 使用Dehilight命令无法低亮器件,如何解决,具体操作步骤如下 点击Display选择De…

Python:每日一题之剪邮票(BFS全排列)

如【图1.jpg】, 有12张连在一起的12生肖的邮票。 现在你要从中剪下 5 张来&#xff0c;要求必须是连着的。 &#xff08;仅仅连接一个角不算相连&#xff09; 比如&#xff0c;【图2.jpg】&#xff0c;【图3.jpg】中&#xff0c;粉红色所示部分就是合格的剪取。 请你计算&…

redis的安装步骤及前台,后台redis服务启动

redis的安装步骤1. 官网下载安装包2. 使用Xftp将安装包传输到Linux的opt目录下3. 使用Xshell连接Linux主机进行redis的安装安装目录说明4. redis 服务启动的两种方式4.1 前台启动4.2 后台启动1. 官网下载安装包 首先&#xff0c;我们进入到redis的官网: https://redis.io/down…

代码随想录算法训练营第三十一天 | 贪心专题-理论基础,455.分发饼干,376. 摆动序列,53. 最大子序和

一、参考资料理论基础https://programmercarl.com/%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html 分发饼干https://programmercarl.com/0455.%E5%88%86%E5%8F%91%E9%A5%BC%E5%B9%B2.html 摆动序列https://programmercarl.com/0376.%E6%91%86…

数据库存储

RAID DSL &#xff1a; Domain Spesic Language 专用领域语言 单机存储 一切皆Key-Value 本地文件系统 一切皆文件 Ceph - 分布式存储 关系型数据库通用组件 Query Engine &#xff1a;解析query&#xff0c;生成查询计划Txn Manager &#xff1a;事务并发管理Lock Man…

知识汇总:Python办公自动化应该学习哪些内容

当前python自动化越来越受到欢迎&#xff0c;python一度成为了加班族的福音。还有大部分人想利用python自动化来简化工作&#xff0c;不知道从何处下手&#xff0c;所以&#xff0c;这里整理了一下python自动化过程中的各种办公场景以及需要用到的python知识点。 Excel办公自动…

【C++】类和对象(第二篇)

文章目录1. 类的6个默认成员函数2. 构造函数2.1 构造函数的引出2.2 构造函数的特性3. 析构函数3.1 析构函数的引出3.2 析构函数的特性4. 拷贝构造函数4.1 概念4.2 特性5.赋值运算符重载5.1 运算符重载概念注意练习5.2 赋值重载实现赋值重载的特性6. const成员函数7. 取地址及co…

传统图机器学习的特征工程

视频资料同济子豪兄中文精讲视频&#xff1a;节点特征工程&#xff1a;https://www.bilibili.com/video/BV1HK411175s连接特征工程&#xff1a;https://www.bilibili.com/video/BV1r3411m7sD全图特征工程&#xff1a;https://www.bilibili.com/video/BV14W4y1V7gg斯坦福原版视频…