搜索树的一种,能保证所有操作在O(log n)的时间内完成,且每次操作全树的拓扑结构更新仅涉及常数个节点的更新。

AVL树能够保证最坏情况下的单次操作时间,但是需在节点中嵌入平衡因子等表示,同时删除操作之后的重平衡操作可能要执行O(log n)次旋转,导致整棵树拓扑结构变化较大;而伸展树虽然实现方便、无需修改节点结构、分摊复杂度低,但是最坏情况下单次操作需要O(n)的时间,不适用于对稳定性要求极高的场所。因而我们引进红黑树。

红黑树的定义

在二叉搜索树BST的基础上为每个节点新增颜色的定义,每个节点的颜色非黑即红

为了加强理解红黑树,对于任意一颗红黑树,统一引入外部节点(外部节点也有颜色,默认为黑),如下图所示:



红黑树的定义如下:

  1. 树根和外部节点始终为黑色
  2. 红色节点的孩子必为黑色节点
  3. 从任意内部节点到其能到达的后代外部节点的路径中黑色节点数目相同

由性质(1)和性质(2)可知红色节点一定是内部节点且其父亲和左右孩子皆存在(左右孩子可能是外部节点)且必为黑色。

由性质(3)引出黑深度黑高度的定义:

黑深度:从根节点出发到当前节点沿途经过的黑节点的个数,根节点黑深度为0,由性质(3)可知任意外部节点的黑深度相同。

黑高度:从当前节点通往其任意后代外部节点的沿途中经过的黑节点个数,所有外部节点黑高度为0,根节点的黑高度即为全树的黑高度,数值上与外部节点黑深度相等。

红黑树的定义看似费解和别扭,实际上任意一颗红黑树都可以等效地转换为对应的4阶B- 树,二者存在极其密切的联系,在此不做细述。

节点与树的定义

以下是红黑树定义

/*****************************************************************/
//宏定义
#define IsRoot(x) ( !((x)->pa) )
#define IsLChild(x) ( !(IsRoot(x) ) && (x)==(x)->pa->lc)
#define IsRChild(x) ( !(IsRoot(x) ) && (x)==(x)->pa->rc)
#define HasLChild(x) ((x)->lc )
#define HasRChild(x) ((x)->rc )
#define HasChild(x) (HasLChild(x) || HasRChild(x))
#define HasBothChild(x) (HasRChild(x) && HasLChild(x) )
#define IsLeaf(x) (! HasChild(x) ) #define RBNodePos(x) RBNode<x> *
typedef enum{RED,BLACK} RBColor;//节点颜色
#define IsBlack(x) ((!x) || (x)->color == BLACK)
#define IsRed(x) (! IsBlack(x)) //非黑即红
/*****************************************************************/
//RBNode 定义
template<typename T>
struct RBNode {
public:
T key;
RBColor color;
RBNodePos(T) pa;//pa -- parent
RBNodePos(T) lc;//lc -- left child
RBNodePos(T) rc;//rc -- right child
//构造函数
RBNode() :color(RED),pa(NULL), lc(NULL), rc(NULL) {}
RBNode(T elem, RBColor c=RED,RBNodePos(T) pa = NULL, RBNodePos(T) lc = NULL, RBNodePos(T) rc = NULL) :
key(elem), color(c), pa(pa), lc(lc), rc(rc) { }
};
/*****************************************************************/
//RBTree 定义
template<typename T>
class RBTree {
private:
RBNodePos(T) root; //树根
RBNodePos(T) hot; //一个内置变量,方便类内其他函数使用
public:
//构造函数和析构函数
RBTree() :root(NULL) {}
~RBTree() {} //遍历函数
void preOrder(); //前序遍历
void inOrder(); //中序遍历
void postOrder(); //后序遍历
//操作函数
RBNodePos(T) search(const T &key); //查找函数,若存在值为key的节点返回相应节点,若不存在则返回NULL,基于searchIn函数实现
RBNodePos(T) insert(const T &key); //插入值为key的节点
bool remove(const T &key); //移除值为key的节点 private:
void preOrder(RBNodePos(T) &x); //以x节点为root进行前序遍历
void inOrder(RBNodePos(T) &x); //以x节点为root进行中序遍历
void postOrder(RBNodePos(T) &x); //以x节点为root进行后序遍历
RBNodePos(T) adjust(RBNodePos(T) &x); //调整x、p = x->pa,和g = p->pa三个节点的结构,返回调整后得到的局部子树的树根
RBNodePos(T) searchIn(const T & key, RBNodePos(T) &x); //以x节点为root查找值为key的节点,hot为返回节点的父节点
void solveDoubleRed(RBNodePos(T) & x); //修正双红问题
void solveDoubleBlack(RBNodePos(T) & x); //修正双黑问题
};
/*****************************************************************/

旋转操作

红黑树的插入和删除操作涉及到旋转操作,如下图所示,利用adjust(RBNodePos(T) & x)函数可以实现旋转操作:(即将中序遍历下的中间那个节点提高作为树根)

情况1(另一情况对称)



情况2(另一情况对称)

通过使用adjust(x)可以调整树形


//lc作为p的左子接入,lc可能为空
template<typename NodePos>
inline void attachAsLChild(NodePos p, NodePos lc) {
p->lc = lc;
if (lc) lc->pa = p;
}
//rc作为p的右子子接入,lc可能为空
template<typename NodePos>
inline void attachAsRChild(NodePos p, NodePos rc) {
p->rc = rc;
if (rc) rc->pa = p;
} //调整x、p = x->pa,和g = p->pa三个节点的结构,返回调整后得到的局部子树的树根
template<typename T>
inline RBNodePos(T) RBTree<T>::adjust(RBNodePos(T)& x){
RBNodePos(T) p = x->pa;
RBNodePos(T) g = p->pa;
RBNodePos(T) r = NULL;//r为调整后局部子树的树根
if (IsLChild(p) && IsLChild(x)) { //情况2
attachAsLChild(g, p->rc); attachAsRChild(p, g); r = p;
}
else if (IsRChild(p) && IsRChild(x)) { //情况2对称
attachAsRChild(g, p->lc); attachAsLChild(p, g); r = p;
}
else if (IsLChild(p) && IsRChild(x)) { //情况1
attachAsRChild(p, x->lc); attachAsLChild(g, x->rc);
attachAsLChild(x, p); attachAsRChild(x, g);
r = x;
}
else if (IsRChild(p) && IsLChild(x)) { //情况1对称
attachAsLChild(p, x->rc); attachAsRChild(g, x->lc);
attachAsLChild(x, g); attachAsRChild(x, p);
r = x;
}
return r;
}

插入操作

执行插入操作与普通的二叉搜索树BST的插入操作基本一致,不难分析,插入得到的新节点x的颜色应该为红色(插入新节点时,也会为新节点虚构两个不存在的黑色的外部节点),如果插入得到的新节点x的父亲p是黑色,那么全树依旧满足RBTree的定义,但是如果p、x的颜色均为红色,就不满足RBTree定义(2),我们就遇到了双红问题,要利用solveDoubleRed(RBNodePos(T) & x)这个函数解决,以下分析可能遇到的各种情况:

情况1:p的兄弟u为黑色

(根据定义,p的父亲g必定存在且为黑色,且根据下图四棵子树T0、T1、T2、T3的黑高度一样,子树可能为外部节点(即不存在),但是仍满足黑高度的定义 ) 我们只需要利用上面提到的旋转操作调整局部子树结构,并将局部子树树根染黑,其左右孩子染红,就能够保证局部子树满足RBTree的定义,且局部子树的黑高度不变,使得全树依旧满足RBTree的定义。

情况2: p的兄弟u为红色

(图中T0、T1、T2、T3、T4五颗子树黑高度相等)这时只需要简单的将p、g、u三个节点的颜色“取反”即可,如下图所示,局部子树满足TRBTree定义且局部子树黑高度不变,但是注意,如果g的父亲也为红色就会引发新的双红问题,要递归执行。

插入操作性能分析

对于情况1,只需做一轮修正,做一次重构和重染色;对于情况2,做一次重染色,然后递归执行。由于修正节点的高度会严格递增,故可知最多执行O(log n)次solveDoubleRed操作,且只要做一次重构就会停止递归(执行情况1),而查找操作花费时间为O(log n),故插入操作可在O(log n)内完成。

代码实现

//插入值为key的节点
template<typename T>
RBNodePos(T) RBTree<T>::insert(const T & key)
{
RBNodePos(T) x = search(key);
if (x) return x; //已经存在值为key的节点
//否则确认key不存在
if (!root)
x = root = new RBNode<T>(key,BLACK);
else {
x = new RBNode<T>(key);//插入节点默认为红色,以方便调整
if (x->key < hot->key)
attachAsLChild(hot, x);//根据search函数定义,hot为返回节点的父亲,即要插入节点的父亲
else
attachAsRChild(hot, x);
solveDoubleRed(x);//解决双红问题
}
return x; //返回新插入节点位置
} //解决双红问题
template<typename T>
inline void RBTree<T>::solveDoubleRed(RBNodePos(T)& x)//注意节点x一定是红色
{
if (IsRoot(x)) { //若x已经为根
x->color = BLACK;//将x染黑
return;
}
RBNodePos(T) p = x->pa;
if (IsBlack(p)) return;//如果x的父亲p为黑,则双红问题解决,可以停止调整
RBNodePos(T) g = p->pa;//否则p为红色,必有黑色父亲g
RBNodePos(T) u = (IsLChild(p) ? g->rc : g->lc); //u为p的兄弟
if (IsBlack(u)) { //若u为黑色 --------情况1
if (IsLChild(x) == IsLChild(p)) //若x与其父亲同侧
p->color = BLACK;
else
x->color =BLACK;
g->color = RED; //g必定由黑色染为红色
//调整树形
RBNodePos(T) gg = g->pa;
RBNodePos(T) r = adjust(x); //r为调整后得到的子树的root
//将r与原树相连
if (!gg) {
root = r;
r->pa = NULL;
r->color = BLACK;
}
else {
if (r->key < gg->key)
attachAsLChild(gg, r);
else
attachAsRChild(gg, r);
}
}else { //否则u为红色 ---------情况2
p->color = BLACK;
u->color = BLACK;
if (!IsRoot(g)) g->color = RED;
solveDoubleRed(g);//继续调整g
}
}

删除操作

删除操作较插入操作要复杂的多,因为删除时涉及到的情况有很多,首先RBTree的删除操作前半段与搜索二叉树BST的删除操作一致:查找要删除的节点,如果存在该节点则执行删除操作,若该节点无左子树,则用右子树代替该节点,直接摘除该节点;若该节点无右子树,则用左子树代替该节点,直接摘除该节点;若该节点有左右子树,则寻找该节点中序遍历下的直接后继,可知直接后继必定无左孩子,然后交换两个节点的数据域,改为摘除直接后继(摘除直接后继的处理方法与处理无左子树的节点的方法一致)。以上是BST的删除操作。

通过上述分析可知最终实际摘除的节点至多只有一个孩子(如果有两个孩子最终也会通过寻找后继操作转换为摘除只有一个节点的节点),同时根据RBTree的定义,若摘除节点为黑色,则其孩子(若存在)必定为红色(否则不满足RBTree定义(3)),若摘除节点为红色,则其孩子(若存在)必定为黑色(可能是虚构的外部节点)。若实际被摘除节点为红色,根据定义不需要调整(摘除红色节点不会影响黑高度),故只有摘除黑色节点时需要调整(摘除黑色节点影响黑高度的计算)。

最终被摘除的节点至多只有一个孩子,我们定义它的接替者为succ(可能为NULL,即虚构的外部节点)

以下讨论删除操作时可能发生的情况:(下面x为实际被摘除节点)

情况1:x的接替者succ为红色

这时我们只需简单的将succ染黑即可保证局部子树黑高度不变,且全树黑高度不变。

情况2:x的接替者succ为黑色

这时我们遇到了双黑问题,即被摘除节点和其接替者均为黑色,我们需要利用solveDoubleBlack(RBNodePos(T) &r)解决双黑问题。双黑问题又分好几种情况:(以下实际被摘除节点为x,接替者为r)

情况2.1:x的父亲p为黑色,x的兄弟s为黑色,但是s有红色孩子

若被摘除节点x的兄弟s有红色孩子t,根据定义s必定为黑色(红父必黑),此时我们只需利用adjust函数先调整局部子树树形,然后重新染色即可,调整得到的新子树树根的颜色为原子树树根p(x和s的父亲)的颜色,然后将t染红即可,相当于牺牲了一个红色节点来保证子树的黑高度,通过下图可知调整后得到的子树满足RBTree定义且子树黑高度不变,调整到此结束。

情况2.2:x的父亲p为黑色,x的兄弟s为黑色,且s没有红色孩子

此时x,p,s全为黑色,考虑x被摘除后原来以x为root的局部子树黑高度减1,我们不妨将s染红,则以s为root的局部子树黑高度也减1,如此我们能保证以p为root的子树能满足RBTree的性质,但是注意此时以p为root的子树黑高度相比于摘除节点x低了一层,故需递归处理p(我们可以看出p原来有一个黑色单子父亲,我们摘除了单子父亲,则p为其父亲的接替者,只需递归处理p即可)。

情况2.3:x的父亲p为黑色,x的兄弟s为红色

此时x、p为黑,s为红,在摘除x之前,考虑到黑色节点x为内部节点,为保证黑高度相同,s必定存在两个实际存在的黑色内部节点孩子,我们不妨取t与s同侧,通过adjust(t)调整局部子树结构,然后交换s和p的颜色,这样我们就得到了一颗新的局部子树,这颗新的子树以s为root,这棵新树仍然存在双黑问题(x和r),但是我们将情况2.3巧妙地转换为了情况2.4或2.1,因为此时x有了一个黑色兄弟(必定存在且为内部节点),而情况2.4不会递归执行下去。我们只需再次solveDoubleBlack(r)即可。

情况2.4:x的父亲p为红色,此时x的兄弟s必定为黑色

此时x、s为黑,p为红,这种情况很好处理,我们只需要将p染黑,s染红即可使得局部子树满足RBTree性质且黑高度不变。

以上便是所有的删除时可能遇到的情况,可以看出相比于插入操作时复杂了很多,现在分析以下删除操作所花费的时间。

删除操作性能分析

首先调用search函数查找要删除的节点,需要O(log n )的时间完成。

情况 是否需要对结构进行拓扑调整 重染色节点个数 是否递归
情况1 × 1 ×
情况2.1 2-3 ×
情况2.2 × 1 √(上升一层)
情况2.3 2 √(转为情况2.1或2.4)
情况2.4 × 2 ×

从上表我们可以看出,执行删除操作时最多执行两次对树形结构的拓扑调整就一定会停止,同时如果递归也是严格上升(情况2.2)或再递归一次便停止(情况2.3),而树高是控制在O(log n)内的,也就是说删除操作只涉及常数个节点的拓扑结构更新和数次重染色,这便是RBTree与AVL树的一项本质差异

代码实现


//删除值为key的节点
template<typename T>
bool RBTree<T>::remove(const T &key) {
RBNodePos(T) x = search(key);
if (!x)
return false;//不存在值为key的节点
RBNodePos(T) w = x; //w是实际被摘除节点,初值等于x
RBNodePos(T) succ = NULL; //succ为被摘除节点的接替者
if(HasBothChild(x)){//如果x左右子树均存在,此时将x与x中序遍历下的直接后继交换数据,然后摘除后继
w = x->rc;//w为x中序遍历下的直接后继
while (HasLChild(w))//找到直接后继
w = w->lc;
//交换两个节点的数据
T tmp = x->key;
x->key = w->key;
w->key = tmp;
//隔离节点w
RBNodePos(T) p = w->pa;
if (p == x) //若x正好为w的父亲,即p->rc == w (x= p)
p->rc = succ = w->rc; //w没有左孩子
else //否则p->lc ==w
p->lc = succ = w->rc;
}else { //否则
if (!HasLChild(x)) //如果x左子树为空
succ = x->rc; //直接用右子树代替x
else//如果x右子树为空
succ = x->lc; //直接用左子树代替x,注意这里succ!= NULL,因为x存在左子树
//隔离节点w
if (!IsRoot(w))//若w非根,则w父亲存在
IsLChild(w) ? w->pa->lc = succ : w->pa->rc = succ;
}
hot = w->pa;//hot为实际被摘除节点的父亲
if (succ)//若继承者不为NULL
succ->pa = hot;
if(!hot)//摘除的是根节点
root = succ; //用succ作为root
// delete w;//摘除w
//以上和BST的remove操作一致
//下面是RBTree独有的部分 //根据上面BST的remove操作,最终实际被摘除的节点最多只有一个孩子(如果有两个孩子最终也会通过寻找后继操作转换为摘除只有一个节点的节点)
//同时根据RBTree的定义,若摘除节点为黑色,则其孩子(若存在)必定为红色,若摘除节点为红色,则其孩子(若存在)必定为黑色
if (!hot) { //根据以上BST的remove操作,若刚刚摘除的是根节点,则可知w至多只有一个孩子
if (root) //如果root存在
root->color = BLACK; //将根节点染黑即可
return true;
}
if (IsRed(w))//若被摘除节点w为红色,则无影响
return true;
//否则被摘除节点w为黑色
if (succ && IsRed(succ)) {//如果接替者succ为红色(若succ为NULL,根据定义外部节点为黑色,不影响下列操作) ------情况1
succ->color = BLACK;
return true;
}
//否则w和接替者succ都为黑色,出现双黑问题(succ可能为外部节点,但是不影响,因为外部节点也可当作黑色节点)
solveDoubleBlack(succ);//解决双黑问题 -------情况2
return true;
} //解决双黑问题
template<typename T>
inline void RBTree<T>::solveDoubleBlack(RBNodePos(T) &r) {
RBNodePos(T) p = (r?r->pa:hot); //r的父亲 p --parent
if (!p) return;
RBNodePos(T) s = ((r ==p->lc)? p->rc : p->lc); //r的兄弟 s --sibling
if (IsRed(s)) { //兄弟s为红,此时p必为黑 ----情况2.3
s->color = BLACK;
p->color = RED;
RBNodePos(T) g = p->pa;
RBNodePos(T) tmp_root = (IsLChild(s) ? adjust(s->lc) : adjust(s->rc));//调整树形
//将tmp_root与原树相连
if (!g) {
tmp_root->color = BLACK;
root = tmp_root;
tmp_root->pa = NULL;
}
else {
if (tmp_root->key < g->key)
attachAsLChild(g, tmp_root);
else
attachAsRChild(g, tmp_root);
}
solveDoubleBlack(r); //递归执行
}else { //兄弟s为黑
RBNodePos(T) t = NULL;//t为兄弟s的红孩子,左右皆红优先取左,皆黑为NULL
if (HasLChild(s) && IsRed(s->lc)) t = s->lc;
else if (HasRChild(s) && IsRed(s->rc)) t = s->rc;
if (t) {//如果兄弟s有红孩子 -----情况2.1
RBColor old_color = p->color; //拷贝原子树根节点p的颜色
RBNodePos(T) g= p->pa;//g为p的父亲
RBNodePos(T) tmp_root = adjust(t);//调整子树,tmp_root为调整后得到的局部子树的树根
//将tmp_root与原树相连
if (!g) {
root = tmp_root;
tmp_root->color = BLACK;
tmp_root->pa = NULL;
}
else {
if (tmp_root->key < g->key)
attachAsLChild(g, tmp_root);
else
attachAsRChild(g, tmp_root);
}
//重新染色
tmp_root->color = old_color;
tmp_root->lc->color = tmp_root->rc->color = BLACK;
}else { //兄弟s没有红孩子
if (IsRed(p)) { //如果p为红 ----情况2.4
p->color = BLACK; //p转黑
s->color = RED; //s转红
}else { //否则p为黑,s和其孩子都为黑,r也为黑 ----情况2.2
s->color = RED; //s转红
solveDoubleBlack(p); //递归
}
}
}
}

完整代码及测试实例

高级搜索树-红黑树(RBTree))代码实现

高级搜索树-红黑树(RBTree)解析的更多相关文章

  1. 高级搜索树-红黑树(RBTree)代码实现

    代码实现 代码参考了<数据结构(c++语言版)>--清华大学邓俊辉 "RBTree.h" #pragma once //#include"pch.h" ...

  2. 平衡搜索树--红黑树 RBTree

    红黑树是一棵二叉搜索树,它在每个节点上增加了一个存储位来表示节点的颜色,可以是Red或Black. 通过对任何一条从根到叶子节点简单路径上的颜色来约束树的高度,红黑树保证最长路径不超过最短路径的两倍, ...

  3. java——红黑树 RBTree

    对于完全随机的数据,普通的二分搜索树就很好用,只是在极端情况下会退化成链表. 对于查询较多的情况,avl树很好用. 红黑树牺牲了平衡性,但是它的统计性能更优(综合增删改查所有的操作). 红黑树java ...

  4. 红黑树(RBTREE)之上-------构造红黑树

    该怎么说呢,现在写代码的速度还是很快的,很高兴,o(^▽^)o. 光棍节到了,早上没忍住,手贱了一般,看到*D的优惠,买了个机械键盘,晚上就到了,敲着还是很舒服的,和老婆炫耀了一把哈哈. 光棍节再去* ...

  5. 高级数据结构---红黑树及其插入左旋右旋代码java实现

    前面我们说到的二叉查找树,可以看到根结点是初始化之后就是固定了的,后续插入的数如果都比它大,或者都比它小,那么这个时候它就退化成了链表了,查询的时间复杂度就变成了O(n),而不是理想中O(logn), ...

  6. 红黑树RBTree

    #pragma onceenum colour    //子节点的颜色{    RED,    BLANK,};template<class K,class V>struct RBTree ...

  7. 【转】B树、B-树、B+树、B*树、红黑树、 二叉排序树、trie树Double Array 字典查找树简介

    B  树 即二叉搜索树: 1.所有非叶子结点至多拥有两个儿子(Left和Right): 2.所有结点存储一个关键字: 3.非叶子结点的左指针指向小于其关键字的子树,右指针指向大于其关键字的子树: 如: ...

  8. RBTree 红黑树

    红黑树 一.红黑树概述 红黑树不仅是一个二叉搜索树,并且满足以下规则: 1>每个节点不是红的就是黑的, 2>根结点为黑色, 3>如果节点为红色,其子节点必须为黑色, 4>任一节 ...

  9. 史上最全HashMap红黑树解析

    HashMap红黑树解析 红黑树介绍 TreeNode结构 树化的过程 红黑树的左旋和右旋 TreeNode的左旋和右旋 红黑树的插入 TreeNode的插入 红黑树的删除 TreeNode的删除节点 ...

随机推荐

  1. ADB-常见命令使用详解

    ADB命令使用详解 ADB是一个 客户端-服务器端 程序, 其中客户端是你用来操作的电脑, 服务器端是android设备. 1.连接android设置adb connect 设备名例如:adb con ...

  2. SpringBoot学习笔记(十七:异步调用)

    @ 目录 1.@EnableAsync 2.@Async 2.1.无返回值的异步方法 2.1.有返回值的异步方法 3. Executor 3.1.方法级别重写Executor 3.2.应用级别重写Ex ...

  3. Web Scraping using Python Scrapy_BS4 - using BeautifulSoup and Python

    Use BeautifulSoup and Python to scrap a website Lib: urllib Parsing HTML Data Web scraping script fr ...

  4. Redis中的Scan命令踩坑记

    1 原本以为自己对redis命令还蛮熟悉的,各种数据模型各种基于redis的骚操作.但是最近在使用redis的scan的命令式却踩了一个坑,顿时发觉自己原来对redis的游标理解的很有限.所以记录下这 ...

  5. python学完可以做什么?Python就业方向最全面的解析

    乔布斯说过:“每一个人都应该学习如何编程,因为编程会教会你如何思考.”下一个时代是人机交互的时代,学习编程不是要让你成为程序员,而让你理解这个时代. 点击免费领取:全网最全python学习导图+14张 ...

  6. Puppeteer爬虫实战(一)

    Puppeteer 爬虫技术实践 信息简介 Puppeteer是Chrome开发团队发布的一个通过Chrome DevTool Protocol来控制浏览器Chrome(下文若无显式称呼Chromiu ...

  7. [redis] -- 集群篇

    三种集群方式 主从同步:主从复制模式中包含一个主数据库实例(master)与一个或多个从数据库实例(slave) 优点: master能自动将数据同步到slave,可以进行读写分离,分担master的 ...

  8. spring读取jdbc(file方式)

    使用PropertyPlaceholderConfigurer类载入外部配置 在Spring项目中,你可能需要从properties文件中读入配置注入到bean中,例如数据库连接信息,memcache ...

  9. 亚马逊如何使用二次验证码/虚拟MFA/两步验证/谷歌验证器?

    一般点账户名——设置——安全设置中开通虚拟MFA两步验证 具体步骤见链接  亚马逊如何使用二次验证码/虚拟MFA/两步验证/谷歌验证器? 二次验证码小程序于谷歌身份验证器APP的优势 1.无需下载ap ...

  10. state实例

    States是SaltStack中的配置语言,在日常进行配置管理时需要编写大量的States文件. 比如我们需要安装一个包,然后管理一个配置文件,最后保证某个服务正常运行. 这里就需要我们编写一些st ...