C++从入门到起飞之——红黑树 全方位剖析!

🌈个人主页:秋风起,再归来~
🔥系列专栏:C++从入门到起飞          
🔖克心守己,律己则安

目录

1. 红⿊树的概念

2. 红⿊树的实现

2.1 构建整体框架

 2.2 红黑树的插入

 2.3 红黑树的验证

 2.4 红黑树的其他简单接口

3、红黑树完整源码

4、完结散花


1. 红⿊树的概念

红⿊树是⼀棵⼆叉搜索树,他的每个结点增加⼀个存储位来表⽰结点的颜⾊,可以是红⾊或者⿊⾊。 通过对任何⼀条从根到叶⼦的路径上各个结点的颜⾊进⾏约束,红⿊树确保没有⼀条路径会⽐其他路 径⻓出2倍,因⽽是接近平衡的。

>红⿊树的规则:

1. 每个结点不是红⾊就是⿊⾊

2. 根结点是⿊⾊的

3. 如果⼀个结点是红⾊的,则它的两个孩⼦结点必须是⿊⾊的,也就是说任意⼀条路径不会有连续的 红⾊结点。

4. 对于任意⼀个结点,从该结点到其所有NULL结点的简单路径上,均包含相同数量的⿊⾊结点

 说明:《算法导论》等书籍上补充了⼀条每个叶⼦结点(NIL)都是⿊⾊的规则。他这⾥所指的叶⼦结点 不是传统的意义上的叶⼦结点,⽽是我们说的空结点,有些书籍上也把NIL叫做外部结点。NIL是为了 ⽅便准确的标识出所有路径,《算法导论》在后续讲解实现的细节中也忽略了NIL结点,所以我们知道 ⼀下这个概念即可。

2. 红⿊树的实现

2.1 构建整体框架

枚举类型定义红黑色:

//枚举类型定义颜色
enum Colour
{RED,BLACK
};

红黑树每个节点的结构: 

//节点结构(默认存储pair类型的key/val结构)
template<class K, class V>
struct RBTreeNode
{RBTreeNode(const pair<K, V>& kv):_kv(kv), _parent(nullptr), _left(nullptr), _right(nullptr),_col(RED){}pair<K, V> _kv;RBTreeNode* _parent;RBTreeNode* _left;RBTreeNode* _right;Colour _col;//初始化为红色
};

红黑树整体结构

//红黑树
template<class K,class V>
class RBTree
{typedef RBTreeNode Node;
public://各种接口的实现private:Node* _root=nullptr;
};

 2.2 红黑树的插入

1、红黑树是特殊的二叉搜索数,所以我们进行插入时,还是先按照二叉搜索数的规则进行插入!

//如果树为空,在根插入并且颜色为黑色
if (_root == nullptr)
{_root = new Node(kv);_root->_col = BLACK;return true;
}
//树不为空按搜索树规则先进行插入
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{if (kv.first < cur->_kv.first)//小往左走{parent = cur;cur = parent->_left;}else if (kv.first > cur->_kv.first)//大往右走{parent = cur;cur = parent->_right;}else{return false;//不支持相同元素的插入}
}
cur = new Node(kv);
cur->_col = RED;
if (kv.first < parent->_kv.first)//K小插入在左边parent->_left = cur;
else//K大插入在右边parent->_right = cur;
cur->_parent = parent;

2、插入成功后,我们就要维护红黑树的规则了。 

>如果插入节点的父亲是黑色,那插入后依然符合红黑树的规则,我们不用处理。

>如果插入节点的父亲是红色,那么此时就有连续的红色节点,我们就要进行处理。

        a、插入节点的叔叔存在且为红,我们只需要进行变色。

变色原理:因为parent的颜色为红,所以g的颜色一定为黑。插入前从g开始的路径的黑色节点的数量相同,要解决连续红色节点的问题,我们必然要把parent变黑色。但从g到cur路径的黑色节点多了一个,所以我们把g变红,在把u变黑就完美的解决了问题!

不过,g变红后,又可能会引起祖先节点的连续红色节点问题,所以我们还要继续向上维护红黑树的规则!

变色代码实现

//插入后进行维护红黑树规则的逻辑
//parent存在且为红
while (parent&& parent->_col = RED)
{Node* grandfather = parent->_parent;//p在g的右边if (parent == grandfather->_right){//g//u		pNode* uncle = grandfather->_left;if (uncle&& uncle->_col = RED)//uncle存在且为红{//变色处理uncle->_col = parent->_col = BLACK;grandfather->_col = RED;//更新cur继续向上处理cur = grandfather;parent = cur->_parent;}else//uncle不存在或者存在且为黑{}}else//p在g的左边{//g//p		uNode* uncle = grandfather->_left;if (uncle&& uncle->_col = RED)//uncle存在且为红{//变色处理uncle->_col = parent->_col = BLACK;grandfather->_col = RED;//更新cur继续向上处理cur = grandfather;parent = cur->_parent;}else//uncle不存在或者存在且为黑{}}
}

        b、如果uncle不存在或者uncle存在且为黑,这时我们就要进行旋转加变色处理。

单旋+变色:

>如果uncle不存在,说明c一定是新增节点,如果c是变色后的节点,那么它在变色前一定是黑色,而从g开始的路径到c就多一个黑色节点!

 >如果uncle存在且为黑,说明c一定是变色后的节点,如果c是新增的节点,那么从g开始的路径到u就多一个黑色节点!

 变色原理:我们根据c、p、g的位置来选择合理的旋转逻辑,然后再把p变黑,g变红即可解决问题!

双旋+变色:

c为红,p为红,g为⿊,u不存在或者u存在且为⿊,u不存在,则c⼀定是新增结点,u存在且为⿊,则 c⼀定不是新增,c之前是⿊⾊的,是在c的⼦树中插⼊,符合情况1,变⾊将c从⿊⾊变成红⾊,更新上 来的。

分析:p必须变⿊,才能解决,连续红⾊结点的问题,u不存在或者是⿊⾊的,这⾥单纯的变⾊⽆法解 决问题,需要旋转+变⾊。

如果p是g的左,c是p的右,那么先以p为旋转点进⾏左单旋,再以g为旋转点进⾏右单旋,再把c变 ⿊,g变红即可。c变成课这颗树新的根,这样⼦树⿊⾊结点的数量不变,没有连续的红⾊结点了,且 不需要往上更新,因为c的⽗亲是⿊⾊还是红⾊或者空都不违反规则。

如果p是g的右,c是p的左,那么先以p为旋转点进⾏右单旋,再以g为旋转点进⾏左单旋,再把c变 ⿊,g变红即可。c变成课这颗树新的根,这样⼦树⿊⾊结点的数量不变,没有连续的红⾊结点了,且 不需要往上更新,因为c的⽗亲是⿊⾊还是红⾊或者空都不违反规则。

 插入完整代码:

bool insert(const pair<K, V>& kv)
{//如果树为空,在根插入并且颜色为黑色if (_root == nullptr){_root = new Node(kv);_root->_col = BLACK;return true;}//树不为空按搜索树规则先进行插入Node* parent = nullptr;Node* cur = _root;while (cur){if (kv.first < cur->_kv.first)//小往左走{parent = cur;cur = parent->_left;}else if (kv.first > cur->_kv.first)//大往右走{parent = cur;cur = parent->_right;}else{return false;//不支持相同元素的插入}}cur = new Node(kv);cur->_col = RED;if (kv.first < parent->_kv.first)//K小插入在左边parent->_left = cur;else//K大插入在右边parent->_right = cur;cur->_parent = parent;//插入后进行维护红黑树规则的逻辑//parent存在且为红while (parent&& parent->_col == RED){Node* grandfather = parent->_parent;//p在g的右边if (parent == grandfather->_right){//g//u		pNode* uncle = grandfather->_left;if (uncle&& uncle->_col == RED)//uncle存在且为红{//变色处理uncle->_col = parent->_col = BLACK;grandfather->_col = RED;//更新cur继续向上处理cur = grandfather;parent = cur->_parent;}else//uncle不存在或者存在且为黑{if (cur == parent->_right){//g//u		p//		   c//以g为旋转点进行左单旋RotateL(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else{//g//u		p//	  c//进行右左双旋RotateR(parent);RotateL(grandfather);cur->_col = BLACK;grandfather->_col = RED;}//旋转+变色后链接祖先节点的节点为黑,必然不会发生连续红色节点的情况直接break;break;}}else//p在g的左边{//g//p		uNode* uncle = grandfather->_left;if (uncle&& uncle->_col == RED)//uncle存在且为红{//变色处理uncle->_col = parent->_col = BLACK;grandfather->_col = RED;//更新cur继续向上处理cur = grandfather;parent = cur->_parent;}else//uncle不存在或者存在且为黑{if (cur == parent->_left){//g//p		u//c//以g为旋转点进行右单旋RotateR(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else{//g//p		u//		c//进行左右双旋RotateL(parent);RotateR(grandfather);cur->_col = BLACK;grandfather->_col = RED;}//旋转+变色后链接祖先节点的节点为黑,必然不会发生连续红色节点的情况直接break;break;}}}//如果持续更新变色到根_root->_col = BLACK;return true;
}

 2.3 红黑树的验证

>检查每条路径的黑色节点是否相等,是否有连续的红色节点

//检查每条路径的黑色节点是否相等,是否有连续的红色节点
bool Check(Node* root, int blackNum, const int refNum)
{if (root == nullptr){// 前序遍历⾛到空时,意味着⼀条路径⾛完了 //cout << blackNum << endl;if (refNum != blackNum){cout << "存在黑色结点的数量不相等的路径" << endl;return false;}return true;}// 检查孩⼦不太⽅便,因为孩⼦有两个,且不⼀定存在,反过来检查⽗亲就⽅便多了 if (root->_col == RED && root->_parent->_col == RED){cout << root->_kv.first << "存在连续的红色节点" << endl;return false;}if (root->_col == BLACK){blackNum++;}return Check(root->_left, blackNum, refNum)&& Check(root->_right, blackNum, refNum);
}

>检查平衡 

//检查平衡
bool IsBalance()
{if (_root == nullptr)return true;if (_root->_col == RED)return false;// 参考值 int refNum = 0;Node* cur = _root;while (cur){if (cur->_col == BLACK){++refNum;}cur = cur->_left;}return Check(_root, 0, refNum);
}

 >插入一万个随机数看看是否平衡

void testInert()
{const int N = 10000;RBTree<int, int> t;vector<int> v;srand((unsigned int)time(nullptr));for (int i = 0; i < N; i++){v.push_back(rand() + i);}for (auto e : v){t.insert({ e,e });}cout << t.IsBalance() << endl;
}

 2.4 红黑树的其他简单接口

//默认构造
RBTree() = default;
//拷贝构造
RBTree(const RBTree<K,V>& rbt)
{_root=_copy(rbt._root);
}
// 赋值重载
RBTree<K, V>& operator=(RBTree<K, V> tmp)
{std::swap(_root, tmp._root);return *this;
}
//中序遍历
void InOrder()
{_InOrder(_root);
}
//二叉树的析构
~RBTree()
{_Destroy(_root);
}
private:
//递归拷贝
Node* _copy(Node* root)
{if (root == nullptr)return nullptr;Node* newNode = new Node(root->_kv);newNode->_left = _copy(root->_left);newNode->_right = _copy(root->_right);return newNode;
}
//中序遍历
void _InOrder(Node* root)
{if (root == nullptr)return;_InOrder(root->_left);cout << "<" << root->_kv.first << "," << root->_kv.second << ">" << endl;_InOrder(root->_right);
}
//二叉树的销毁
void _Destroy(Node* root)
{if (root == nullptr)return;_Destroy(root->_left);_Destroy(root->_right);delete root;
}

3、红黑树完整源码

 

#pragma once
#include<iostream>
using namespace std;
//枚举类型定义颜色
enum Colour
{RED,BLACK
};
//节点结构(默认存储pair类型的key/val结构)
template<class K, class V>
struct RBTreeNode
{RBTreeNode(const pair<K, V>& kv):_kv(kv), _parent(nullptr), _left(nullptr), _right(nullptr),_col(RED){}pair<K, V> _kv;RBTreeNode* _parent;RBTreeNode* _left;RBTreeNode* _right;Colour _col;//初始化为红色
};//红黑树
template<class K,class V>
class RBTree
{typedef RBTreeNode<K,V> Node;
public://默认构造RBTree() = default;//拷贝构造RBTree(const RBTree<K,V>& rbt){_root=_copy(rbt._root);}// 赋值重载RBTree<K, V>& operator=(RBTree<K, V> tmp){std::swap(_root, tmp._root);return *this;}//中序遍历void InOrder(){_InOrder(_root);}//二叉树的析构~RBTree(){_Destroy(_root);}//红黑树的查找Node* Find(const K& key){Node* cur = _root;while (cur){if (cur->_kv.first < key){cur = cur->_right;}else if (cur->_kv.first > key){cur = cur->_left;}else{return cur;}}return nullptr;}//红黑树的插入bool insert(const pair<K, V>& kv){//如果树为空,在根插入并且颜色为黑色if (_root == nullptr){_root = new Node(kv);_root->_col = BLACK;return true;}//树不为空按搜索树规则先进行插入Node* parent = nullptr;Node* cur = _root;while (cur){if (kv.first < cur->_kv.first)//小往左走{parent = cur;cur = parent->_left;}else if (kv.first > cur->_kv.first)//大往右走{parent = cur;cur = parent->_right;}else{return false;//不支持相同元素的插入}}cur = new Node(kv);cur->_col = RED;if (kv.first < parent->_kv.first)//K小插入在左边parent->_left = cur;else//K大插入在右边parent->_right = cur;cur->_parent = parent;//插入后进行维护红黑树规则的逻辑//parent存在且为红while (parent&& parent->_col == RED){Node* grandfather = parent->_parent;//p在g的右边if (parent == grandfather->_right){//g//u		pNode* uncle = grandfather->_left;if (uncle&& uncle->_col == RED)//uncle存在且为红{//变色处理uncle->_col = parent->_col = BLACK;grandfather->_col = RED;//更新cur继续向上处理cur = grandfather;parent = cur->_parent;}else//uncle不存在或者存在且为黑{if (cur == parent->_right){//g//u		p//		   c//以g为旋转点进行左单旋RotateL(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else{//g//u		p//	  c//进行右左双旋RotateR(parent);RotateL(grandfather);cur->_col = BLACK;grandfather->_col = RED;}//旋转+变色后链接祖先节点的节点为黑,必然不会发生连续红色节点的情况直接break;break;}}else//p在g的左边{//g//p		uNode* uncle = grandfather->_right;if (uncle&& uncle->_col == RED)//uncle存在且为红{//变色处理uncle->_col = parent->_col = BLACK;grandfather->_col = RED;//更新cur继续向上处理cur = grandfather;parent = cur->_parent;}else//uncle不存在或者存在且为黑{if (cur == parent->_left){//g//p		u//c//以g为旋转点进行右单旋RotateR(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else{//g//p		u//		c//进行左右双旋RotateL(parent);RotateR(grandfather);cur->_col = BLACK;grandfather->_col = RED;}//旋转+变色后链接祖先节点的节点为黑,必然不会发生连续红色节点的情况直接break;break;}}}//如果持续更新变色到根_root->_col = BLACK;return true;}//检查平衡bool IsBalance(){if (_root == nullptr)return true;if (_root->_col == RED)return false;// 参考值 int refNum = 0;Node* cur = _root;while (cur){if (cur->_col == BLACK){++refNum;}cur = cur->_left;}return Check(_root, 0, refNum);}private://右单旋void RotateR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;Node* pParent = parent->_parent;parent->_left = subLR;if (subLR)//如果不为空subLR->_parent = parent;subL->_right = parent;parent->_parent = subL;if (pParent == nullptr){_root = subL;subL->_parent = nullptr;}else{if (pParent->_left == parent){pParent->_left = subL;}else{pParent->_right = subL;}subL->_parent = pParent;}}//左单旋void RotateL(Node* parent){Node* pParent = parent->_parent;Node* subR = parent->_right;Node* subRL = subR->_left;subR->_left = parent;parent->_parent = subR;parent->_right = subRL;if (subRL)subRL->_parent = parent;if (pParent == nullptr){_root = subR;subR->_parent = nullptr;}else{if (pParent->_left == parent){pParent->_left = subR;}else{pParent->_right = subR;}subR->_parent = pParent;}}//检查每条路径的黑色节点是否相等,是否有连续的红色节点bool Check(Node* root, int blackNum, const int refNum){if (root == nullptr){// 前序遍历⾛到空时,意味着⼀条路径⾛完了 //cout << blackNum << endl;if (refNum != blackNum){cout << "存在黑色结点的数量不相等的路径" << endl;return false;}return true;}// 检查孩⼦不太⽅便,因为孩⼦有两个,且不⼀定存在,反过来检查⽗亲就⽅便多了 if (root->_col == RED && root->_parent->_col == RED){cout << root->_kv.first << "存在连续的红色节点" << endl;return false;}if (root->_col == BLACK){blackNum++;}return Check(root->_left, blackNum, refNum)&& Check(root->_right, blackNum, refNum);}//递归拷贝Node* _copy(Node* root){if (root == nullptr)return nullptr;Node* newNode = new Node(root->_kv);newNode->_left = _copy(root->_left);newNode->_right = _copy(root->_right);return newNode;}//中序遍历void _InOrder(Node* root){if (root == nullptr)return;_InOrder(root->_left);cout << "<" << root->_kv.first << "," << root->_kv.second << ">" << endl;_InOrder(root->_right);}//二叉树的销毁void _Destroy(Node* root){if (root == nullptr)return;_Destroy(root->_left);_Destroy(root->_right);delete root;}private:Node* _root=nullptr;
};

4、完结散花

好了,这期的分享到这里就结束了~

如果这篇博客对你有帮助的话,可以用你们的小手指点一个免费的赞并收藏起来哟~

如果期待博主下期内容的话,可以点点关注,避免找不到我了呢~

我们下期不见不散~~

​​​​

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

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

相关文章

数据库、数据仓库、数据湖和数据中台有什么区别

很多企业在面对数据存储和管理时不知道如何选择合适的方式&#xff0c;数据库、数据仓库、数据湖和数据中台&#xff0c;这些方式都是什么&#xff1f;有什么样的区别&#xff1f;企业根据其业务类型该选择哪一种&#xff1f;本文就针对这些问题&#xff0c;来探讨下这些方式都…

ASP.NET Core 8.0 中使用 Hangfire 调度 API

在这篇博文中&#xff0c;我们将引导您完成将 Hangfire 集成到 ASP.NET Core NET Core 项目中以安排 API 每天运行的步骤。Hangfire 是一个功能强大的库&#xff0c;可简化 .NET 应用程序中的后台作业处理&#xff0c;使其成为调度任务的绝佳选择。继续阅读以了解如何设置 Hang…

山西农业大学20241025

06-VUE 一. 生命周期1. 概念2. 生命周期的钩子函数 二. 工程化开发和脚手架1. 开发vue的两种方式2. 脚手架 Vue CLI3. 使用步骤4 . 项目目录介绍4.1 项目目录4.2 总结 一. 生命周期 1. 概念 VUE生命周期: 就是vue实例从创建到销毁的整个 生命周期经历了四个阶段: ①创建 ②挂载…

Clickhouse 笔记(一) 单机版安装并将clickhouse-server定义成服务

ClickHouse 是一个高性能的列式数据库管理系统&#xff08;DBMS&#xff09;&#xff0c;主要用于在线分析处理&#xff08;OLAP&#xff09;场景。它由俄罗斯搜索引擎公司 Yandex 开发&#xff0c;并在 2016 年开源。ClickHouse 以其卓越的查询性能和灵活的扩展性而闻名&#…

2-133 基于matlab的粒子群算法PSO优化BP神经网络

基于matlab的粒子群算法PSO优化BP神经网络&#xff0c;BP神经网络算法采用梯度下降算法&#xff0c;以输出误差平方最小为目标&#xff0c;采用误差反向传播&#xff0c;训练网络节点权值和偏置值&#xff0c;得到训练模型。BP神经网络的结构(层数、每层节点个数)较复杂时&…

【linux网络编程】| 网络基础 | 解析IP与Mac地址的区别

前言&#xff1a;本节内容讲解一些网络基础相关的知识点&#xff0c; 不涉及网络代码&#xff01;同样的本节内容是作为前一篇的补充知识点&#xff0c; 前一篇文章地址&#xff1a;【linux网络编程】 | 网络基础Ⅰ| 认识网络-CSDN博客&#xff0c;本篇文章内容较少&#xff0c…

AnaTraf | 全面掌握网络健康状态:全流量的分布式网络性能监测系统

AnaTraf 网络性能监控系统NPM | 全流量回溯分析 | 网络故障排除工具AnaTraf网络流量分析仪是一款基于全流量&#xff0c;能够实时监控网络流量和历史流量回溯分析的网络性能监控与诊断系统&#xff08;NPMD&#xff09;。通过对网络各个关键节点的监测&#xff0c;收集网络性能…

内置数据类型、变量名、字符串、数字及其运算、数字的处理、类型转换

内置数据类型 python中的内置数据类型包括&#xff1a;整数、浮点数、布尔类型&#xff08;以大写字母开头&#xff09;、字符串 变量名 命名变量要见名知意&#xff0c;确保变量名称具有描述性和意义&#xff0c;这样可以使得代码更容易维护&#xff0c;使用_可以使得变量名…

2024.7最新子比主题zibll7.9.2开心版源码+授权教程

授权教程&#xff1a; 1.进入宝塔搭建一个站点 绑定 api.zibll.com 域名 并上传 index.php 文件 2.设置伪静态 3.开启SSL证书&#xff0c;找一个能用的域名证书&#xff0c;将密钥(KEY)和证书(PEM格式)复制进去即可 4.在宝塔文件地址栏中输入 /etc 找到 hosts文件并打开&a…

[Linux][进程间通信] 命名管道

命名管道是一种进程间通信的方式,底层原理与匿名管道极为相似,本质是通过在磁盘上新建一个特殊的文件,然后通过这个文件来进行通信 指令: mkfifo [文件名/路径] 该指令用于创建一个命名管道,可以看到文件的类型是p p 类型 命名管道文件 p文件大小恒为0 可通过echo和cat向其…

JavaEE----多线程(四)----阻塞队列的介绍和初步实现

文章目录 1.阻塞队列1.1作用一&#xff1a;解耦合1.2作用二&#xff1a;削峰填谷1.3系统里面的阻塞队列的使用1.4实现普通队列1.5在普通队列的基础上面实现阻塞队列1.6设计优化1.7实现初步的生产者消费者模型 1.阻塞队列 阻塞队列的最大意义&#xff1a;就是实现“生产者消费者…

SQL 干货 | SQL 半连接

大多数数据库开发人员和管理员都熟悉标准的内、外、左和右连接类型。虽然可以使用 ANSI SQL 编写这些连接类型&#xff0c;但还有一些连接类型是基于关系代数运算符的&#xff0c;在 SQL 中没有语法表示。今天我们将学习一种这样的连接类型&#xff1a;半连接&#xff08;Semi …

后台管理员登录实现--系统篇

我的小系统后台原来就有一个上传图片的功能还夹带个删除图片的功能&#xff0c;还嵌到了一个菜单里面。之前效果如下 那么现在为了加大安全力度&#xff0c;想增加一个登录页面。通过登录再到这个页面。看着貌似很简单&#xff0c;但是听我细细说来&#xff0c;要新增些什么东西…

C#第四讲:C#语言基本元素概览,初识类型、变量与方法,算法简介

一、构成C#语言的基本元素 1、标识符 允许将下划线用作初始字符(这是C编程语言的传统)。 允许在标识符中使用 Unicode 转义序列&#xff0c;以及允许“”字符作为前缀以使关键字能够用作标识符。 &#xff08;1&#xff09;命名方法 变量名&#xff1a;用驼峰法。&#xff…

【SQL实验】表的更新和简单查询

完整代码在文章末尾 在上次实验创建的educ数据库基础上&#xff0c;用SQL语句为student表、course表和sc表中添加以下记录 【SQL实验】数据库、表、模式的SQL语句操作_创建一个名为educ数据库,要求如下: (下面三个表中属性的数据类型需要自己设计合适-CSDN博客在这篇博文中已经…

安全见闻---清风

注&#xff1a;本文章源于泷羽SEC&#xff0c;如有侵权请联系我&#xff0c;违规必删 学习请认准泷羽SEC学习视频:https://space.bilibili.com/350329294 安全见闻1 泷哥语录&#xff1a;安全领域什么都有&#xff0c;不要被表象所迷惑&#xff0c;无论技术也好还是其他方面…

[jeecg-boot] vue3 版本 nvm 下载node版本

安装pnpm 使用cnpm 进行下载依赖

JavaWeb 23.一文速通npm的配置和使用

目录 一、npm的介绍 二、npm的安装和配置 1.安装 &#xff1a; 2.配置依赖下载使用阿里镜像 3. 配置全局依赖下载后存储位置 4.升级npm版本 5.环境变量配置 三、npm常用命令 1.项目初始化 npm.init npm init -y 2.安装依赖文件 3. 升级依赖 4.卸载依赖 5.查看依赖 查看项目…

深入浅出 Vue3 nextTick

程序员节日快乐~ #1024程序员节 | 征文# nextTick 概念 当你在 Vue 的响应式数据模型中对数据进行修改时&#xff0c;这些变化并不会立即同步到 DOM 上_&#xff0c;而是会在当前的微任务队列&#xff08;microtask queue&#xff09;执行完毕后进行批量更新。这种机制被称为…

内网穿透:如何借助Cloudflare连接没有公网的电脑的远程桌面(RDP)

内网穿透&#xff1a;如何借助Cloudflare连接没有公网的电脑的远程桌面(RDP)-含详细原理配置说明介绍 前言 远程桌面协议(RDP, Remote Desktop Protocol)可用于远程桌面连接&#xff0c;Windows系统&#xff08;家庭版除外&#xff09;也是支持这种协议的&#xff0c;无需安装…