C++——红黑树

作者:几冬雪来

时间:2023年12月7日

内容:C++——红黑树讲解

目录

前言: 

红黑树的概念: 

红黑树的性质:

红黑树的路径计算: 

最长路径和最短路径:

AVL树与红黑树的区别: 

红黑树的框架: 

红黑树插入节点: 

插入节点颜色的变化:

颜色变化的特殊情况: 

变色与旋转: 

判断是否为红黑树: 

代码: 

结尾: 


前言: 

在上一篇博客中我们讲解了在C++两棵重要的树中的一棵树,也就是AVL树。而另一棵树就是今天要对其讲解的红黑树。而红黑树也算C++板块的一个重要的知识点,在解题的时候经常被使用。

 

红黑树的概念: 

在上一篇博客讲解AVL树的时候就有谈到过,红黑树和AVL树都属于二叉搜索树的一个延伸,二者的做法差不多,但是其性质却不一样

这里的红黑树,顾名思义就是在每个结点上都增加一个储存为表示该节点的颜色,可以说红或者黑色。而对比起之前讲解的C++的AVL树,红黑树会在一定程度上更优一点。 

红黑树在一定程度上占优,这是因为AVL树过于严格了,它必须保证最高的结点减去最短结点后高度要小于2或者大于-2

这个地方红黑树的做法是,使得每个结点都有红黑两种颜色限制达到最长路径不超过最短路径的两倍

但是这种行为也有它的弊端,那就是对比AVL树,AVL树的平衡是几乎平衡而红黑树的平衡只是近似平衡而且,且在插入相同数量结点的情况下红黑树的高度要较高于AVL树

虽然红黑树的高度相较于AVL树要高,但是其效率却不输AVL树

红黑树的性质:

在讲解完了红黑树的概念之后,接下来就来说一说红黑树的性质

这里看一下红黑树的4个性质,第一个要求每个结点不是黑的就是红色的

接下来就是规定了根节点必须是黑色的

第三条性质讲解了,在红黑树中的任何路径没有连续的两个同为红色的结点

这个地方的最后一条说明了,每条路径都包含了相同数量的黑色结点

红黑树的路径计算: 

在上面最后一条性质讲了,红黑树的每条路径都包含了相同数量的黑色结点

那么这个地方就延伸出一个问题,红黑树要怎么去数它路径的多少,就以刚刚上边的那张红黑树的图为例:

这里路径数量的计算方式是,从根节点到空节点算一条路径,因此要算该红黑树有多少路径的话,最优的方法就是看它空节点的数量是多少

最长路径和最短路径:

在了解完了红黑树路径的计算后,接下来我们就来看看它的最长和最短路径是如何得出的。在AVL树中我们可以得知,最长路径和最短路径是相互限制的,最长路径不能超过最短路径的高度差不超过1

而在红黑树中最长路径和最短路径有了新的变化

首先红黑树的最短路径如上图的左子树,全为黑色结点的路径就为最短路径。而最长路径则是一红一黑相间的路径

AVL树与红黑树的区别: 

在初步了解完了红黑树之后,接下来就是要让红黑树与先前学习的AVL树做对比,对比二者的优缺点

首先就是二者的时间复杂度

从图中可以看出红黑树的时间复杂度是AVL树的两倍,但是先前我们又讲解过红黑树要优于AVL树,这是因为二者处于同一量级,对此一倍与两倍对编译器的效率影响可以忽略不计

话虽如此,但是AVL树看起来时间复杂度是比AVL树小的,同时前面也有说明AVL树是被严格控制平衡的,红黑树只是近似平衡

但是AVL树严格控制平衡会有一定的代价,那就是AVL树在插入和删除的时候需要进行大量的旋转操作,而红黑树对比AVL树不需要大量的旋转操作

红黑树的框架: 

和书写AVL一样,书写红黑树也要有一个它自己的框架,又因为红黑树不用动用到平衡因子,因此也就不需要一个整形来存放平衡因子的值,二者框架的代码也会略微不同。

在这个地方依旧的要创建一个左结点,一个右结点和一个父结点。而后再借用map的pair创建一个_kv

与AVL在这个地方创建一个整形类型的_bf来储存平衡因子不同,红黑树因为没有平衡因子所以不需要创建_bf来使用

但是红黑树在这里涉及到了结点的颜色,因此此处要用一个枚举函数枚举出RED和BLACK两种颜色

再然后对它们进行初始化,这里的col就从RED与BLACK中随机选一个颜色进行初始化即可。、 

红黑树插入节点: 

在书写完了框架之后,接下来就是红黑树节点的插入

因为红黑树和AVL树都是二叉搜索树的延伸,因此在书写插入代码的时候二者十分的相似,这里我们就可以使用CV操作,将AVL树插入的代码拷贝过来

但是在红黑树的性质那里有规定,根节点的颜色必须为黑色,因此当根节点为空第一次插入结点的时候,这里要将这个节点的颜色设计为黑色

而在插入首个节点,接下来每个新插入节点的颜色会带来一些问题,也就是我们它的节点颜色是红色还是黑色

要解决这个问题,这里需要返回到之前红黑树性质的4条规则那里

在这里着重要关注的是第3条与第4条规则

如果插入的结点颜色为黑色的话,这个地方可能会使得第四条规则违规,因为规则里面有说要保证每个路径的黑色节点的个数相同,要是增加的节点颜色为黑色的话,最终会导致该路径的黑节点的个数与其他路径的不一致

如果新插入节点为红色,那这个地方要保证期父节点的颜色不为红色

因此插入的新节点颜色只能为红色,并且要对新节点的父节点进行判断

插入节点颜色的变化:

在上文我们讲解到红黑树插入的新节点的颜色必然是红色,而且如果插入的新节点的父节点也会红色的话,这里就要进行节点变色的操作

首先就是将新插入的节点定义为cur,接下来将它父节点定义为parent同时将parent的父节点的另一边节点定义为uncle

如果新插入的节点cur与parent的颜色同为红色的话,这里就违反了规则不能有两个红色的节点连续

因此parent结点就需要更改为黑色,又因为在红黑树中每条路径黑色结点的数量必须都一样,所以uncle要换为黑色,同时也因为每条路径黑色节点的数量不能改变,所以uncle的父节点要换为红色,如果parent的父节点为红且不为根节点的话,我们就继续向上处理。 

因此这个地方就总结出来了几个情况: 

1.如果grandfather没有父亲,也就是grandfather为根,只需要将其变黑即可

2.如果grandfather有父亲,且它的父亲为黑色,则结束运行

3. 如果grandfather有父亲,它的颜色为红色,这里就要向上处理,cur为第一次修改的grandfather位置

颜色变化的特殊情况: 

在什么我们得知了插入节点后节点颜色变化的规律,插入的必然是红节点,如果其父节点与uncle也为红节点的话,就要将它与uncle节点都改为黑色

接下来再判断它们的grandfather的颜色与有无父节点来决定要不要继续向上处理

但是这里可能会出现一些特殊的情况

就如同上图,新添加的cur节点它的父节点为红色,但是这里并没有uncle节点

这样就意味着,这个地方我们不能直接去修改parent与grandfather的颜色,如果修改后就会和上图一样,有一条路径的黑色节点的个数变少了。

这个地方的另一种特殊情况则是增加节点导致了最长路径超过了最短路径的两倍,在这种情况下我们也不能只是单纯的对其进行换色操作

而且这里为了解决这些问题就要利用到AVL树中的旋转操作了,同时这里的旋转也是有单旋和双旋的做法

就拿最长路径超过了最短路径的两倍的特殊情况来举例,这个地方要使这棵错误的红黑树变为正常的红黑树

首先就是以值为25的结点为旋转点进行一次右旋的操作,使得最长路径与最短路径在两倍或者两倍以内,再旋转后才能判断后对节点进行上色操作

同样的红黑树没有uncle节点也是通过旋转再上色的操作完成的

因此这里我们就得出了一些特殊情况:

1.uncle节点不存在的情况

2.uncle节点存在,且该节点颜色为黑

3.uncle节点存在,且该节点颜色为红

同时也对上面特殊情况的处理做了总结:

1.如果uncle存在且为红,则进行变色操作后再向上处理

2.如果uncle不存在或者存在且为黑,则先进行旋转操作后再进行换色操作 

这里我们也能看出红黑树插入后的旋转或者变色操作,关键还是要看uncle。 

变色与旋转: 

在进行完了节点插入的操作,并且也清楚的了解了红黑树的节点颜色的一些规矩,并且也知道了红黑树的变色与旋转操作,关键是在于uncle节点的情况

但是使得上下两个连接的节点都呈现红色的情况不单单只有parent为红节点,cur为新插入的节点这一种操作而已

下来我们就来讲解在哪些情况会出现下上下两个连接节点都为红色的情况

在这里的c,d,e处如果是只有一个黑节点的局部树的话有4种节点形状的插入,外加上新插入的节点有4个位置能插入,因此能计算出来这里最后能有256种(4*4*4*4)插入的结果

并且从上图我们也可以看出来使得上下两个节点都变为红色的不止只有新插入节点的情况,也可能是新插入的节点使得grandfather变为红色,让grandfather和它的父节点颜色相同。 

因此红黑树在这里有无数种组合情况。 

知道红黑树在什么情况下要对其节点进行变色,在什么情况下要对其节点进行旋转操作后,接下来我们就来书写它的代码。

因为在红黑树中我们可能要不断的向上处理,首先就是一个循环判断新插入节点的父节点存不存在存在的同时它的颜色是不是为红色,只有同时满足两个条件才能进入循环

接下来就是确定父节点的父节点,接下来如果parent节点为grandfather节点的左边的话,这个地方我们就能确定uncle节点的位置

再下来还是判断,如果uncle节点存在且为红色的话,这个地方我们就将parent与uncle节点更改为黑色,再将grandfather节点更改为红色

如果第一次变色结束后修改后的parent节点不存在或者存在但是为黑的话,这里的做法就是将根节点的颜色改为黑色

接下来就是几种比较特殊的情况,上图这种就是uncle节点不存在的情况,这种情况就需要进行旋转操作

而下面的则是存在uncle,但是parent节点为红uncle节点的颜色为黑色的情况。在这种情况下cur节点不可能为新插入的节点,因为这会导致它们路径上的黑节点不同

与此同时,c处应该是有一个黑色节点的局部树,d与e两个位置可以为红节点也可以为空

并且这里旋转的代码可以从AVL树那边CV过来,唯一需要更改的就是将平衡因子删除

这个地方在原parent为grandfather的左节点的基本上进行判断,这里如果uncle不存在或者uncle节点为黑,对其进行分类讨论

如果parent为grandfather的左节点,cur也为parent的左节点,这里就像AVL树一样,以grandfather为旋转点进行一次右旋,而后将parent与grandfather的节点颜色进行更改

如果cur为parent的右节点,这里也是和AVL树一样进行双旋后再对节点颜色进行更改

在上面我们就以parent为grandfather的左节点写了变色和处理的办法

这个地方则继续写出当parent为grandfather的右节点的代码,其变化和旋转的方法不变,唯一变化的则是方向可能会发生改变

判断是否为红黑树: 

在书写完了红黑树之后,接下来就是一个重点知识——如何判断该树是否为红黑树,或者说改红黑树有没有错误

像书写AVL树一样,在代码的在最后我们也需要一个判断,判断在AVL树中它插入节点后的平衡因子会不会发生异常

同样的红黑树也要这个操作,不同与AVL的是红黑树并没有平衡因子,而它的判断的内容则是红黑树性质的4大规则

这里要判断的就是以上的规则:根节点是否为黑色,是否有两个红节点相连接,它每条路径的黑节点个数是否一样

在了解完了详情之后,接下来就是对代码的书写了。

首先就是进行基础的判断,如果插入后的红黑树它的根节点为空这里就返回true,如果颜色不为黑色,这个地方就返回false

接下来定义一个整形用来存储某条路径黑色节点的个数,这里选择的就是最左路径,统计它最左路径黑色节点的个数

然后上面的代码就跳转到这里,也是一样先看看根节点是否为空,然后再判断里面还需要判断路径的黑色节点数是否相同(递归结束回到根节点处)

如果root处的节点颜色为黑色,这里就对计算黑色节点的整形进行++操作

然后就是if条件判断是否会出现连续的红节点,如果这里的root存在且为红,root的parent存在且为红,这种情况就是出现了连续的红节点,就需要报错后返回false。(这里不能用parent为红,parent的cur节点也为红的条件来判断,因为这里可能存在空节点)

最后这里要递归root节点的左子树与右子树,递归到根节点的parent,回到开头判断各个路径黑色节点的个数是否相同。  

这个地方就是我们的红黑树的判断

代码: 

RBTree.cpp

#include "RBTree.h"int main()
{int a[] = { 16,3,7,11,9 };RBTree<int, int> t;for (auto e : a){t.Insert(make_pair(e, e));cout << "Insert:" << e << "->" << t._IsBalance() << endl;}return 0;
}

RBTree.h 

#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;Colour _col;RBTreeNode(const pair<K, V>& kv):_left(nullptr),_right(nullptr),_parent(nullptr),_kv(kv),_col(RED){}
};template<class K, class V>
struct RBTree
{typedef RBTreeNode<K, V> Node;
public:bool Insert(const 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 (cur->_kv.first < kv.first){parent = cur;cur = cur->_right;}else if (cur->_kv.first > kv.first){parent = cur;cur = cur->_left;}else{return false;}}cur = new Node(kv);cur->_col = RED;if (parent->_kv.first < kv.first){parent->_right = cur;}else{parent->_left = cur;}cur->_parent = parent;while (parent && parent->_col == RED){Node* grandfather = parent->_parent;if (parent == grandfather->_left){Node* uncle = grandfather->_right;if (uncle && uncle->_col == RED){parent->_col = uncle->_col = BLACK;grandfather->_col = RED; cur = grandfather;parent = cur->_parent;}else{if (cur == parent->_left){RotateR(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else{RotateL(parent);RotateR(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break;}}else//	if (parent == grandfather->_right){Node* uncle = grandfather->_left;if (uncle && uncle->_col == RED){parent->_col = uncle->_col = BLACK;grandfather->_col = RED;cur = grandfather;parent = cur->_parent;}else{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 RotateL(Node* parent){Node* cur = parent->_right;Node* curleft = cur->_left;Node* ppnode = parent->_parent;parent->_right = curleft;if (curleft){curleft->_parent = parent;}cur->_left = parent;parent->_parent = cur;if (parent == _root){_root = cur;cur->_parent = nullptr;}else{if (ppnode->_left == parent){ppnode->_left = cur;}else{ppnode->_right = cur;}cur->_parent = ppnode;}}void RotateR(Node* parent){Node* cur = parent->_left;Node* curright = cur->_right;Node* ppnode = parent->_parent;parent->_left = cur->_right;if (curright){curright->_parent = parent;}cur->_right = parent;parent->_parent = cur;if (ppnode == nullptr){_root = cur;cur->_parent = nullptr;}else{if (ppnode->_left == parent){ppnode->_left = cur;}else{ppnode->_right = cur;}cur->_parent = ppnode;}}bool CheckColour(Node* root, int blacknum, int benchmark){if (root == nullptr){if (blacknum != benchmark){return false;}return true;}if (root->_col == BLACK){++blacknum; }if (root->_col == RED && root->_parent && root->_parent->_col == RED){cout << root->_kv.first << "出现连续红节点" << endl;return false;}return CheckColour(root->_left, blacknum, benchmark) && CheckColour(root->_right, blacknum, benchmark);}bool _IsBalance(){return IsBalance(_root);}bool IsBalance(Node* root){if (root == nullptr){return true;}if (root->_col != BLACK){return false; }int benchmark = 0;Node* cur = _root;while (cur){if (cur->_col == BLACK){++benchmark;}cur = cur->_left;}return CheckColour(root, 0, benchmark);}private:Node* _root = nullptr; 
};

结尾: 

到这里我们的红黑树代码的基本讲解就告一段落了,但是这并不意味着红黑树就结束了,在以后学习的日子中我们会学习然后对红黑树进行封装,如何用AVL树和红黑树去解决一下K,V问题,最后希望这篇博客能给各位带来一些帮助。 

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

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

相关文章

【Docker】Swarm的overlay网络

对于理解swarm的网络来讲&#xff0c;个人认为最重要的两个点&#xff1a; 第一是外部如何访问部署运行在swarm集群内的服务&#xff0c;可以称之为入方向流量&#xff0c;在swarm里我们通过ingress来解决。 第二是部署在swarm集群里的服务&#xff0c;如何对外进行访问&…

力扣题:字符的统计-12.6

力扣题-12.6 [力扣刷题攻略] Re&#xff1a;从零开始的力扣刷题生活 力扣题1&#xff1a;696. 计数二进制子串 解题思想&#xff1a;先统计连续的0和连续的1的个数&#xff0c;然后进行相加即可&#xff08;想不到一点&#xff09; class Solution(object):def countBinaryS…

EI级 | Matlab实现TCN-LSTM-Multihead-Attention多头注意力机制多变量时间序列预测

EI级 | Matlab实现TCN-LSTM-Multihead-Attention多头注意力机制多变量时间序列预测 目录 EI级 | Matlab实现TCN-LSTM-Multihead-Attention多头注意力机制多变量时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.【EI级】Matlab实现TCN-LSTM-Multihead-Attent…

egen3 rowwise().maxCoeff()的使用

1、安装eigen3 2、引用头文件 3、代码测试 MatrixXf aaa(2, 4);aaa << 1, 2, 3, 4, 5, 6, 7, 8; Vector2f diff(10, 20);aaa.colwise() diff;std::cout << "new_aaa : " << aaa << endl; 全部代码&#xff1a; int main() {MatrixXf …

Ubuntu18安装(重启黑屏问题)

1. F10 进入bios&#xff0c;选择u盘里的ubuntu镜像 2.进入使用ubuntu&#xff0c;下载 3.重启&#xff0c;esc 4.ubuntu 安e进入 5. nomodeset&#xff08;&#xff09; F10 保存启动 6. 7.没有网 手机usb提供网络 下载有限网卡驱动

【头歌实训】分布式文件系统 HDFS

文章目录 第1关&#xff1a;HDFS的基本操作任务描述相关知识HDFS的设计分布式文件系统NameNode与DataNode HDFS的常用命令 编程要求测试说明答案代码 第2关&#xff1a;HDFS-JAVA接口之读取文件任务描述相关知识FileSystem对象FSDataInputStream对象 编程要求测试说明答案代码 …

代码随想录算法训练营 ---第五十七天

今天是两道动态规划的经典题目。 第一题&#xff1a; 简介&#xff1a; 做了今天的题目我有了新的理解&#xff0c;我觉得过去我过于注重对于二维数组的理解&#xff0c;忽略了对dp数组i 和 j 的含义的理解。 动态规划五部曲&#xff1a; 1.确定dp数组的含义 本题我们将i …

vue使用vue-router路由控件

vue-router是官方路由控件&#xff0c;但官网竟然没有该路由的使用方法啊&#xff0c;只有单页面的简单示例&#xff0c;引用的是网页js模式&#xff0c;不是vue的引用模式啊。 {"name": "vue01","private": true,"version": "0…

面试操作系统八股文五问五答第一期

面试操作系统八股文五问五答第一期 作者&#xff1a;程序员小白条&#xff0c;个人博客 相信看了本文后&#xff0c;对你的面试是有一定帮助的&#xff01; ⭐点赞⭐收藏⭐不迷路&#xff01;⭐ 1.死锁产生的条件 1.互斥条件&#xff0c;即当资源被一个线程使用&#xff08;…

流畅的python--小技巧总结

对于百万级以上数据的读写效率优化 在Python中&#xff0c;我们可以通过array模块来方便地创建和操作数组&#xff0c;array模块提供了性能优异的数组操作方法&#xff0c;可以使我们的代码更加高效。同时&#xff0c;Python也提供了读写二进制文件和普通文本文件的方法&#…

Rename--CAM在checkpoint恢复上的流程与优势

参考自&#xff1a;计算机体系结构-寄存器重命名 - 知乎 (zhihu.com) 这里的重点在红色部分描述上&#xff0c;也是容易想不通的地方&#xff1b; 在SRAM方法中要恢复映射表状态&#xff0c;需要事先把分支指令来临时刻的映射表全份拷贝下来&#xff0c;然后在恢复时把备份复制…

Linux 环境下,jdbc连接mysql问题

1. 下载MySQL的JDBC驱动&#xff1a; 从MySQL官网下载最新的MySQL Connector/J&#xff0c;并将其解压到某个目录&#xff0c;比如/usr/local/mysql/。 2. 将JDBC驱动添加到类路径&#xff1a; 将JDBC驱动添加到类路径&#xff0c;可以使用以下命令&#xff1a; export CLA…

AMEYA360分析兆易创新GD32A490系列车规级MCU

兆易创新GigaDevice今日宣布&#xff0c;正式推出全新GD32A490系列高性能车规级MCU&#xff0c;以高主频、大容量、高集成和高可靠等优势特性紧贴汽车电子开发需求&#xff0c;适用于车窗、雨刷、智能车锁、电动座椅等BCM车身控制系统&#xff0c;以及仪表盘、娱乐影音、中控导…

智慧景区(园区)数字孪生可视化GIS解决方案

随着技术的日新月异&#xff0c;景区日常管理及运营中使用到的智慧化工具越来越丰富&#xff0c;智慧化硬件设备也越来越多&#xff0c;而其中各个管理系统往往又是相互独立&#xff0c;形成一个个数据孤岛。智慧景区管理平台就是将各个孤岛中的数据及功能汇集起来&#xff0c;…

政采云基于 Dubbo 的混合云数据跨网实践

作者&#xff1a;王晓彬&#xff0c;政采云资深开发工程师 项目背景 政采云的业务是为政府提供的购物网站&#xff0c;类似于淘宝。政府采购会在政采云上做企业采购、政府采购的业务。 云岛中的“云”是指我们的云平台&#xff0c;云平台是我们公司自己部署的一套购物网站&a…

【计算机网络漫谈】OSI七层模型与TCP/IP四层(参考)模型

一、七层&#xff1f;四层&#xff1f; 1.为什么需要协议&#xff1f;2.OSI七层模型是干什么的&#xff1f;3.TCP/IP四层&#xff08;参考&#xff09;模型4.TCP/IP&#xff08;参考&#xff09;模型与OSI七层模型有什么异同&#xff1f; 二、自底向上的网络分层 1. 物理层2…

防火墙 iptables的使用

目录 什么是防火墙 原理 代理 防火墙的工具 4表5列 五链&#xff1a;控制流量的时机 四个表&#xff1a;如何控制流量 ​编辑 iptables 软件 格式 选项 跳转 查iptables 的规则 添加规则 A I 删除规则 清空规则 替换规则 R 修改默认规则&#xff08;默…

校园外卖小程序源码系统 附带完整的搭建教程

随着大学生消费水平的提高&#xff0c;对于外卖服务的需求也在不断增加。很多学生都面临着课业繁重、时间紧张等问题&#xff0c;无法亲自到餐厅就餐。因此&#xff0c;开发一款适合校园外卖市场的应用软件&#xff0c;将为广大学生提供极大的便利。 以下是部分代码示例&#…

Python Numpy学习(1)

python: np.pad() 函数的用法 np.einsum(爱因斯坦求和约定) python编程之np.isnan()的用法 Python_Numpy库中各种矩阵基本运算 numpy基本加减和取行操作 numpy的加减乘除运算 NumPy 算术函数 TypeError: can’t convert cuda:0 device type tensor to numpy. Use Tenso…

了解linux网络时间服务器

本章主要介绍网络时间服务器。 使用chrony配置时间服务器 配置chrony客户端向服务器同步时间 20.1 时间同步的必要性 些服务对时间要求非常严格&#xff0c;例如&#xff0c;图20-1所示的由三台服务器搭建的ceph集群。 这三台服务器的时间必须保持一致&#xff0c;如果不一致…