二叉搜索树-----红黑树

✅<1>主页:我的代码爱吃辣
📃<2>知识讲解:数据结构——红黑树
☂️<3>开发环境:Visual Studio 2022
💬<4>前言:红黑树也是一颗二叉搜索树,其作为map,set的底层容器,具有非常好的搜索性能,仅仅通过控制颜色和位置就能达到一种,近似平衡的效果,大大减少了旋转的次数。

目录

一.红黑树的概念

二. 红黑树的性质

三.红黑树节点及其整体的定义

四.红黑树的插入操作

五.红黑树 find

六.析构函数

七.红黑树的验证

八. 红黑树与AVL树的比较


一.红黑树的概念

红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或
Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路
径会比其他路径长出俩倍,因而是接近平衡的。

二. 红黑树的性质

1. 每个结点不是红色就是黑色
2. 根节点是黑色的
3. 如果一个节点是红色的,则它的两个孩子结点是黑色的
4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均 包含相同数目的黑色结点
5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)NIL结点

思考:为什么满足上面的性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点个数的两倍?

答案:因为性质3限制了,一条路径上,不可能出现连续的两个红色结点。又因为性质4,每条路径上黑色结点数目相同,那么最短路径就一定是全是黑色结点的路径,最长路径一定是红黑交错的路径,因为根节点一定是黑色,那么最长路径上红黑结点树一定是相等的,所以最长路径最多是最短路径的两倍

三.红黑树节点及其整体的定义

//枚举
enum Color
{
	RED,//红色
	BLACK//黑色
};
template<class K,class V>
struct _RBTreeNode
{
	_RBTreeNode(pair<K,V> kv)
		:_kv(kv),
		_col(RED),//默认红色
		_left(nullptr),
		_right(nullptr),
		_parent(nullptr)
	{
	}

	pair<K, V> _kv;              //存储数值
	Color _col;                  //颜色
	_RBTreeNode<K, V>* _left;    //左孩子
	_RBTreeNode<K, V>* _right;   //右孩子
	_RBTreeNode<K, V>* _parent;  //父亲
};
#pragma once
#include<iostream>
using namespace std;


template<class K,class V>
class RBTRee
{
	typedef _RBTreeNode<K, V> Node;//结点
public:
	Node* find(const K key)
	{
		//....
	}
	bool insert(pair<K, V> kv)
	{
		//....
	}
	void Inorder()
	{
		//...
	}

	~RBTRee()
	{
		//...
	}

private:
	Node* _root = nullptr;//根节点
};

思考:在节点的定义中,为什么要将节点的默认颜色给成红色的?

答案:新创建的结点,妖色要么红色,要么黑色,除了颜色区别之外,就是在插入时对整个树的影响不同,如果插入的是黑色,会影响整颗树,所有路径上的黑色结点说就会不同,必然违反性质4。如果插入的是红色结点,仅仅是局部的影响,可能会影响性质3,一定不会影响性质4。

 四.红黑树的插入操作

红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:

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

bool insert(pair<K, V> kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->_col = BLACK;
			return true;
		}

		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (kv.first < cur->_kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (kv.first > cur->_kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return false;
			}
		}
        //找到了合适的位置
		cur = new Node(kv);
		if (kv.first < parent->_kv.first)
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}
		cur->_parent = parent;
        

        //....  
}

因为性质2,所以我们每一次插入数据都想根节点变成黑色。

2.检测新节点插入后,红黑树的性质是否造到破坏,若满足直接退出,否则对红黑树进行旋转着色处理。

因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何
性质,则不需要调整;但当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连
在一起的红色节点,此时需要对红黑树分情况来讨论:

约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点。

情况一: cur为红,p为红,g为黑,u存在且为红


解决方式:将 p , u 改为黑,g 改为红,然后把 g 当成 cur,继续向上调整。

情况二: cur为红,p为红,g为黑,u不存在/u存在且为黑

  • p为g的左孩子,cur为p的左孩子,则进行右单旋转;相反,
  • p、g变色--p变黑,g变红

情况三:cur为红,p为红,g为黑,u不存在/u存在且为黑

  • p为g的左孩子,cur为p的右孩子,则针对p做左单旋转;

  • p为g的左孩子,cur为p的左孩子,则进行右单旋转;相反,
  • p、g变色--p变黑,g变红

这里的cur的也有可能是新增的结点,如果是cur本身就是新增节点那么u结点就是不存在的,否则违反规则 4,也有可能是因为cur下面的结点变黑导致 cur 变红色。

代码:

    while (parent && parent->_col == RED)
	{
		Node* grandfather = parent->_parent;
		//           g(B)                     g(R)
		//     p(R)       u(R)  -->     p(B)       u(B)
		//c(R)                     c(R)
		if (grandfather->_left == parent)
		{
			Node* uncle = grandfather->_right;
			if (uncle && uncle->_col == RED)
			{
				parent->_col = BLACK;
				uncle->_col = BLACK;
				grandfather->_col = RED;

				//继续向上调整
				cur = grandfather;
				parent = cur->_parent;
			}
			else //u不存在/u存在且为黑,旋转+变色
			{
				//           g(B)                   p(R)
				//     p(R)       u(B)   -->  u(B)        g(B)
				//c(R)										     u(B)
				if (cur == parent->_left)
				{
					//右单旋
					RotateR(grandfather);
					parent->_col = BLACK;
					//cur->_col = RED;
					grandfather->_col = RED;
				}
				else
				{
					//            g(B)                   P(B)   
					//     p(R)         u(B)  --> c(R)          g(R)
					//         c(R)                                     u(B)
					// 左右双旋
					RotateL(parent);
					RotateR(grandfather);
					cur->_col = BLACK;
					grandfather->_col = RED;
				}
				break;
			}
		}
		else //grandfather->_right == parent,与上述情况相反
		{
			Node* uncle = grandfather->_left;
			if (uncle && uncle->_col == RED)
			{
				parent->_col = BLACK;
				uncle->_col = BLACK;
				grandfather->_col = RED;

				cur = grandfather;
				parent = cur->_parent;
			}
			else //u不存在/u存在且为黑,旋转+变色
			{
				if (cur == parent->_right)
				{
					//左单旋
					RotateL(grandfather);
					parent->_col = BLACK;
					grandfather->_col = RED;
				}
				else
				{
					// 右左双旋
					RotateR(parent);
					RotateL(grandfather);
					cur->_col = BLACK;
					grandfather->_col = RED;
				}
				break;
			}
		}
	}

 旋转:

    void RotateL(Node* parent)
	{
		Node* curR = parent->_right;
		Node* curRL = curR->_left;

		//调整结点,并且修改其父亲结点指针
		parent->_right = curRL;
		if (curRL)//可能为空
		{
			curRL->_parent = parent;
		}
		//在修改子树根节点之前,保存子树根节点的父亲
		Node* pparent = parent->_parent;
		//修改子树根节点
		curR->_left = parent;
		parent->_parent = curR;

		//子树根节点有可能是整棵树的根节点
		if (pparent == nullptr)
		{
			_root = curR;
			_root->_parent = nullptr;
		}
		else//子树根节点不是整棵树的根节点
		{
			//还要看子树是它父亲的左孩子还是右孩子
			if (pparent->_left == parent)
			{
				pparent->_left = curR;
			}
			else
			{
				pparent->_right = curR;
			}
			curR->_parent = pparent;
		}
	}

	void RotateR(Node* parent)
	{
		Node* curL = parent->_left;
		Node* curLR = curL->_right;

		parent->_left = curLR;
		if (curLR)
		{
			curLR->_parent = parent;
		}

		Node* pparent = parent->_parent;

		curL->_right = parent;
		parent->_parent = curL;

		if (parent == _root)
		{
			_root = curL;
			_root->_parent = nullptr;
		}
		else
		{
			if (pparent->_left == parent)
			{
				pparent->_left = curL;
			}
			else
			{
				pparent->_right = curL;

			}
			curL->_parent = pparent;
		}
	}

红黑树顺序插入:

 红黑树随机插入:

五.红黑树 find

根据二叉搜索树特性去查找:

    Node* find(const K key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (key < cur->_kv.first)
			{
				cur = cur->_left;
			}
			else if (key > cur->_kv.first)
			{
				cur = cur->_right;
			}
			else
			{
				return cur;
			}
		}
		return nullptr;
	}

六.析构函数

后续遍历析构树:

    ~RBTRee()
	{
		_Destrory(_root);
		_root = nullptr;
	}

	void _Destrory(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}

		_Destrory(root->_left);
		_Destrory(root->_right);
		delete root;
	}

七.红黑树的验证

红黑树的检测分为两步:

  1. 检测其是否满足二叉搜索树(中序遍历是否为有序序列)
  2. 检测其是否满足红黑树的性质

 其中是否满足搜索树我们只要对其中序遍历是否有序即可。

 完整代码:

#pragma once
#include<iostream>
using namespace std;

enum Color
{
	RED,
	BLACK
};
template<class K,class V>
struct _RBTreeNode
{
	_RBTreeNode(pair<K,V> kv)
		:_kv(kv),
		_col(RED),
		_left(nullptr),
		_right(nullptr),
		_parent(nullptr)
	{
	}

	pair<K, V> _kv;
	Color _col;
	_RBTreeNode<K, V>* _left;
	_RBTreeNode<K, V>* _right;
	_RBTreeNode<K, V>* _parent;
};

template<class K,class V>
class RBTRee
{
	typedef _RBTreeNode<K, V> Node;
public:
	Node* find(const K key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (key < cur->_kv.first)
			{
				cur = cur->_left;
			}
			else if (key > cur->_kv.first)
			{
				cur = cur->_right;
			}
			else
			{
				return cur;
			}
		}
		return nullptr;
	}
	bool insert(pair<K, V> kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->_col = BLACK;
			return true;
		}

		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (kv.first < cur->_kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (kv.first > cur->_kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return false;
			}
		}
		cur = new Node(kv);
		//找到了合适的位置

		if (kv.first < parent->_kv.first)
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}
		cur->_parent = parent;

		while ( parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent;
			//           g(B)                     g(R)
			//     p(R)       u(R)  -->     p(B)       u(B)
			//c(R)                     c(R)
			if ( grandfather->_left == parent)
			{
				Node* uncle = grandfather->_right;
				if (uncle && uncle->_col == RED)
				{
					parent->_col = BLACK;
					uncle->_col = BLACK;
					grandfather->_col = RED;

					//继续向上调整
					cur = grandfather;
					parent = cur->_parent;
				}
				else //u不存在/u存在且为黑,旋转+变色
				{
					//           g(B)                   p(R)
					//     p(R)       u(B)   -->  u(B)        g(B)
					//c(R)										     u(B)
					if (cur == parent->_left)
					{
						//右单旋
						RotateR(grandfather);
						parent->_col = BLACK;
						//cur->_col = RED;
						grandfather->_col = RED;
					}
					else
					{
						//            g(B)                   P(B)   
						//     p(R)         u(B)  --> c(R)          g(R)
						//         c(R)                                     u(B)
						// 左右双旋
						RotateL(parent);
						RotateR(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
			else //grandfather->_right == parent,与上述情况相反
			{
				Node* uncle = grandfather->_left;
				if (uncle && uncle->_col == RED)
				{
					parent->_col = BLACK;
					uncle->_col = BLACK;
					grandfather->_col = RED;

					cur = grandfather;
					parent = cur->_parent;
				}
				else //u不存在/u存在且为黑,旋转+变色
				{
					if (cur == parent->_right)
					{
						//左单旋
						RotateL(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						// 右左双旋
						RotateR(parent);
						RotateL(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
		}
		_root->_col = BLACK;
		return true;
	}
	void Inorder(vector<K> & v)
	{
		_inorder(_root,v);
		cout << endl;
	}

	~RBTRee()
	{
		_Destrory(_root);
		_root = nullptr;
	}

private:

	void _Destrory(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}

		_Destrory(root->_left);
		_Destrory(root->_right);
		delete root;
	}
    void _inorder(Node* root, vector<K>& v)
	{
		if (root == nullptr)
		{
			return;
		}
		_inorder(root->_left,v);
		v.push_back(root->_kv.first);
		_inorder(root->_right,v);
	}
	void RotateL(Node* parent)
	{
		Node* curR = parent->_right;
		Node* curRL = curR->_left;

		//调整结点,并且修改其父亲结点指针
		parent->_right = curRL;
		if (curRL)//可能为空
		{
			curRL->_parent = parent;
		}
		//在修改子树根节点之前,保存子树根节点的父亲
		Node* pparent = parent->_parent;
		//修改子树根节点
		curR->_left = parent;
		parent->_parent = curR;

		//子树根节点有可能是整棵树的根节点
		if (pparent == nullptr)
		{
			_root = curR;
			_root->_parent = nullptr;
		}
		else//子树根节点不是整棵树的根节点
		{
			//还要看子树是它父亲的左孩子还是右孩子
			if (pparent->_left == parent)
			{
				pparent->_left = curR;
			}
			else
			{
				pparent->_right = curR;
			}
			curR->_parent = pparent;
		}
	}

	void RotateR(Node* parent)
	{
		Node* curL = parent->_left;
		Node* curLR = curL->_right;

		parent->_left = curLR;
		if (curLR)
		{
			curLR->_parent = parent;
		}

		Node* pparent = parent->_parent;

		curL->_right = parent;
		parent->_parent = curL;

		if (parent == _root)
		{
			_root = curL;
			_root->_parent = nullptr;
		}
		else
		{
			if (pparent->_left == parent)
			{
				pparent->_left = curL;
			}
			else
			{
				pparent->_right = curL;

			}
			curL->_parent = pparent;
		}
	}

	Node* _root = nullptr;

};
	//传参时benchmark是-1,代表还没有基准值,当走完第一条路径时,
	//将第一条路径的黑色节点数作为基准值,后续路径走到null时,就与基准值比较。
	//blacknum记录路径上的黑色节点数
	bool _isRBTree(Node* root, int blacknum, int benchmark)
	{
		if (root == nullptr)
		{
			if (benchmark == -1)
			{
				benchmark = blacknum;
			}
			else
			{
				if (blacknum != benchmark)
				{
					cout << "black Node !=" << endl;
					return false;
				}
			}
			return true;
		}

		if (root->_col == BLACK)
		{
			blacknum++;
		}
        //判断时候出现两个连续的红色结点
		if (root->_col == RED && root->_parent && root->_parent->_col == RED)
		{
			cout << "red connect " << endl;
			return false;
		}

		return _isRBTree(root->_left, blacknum, benchmark) && _isRBTree(root->_right, blacknum, benchmark);
	}

main.cpp

#include<vector>
#include<cassert>
#include"RB_Tree.hpp"

int main()
{

	RBTRee<int, int> rb;
	int i = 100000;
	while(i--)
	{
		int num = rand() + i;
		rb.insert(make_pair(num,num));
	}
	vector<int> v;
	rb.Inorder(v);
	for (int i = 0; i < v.size() - 1; i++)
	{
		if (v[i] > v[i + 1])
		{
			assert(0);
		}
	}

	cout << rb.isRBTree() << endl;
	return 0;
}

 八. 红黑树与AVL树的比较

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

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

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

相关文章

【分析绘图】R语言实现一些常见的绘图

微生信-在线绘图网站 线性图 library(ggplot2)x <- rnorm(100, 14, 5) # rnorm(n, mean 0, sd 1) y <- x rnorm(100, 0, 1) ggplot(data NULL, aes(x x, y y)) # 开始绘图geom_point(color "darkred") # 添加点annotate("text",x 13,…

23款奔驰GLE450轿跑升级原厂外观暗夜套件,战斗感满满的

升级的方案基本都是替换原来车身部位的镀铬件&#xff0c;可能会有人问&#xff1a;“难道直接用改色膜贴黑不好吗&#xff1f;”如果是贴膜的话&#xff0c;第一个是颜色没有那么纯正&#xff0c;这些镀铬件贴黑的技术难度先抛开不说&#xff0c;即使贴上去了&#xff0c;那过…

原生小案例:如何使用HTML5 Canvas构建画板应用程序

使用HTML5 Canvas构建绘图应用是在Web浏览器中创建交互式和动态绘图体验的绝佳方式。HTML5 Canvas元素提供了一个绘图表面&#xff0c;允许您操作像素并以编程方式创建各种形状和图形。本文将为您提供使用HTML5 Canvas创建绘图应用的概述和指导。此外&#xff0c;它还将通过解释…

【MYSQL8.0从入门到精通】

文章目录 MySQL 8.0一.MySQL的多表操作1.外键约束&#xff08;一对多&#xff09;2.外键约束&#xff08;多对多&#xff09; MySQL 8.0 一.MySQL的多表操作 1.外键约束&#xff08;一对多&#xff09; 方式1 在创建表的同时创建外键约束 -- 1.创建主表 create table if no…

性能优化——分库分表

1、什么是分库分表 1.1、分表 将同一个库中的一张表&#xff08;比如SPU表&#xff09;按某种方式&#xff08;垂直拆分、水平拆分&#xff09;拆分成SPU1、SPU2、SPU3、SPU4…等若干张表&#xff0c;如下图所示&#xff1a; 1.2、分库 在表数据不变的情况下&#xff0c;对…

【锐捷】OSPF 多区域配置

【实验名称】 配置 OSPF 多区域。 【实验目的】 配置 OSPF 多区域&#xff0c;理解 OSPF 层次型网络的特点。 【背景描述】 本实验拓扑图中有 3 台路由器&#xff0c;路由器在区域 0 和区域 1 中&#xff0c;路由器 B 在区域 0 和区域 30&#xff0c; 路由器 C 在区域 30。 【需…

SQL 数据库

安装配置 【1】 MySQL安装配置教程&#xff08;超级详细、保姆级&#xff09; 【2】 MySQLNavicat安装配置教程&#xff08;超级详细、保姆级&#xff09; 学习资料 【戴师兄】SQL入门免费教程 刷题链接&#xff1a;https://share.mubu.com/doc/4BHMMbbvIMb 学习笔记&#xf…

vue3 pdf、word等文件下载

效果&#xff1a; <div class"byLawBox"><div class"titleBox">规章制度公示</div><div class"contentBox"><TableList:loading"byLawloading"ref"byLawtablistRef":hasImport"false"…

时序预测 | MATLAB实现SO-CNN-LSTM蛇群算法优化卷积长短期记忆神经网络时间序列预测

时序预测 | MATLAB实现SO-CNN-LSTM蛇群算法优化卷积长短期记忆神经网络时间序列预测 目录 时序预测 | MATLAB实现SO-CNN-LSTM蛇群算法优化卷积长短期记忆神经网络时间序列预测预测效果基本介绍程序设计学习总结参考资料 预测效果 基本介绍 时序预测 | MATLAB实现SO-CNN-LSTM蛇群…

清洁能源使用的社会发展意义

应用清洁能源是转变经济增加途径的有效手段&#xff0c;能够在减少污染物、降低企业经营成本的同时&#xff0c;提高企业经济效益和社会经济效益。 应用清洁能源是保护环境的最佳方式和必然选择&#xff0c;改变末端治理的现状&#xff0c;采取以预防为主的环境保护与发展理…

波奇学C++:stl的list模拟实现

list是双向带头链表。所以迭代器end()相当于哨兵卫的头。 list不支持和[]重载&#xff0c;原因在于list空间不是连续的&#xff0c;和[]的代价比较大。 访问第n个节点&#xff0c;只能用for循环&#xff0c;来实现 list<int> l; l.push_back(0); l.push_back(1); l.pu…

queue ide is not exists in YARN

报错内容: 2023-08-17 17:30:31.342 [ERROR] [BaseTaskScheduler-Thread-7 ] o.a.l.o.s.a.AsyncExecTaskRunnerImpl (79) [run] - Failed to execute task astJob_1_codeExec_1 org.apache.linkis.orchestrator.ecm.exception.ECMPluginErrorException: errCode:…

LeetCode--HOT100题(37)

目录 题目描述&#xff1a;104. 二叉树的最大深度&#xff08;简单&#xff09;题目接口解题思路代码 PS: 题目描述&#xff1a;104. 二叉树的最大深度&#xff08;简单&#xff09; 给定一个二叉树 root &#xff0c;返回其最大深度。 二叉树的 最大深度 是指从根节点到最远…

【Go 基础篇】Go语言中的defer关键字:延迟执行与资源管理

介绍 在Go语言中&#xff0c;defer 是一种用于延迟执行函数调用的关键字。它提供了一种简洁而强大的方式&#xff0c;用于在函数返回之前执行一些必要的清理操作或者释放资源。defer 的灵活性和易用性使得它在Go语言中广泛应用于资源管理、错误处理和代码结构优化等方面。&…

Redis笔记——(狂神说)待续

Nosql概述 为什么要用NoSql&#xff1f; 1、单机mysql的年代&#xff1a;90年代&#xff0c;网站访问量小&#xff0c;很多使用静态网页html写的&#xff0c;服务器没压力。 当时瓶颈是&#xff1a;1)数据量太大一个机器放不下。2)数据的索引(BTree)&#xff0c;一个机器内存也…

rabbitmq的优先级队列

在我们系统中有一个 订单催付 的场景&#xff0c;我们的客户在天猫下的订单 , 淘宝会及时将订单推送给我们&#xff0c;如果在用户设定的时间内未付款那么就会给用户推送一条短信提醒&#xff0c;很简单的一个功能对吧&#xff0c;但是&#xff0c;tianmao商家对我们来说&#…

数据工厂调研及结果展示

数据工厂 一、背景 在开发自测、测试迭代测试、产品验收的过程中&#xff0c;都需要各种各样的前置数据&#xff0c;大致分为如下几类&#xff1a; 账号&#xff08;实名、权益等级、注册等&#xff09; 货源&#xff08;优货、急走、相似、一手、普通货源等&#xff09; …

创建 github 项目,并自动化配置

一 新建项目 github 创建新项目&#xff0c;并自动化部署 二 github 到本地 三 自动化部署 配置docker 和 Jenkins文件 改 Jenkins 配置。 3.1 需要配置的文件 3.2 修改jenkinsfile 项目名称 如果要新项目&#xff0c;就修改里面的依赖和Jenkinsfile里面的project name 四 …

开源文库系统moredoc

什么是 moredoc &#xff1f; moredoc 中文名 魔豆文库&#xff0c;是基于 golang 开发的类似百度文库、新浪爱问文库的开源文库系统&#xff0c;支持 TXT、PDF、EPUB、MOBI、Office 等格式文档的在线预览与管理&#xff0c;为 dochub 文库(github, gitee &#xff09;的重构版…

⌈算法进阶⌋图论::拓扑排序(Topological Sorting)——快速理解到熟练运用

目录 一、原理 1. 引例&#xff1a;207.课程表 2. 应用场景 3. 代码思路 二、代码模板 三、练习 1、210.课程表Ⅱ&#x1f7e2; 2、2392.给定条件下构造举证&#x1f7e1; 3、310.最小高度树 &#x1f7e1; 一、原理 1. 引例&#xff1a;207.课程表 就如大学课程安排一样&…