Files
Data-Structure/BinaryTree/BalanceTree/B-Tree/README.MD
2025-08-26 21:39:35 +08:00

711 lines
63 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<p><a href="https://www.cnblogs.com/lianzhilei/p/11250589.html">B树、B+树详解 - Assassinの - 博客园 (cnblogs.com)</a></p>
<h3>B树</h3>
<p><strong>前言  </strong></p>
<p>  首先为什么要总结B树、B+树的知识呢?最近在学习数据库索引调优相关知识,<strong>数据库系统普遍采用B-/+Tree作为索引结构</strong>例如mysql的InnoDB引擎使用的B+树理解不透彻B树则无法理解数据库的索引机制接下来将用最简洁直白的内容来了解B树、B+树的数据结构</p>
<p>  另外B-树即为B树。因为B树的原英文名称为B-tree而国内很多人喜欢把B-tree译作B-树其实这是个非常不好的直译很容易让人产生误解。如人们可能会以为B-树是一种树而B树又是一种树。而事实上是B-tree就是指的B树目前理解B的意思为平衡</p>
<p>  B树的出现是为了弥合不同的存储级别之间的访问速度上的巨大差异实现高效的 I/O。平衡二叉树的查找效率是非常高的并可以通过降低树的深度来提高查找的效率。<span style="color: rgba(255, 0, 0, 1)">但是当数据量非常大树的存储的元素数量是有限的这样会导致二叉查找树结构由于树的深度过大而造成磁盘I/O读写过于频繁进而导致查询效率低下。另外数据量过大会导致内存空间不够容纳平衡二叉树所有结点的情况。B树是解决这个问题的很好的结构</span></p>
<p>&nbsp;</p>
<p><strong>概念</strong></p>
<p>  首先B树不要和二叉树混淆<a title="计算机科学" href="https://en.wikipedia.org/wiki/Computer_science" rel="noopener nofollow">计算机科学中</a><strong>B树</strong>是一种自平衡<a class="mw-redirect" title="树数据结构" href="https://en.wikipedia.org/wiki/Tree_data_structure" rel="noopener nofollow">树数据结构</a>,它维护有序数据并允许以<a class="mw-redirect" title="对数时间" href="https://en.wikipedia.org/wiki/Logarithmic_time" rel="noopener nofollow">对数时间</a>进行搜索顺序访问插入和删除。B树是<a title="二进制搜索树" href="https://en.wikipedia.org/wiki/Binary_search_tree" rel="noopener nofollow">二叉搜索树</a>的一般化,因为节点可以有两个以上的子节点。<sup id="cite_ref-Comer_1-0" class="reference"><a href="https://en.wikipedia.org/wiki/B-tree#cite_note-Comer-1" rel="noopener nofollow">[1]</a></sup>与其他<a title="自平衡二叉搜索树" href="https://en.wikipedia.org/wiki/Self-balancing_binary_search_tree" rel="noopener nofollow">自平衡二进制搜索树不同</a>B树非常适合读取和写入相对较大的数据块如光盘的存储系统。它通常用于<a title="数据库" href="https://en.wikipedia.org/wiki/Database" rel="noopener nofollow">数据库</a><a title="文件系统" href="https://en.wikipedia.org/wiki/File_system" rel="noopener nofollow">文件系统</a></p>
<p>&nbsp;</p>
<p><strong>定义</strong></p>
<p>B树是一种平衡的多分树通常我们说m阶的B树它必须满足如下条件&nbsp;</p>
<ul>
<li><span style="color: rgba(255, 0, 0, 1)">每个节点最多只有m个子节点。</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">每个非叶子节点(除了根)具有至少⌈ m/2⌉子节点含有ceil(m/2)-1到m-1个元素。</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">如果根不是叶节点,则根至少有两个子节点。</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">具有<em>k</em>个子节点的非叶节点包含<em>k</em>&nbsp;-1个键。</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">所有叶子都出现在同一水平,没有任何信息(高度一致)。</span></li>
</ul>
<p>第一次看到这个定义的时候,在想什么鬼?。。。。什么是阶?子节点、飞叶子点、根???啥意思!少年别慌。。。</p>
<p><strong>什么是B树的阶 </strong></p>
<p><span style="color: rgba(255, 0, 0, 1)">B树中一个节点的子节点数目的最大值用m表示</span>假如最大值为10则为10阶如图</p>
<p><img src="https://img2018.cnblogs.com/blog/997909/201907/997909-20190727111522935-981534929.png" alt="" width="756" height="240" class="medium-zoom-image"></p>
<p>所有节点中节点【13,16,19】拥有的子节点数目最多四个子节点灰色节点所以可以定义上面的图片为4阶B树现在懂什么是阶了吧</p>
<p><strong>什么是根节点 </strong></p>
<p>节点【10】即为根节点特征根节点拥有的子节点数量的上限和内部节点相同如果根节点不是树中唯一节点的话至少有俩个子节点不然就变成单支了<span style="color: rgba(255, 0, 0, 1)">在m阶B树中根节点非树中唯一节点那么根结点有关系式2&lt;= M &lt;=mM为子节点数量包含的元素数量 1&lt;= K &lt;=m-1,K为元素数量。</span><strong><br></strong></p>
<p><strong>什么是内部节点 </strong></p>
<p>节点【13,16,19】、节点【3,6】都为内部节点特征<span style="color: rgba(255, 0, 0, 1)">内部节点是除叶子节点和根节点之外的所有节点,拥有父节点和子节点</span>。假定m阶B树的内部节点的子节点数量为M<span style="color: rgba(255, 0, 0, 1)">则一定要符合m/2&lt;=&nbsp; M &lt;=m关系式包含元素数量M-1包含的元素数量 m/2-1&lt;= K &lt;=m-1,K为元素数量。m/2向上取整</span></p>
<p><strong>什么是叶子节点?</strong></p>
<p>节点【1,2】、节点【11,12】等最后一层都为叶子节<span style="color: rgba(255, 0, 0, 1)">叶子节点对元素的数量有相同的限制但是没有子节点也没有指向子节点的指针。特征在m阶B树中叶子节点的元素符合m/2-1&lt;= K &lt;=m-1。</span></p>
<p>好了,概念已经清楚,不用着急背公式, 接着往下看</p>
<p>&nbsp;</p>
<p><strong>插入</strong></p>
<p>针对m阶高度h的B树插入一个元素时首先在B树中是否存在如果不存在即在叶子结点处结束然后在叶子结点中插入该新的元素。</p>
<ul>
<li><span style="color: rgba(255, 0, 0, 1)">若该节点元素个数小于m-1直接插入</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">若该节点元素个数等于m-1引起节点分裂以该节点中间元素为分界取中间元素偶数个数中间两个随机选取插入到父节点中</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">重复上面动作直到所有节点符合B树的规则最坏的情况一直分裂到根节点生成新的根节点高度增加1</span></li>
</ul>
<p>上面三段话为插入动作的核心接下来以5阶B树为例详细讲解插入的动作</p>
<p>5阶B树关键点:</p>
<ul>
<li>2&lt;=根节点子节点个数&lt;=5</li>
<li>3&lt;=内节点子节点个数&lt;=5</li>
<li>1&lt;=根节点元素个数&lt;=4</li>
<li>2&lt;=非根节点元素个数&lt;=4</li>
</ul>
<p><img src="https://img2018.cnblogs.com/blog/997909/201907/997909-20190727135621105-1688663582.png" alt="">&nbsp; &nbsp; &nbsp;插入8&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<img src="https://img2018.cnblogs.com/blog/997909/201907/997909-20190727150240628-562673561.png" alt="" class="medium-zoom-image"></p>
<p>1插入元素【8】后变为图2此时根节点元素个数为5不符合&nbsp;1&lt;=根节点元素个数&lt;=4进行分裂真实情况是先分裂然后插入元素这里是为了直观而先插入元素下面的操作都一样不再赘述取节点中间元素【7】加入到父节点左右分裂为2个节点如图3</p>
<p><img src="https://img2018.cnblogs.com/blog/997909/201907/997909-20190727150127189-82041957.png" alt="" width="275" height="206" class="medium-zoom-image"></p>
<p>接着插入元素【5】【11】【17】时不需要任何分裂操作如图4</p>
<p>&nbsp; &nbsp;<img src="https://img2018.cnblogs.com/blog/997909/201907/997909-20190727150529778-12220622.png" alt="" width="445" height="190" class="medium-zoom-image"></p>
<p>插入元素【13】</p>
<p><img src="https://img2018.cnblogs.com/blog/997909/201907/997909-20190727150325549-653814900.png" alt="" width="494" height="202" class="medium-zoom-image"></p>
<p>节点元素超出最大数量进行分裂提取中间元素【13】插入到父节点当中如图6</p>
<p>&nbsp; &nbsp;&nbsp;<img src="https://img2018.cnblogs.com/blog/997909/201907/997909-20190727150403812-1196185160.png" alt="" width="429" height="217" class="medium-zoom-image"></p>
<p>&nbsp;</p>
<p>&nbsp;接着插入元素【6】【12】【20】【23】时不需要任何分裂操作如图7</p>
<p><img src="https://img2018.cnblogs.com/blog/997909/201907/997909-20190727151202374-95300359.png" alt="" width="618" height="225" class="medium-zoom-image"></p>
<p>插入【26】时最右的叶子结点空间满了需要进行分裂操作中间元素【20】上移到父节点中注意通过上移中间元素树最终还是保持平衡分裂结果的结点存在2个关键字元素。</p>
<p><img src="https://img2018.cnblogs.com/blog/997909/201907/997909-20190727152815688-1828133575.png" alt="" width="682" height="230" class="medium-zoom-image"></p>
<p>&nbsp;</p>
<p>插入【4】时导致最左边的叶子结点被分裂【4】恰好也是中间元素上移到父节点中然后元素【16】,【18】,【24】,【25】陆续插入不需要任何分裂操作</p>
<p>&nbsp;<img src="https://img2018.cnblogs.com/blog/997909/201907/997909-20190727153234424-288986187.png" alt="" width="768" height="218" class="medium-zoom-image"></p>
<p>最后当插入【19】时含有【14】,【16】,【17】,【18】的结点需要分裂把中间元素【17】上移到父节点中但是情况来了父节点中空间已经满了所以也要进行分裂将父节点中的中间元素【13】上移到新形成的根结点中这样具体插入操作的完成。</p>
<p><img src="https://img2018.cnblogs.com/blog/997909/201907/997909-20190727154112560-585266052.png" alt="" width="826" height="275" class="medium-zoom-image"></p>
<p>&nbsp;</p>
<p><strong>删除</strong></p>
<p>首先查找B树中需删除的元素,如果该元素在B树中存在则将该元素在其结点中进行删除删除该元素后首先判断该元素是否有左右孩子结点如果有则上移孩子结点中的某相近元素(“左孩子最右边的节点”或“右孩子最左边的节点”)到父节点中,然后是移动之后的情况;如果没有,直接删除。</p>
<ul>
<li><span style="color: rgba(255, 0, 0, 1)">某结点中元素数目小于m/2-1,(m/2)向上取整,则需要看其某相邻兄弟结点是否丰满;</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">如果丰满(结点中元素个数大于(m/2)-1则向父节点借一个元素来满足条件</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">如果其相邻兄弟都不丰满,即其结点数目等于(m/2)-1则该结点与其相邻的某一兄弟结点进行“合并”成一个结点</span></li>
</ul>
<p>接下来还以5阶B树为例详细讲解删除的动作</p>
<ul>
<li>关键要领,元素个数小于 2m/2 -1就合并大于4m-1就分裂</li>
</ul>
<p>如图依次删除依次删除【8】,【20】,【18】,【5】</p>
<p><img src="https://img2018.cnblogs.com/blog/997909/201907/997909-20190727160351637-1629033369.png" alt="" width="814" height="277" class="medium-zoom-image"></p>
<p>首先删除元素【8】当然首先查找【8】【8】在一个叶子结点中删除后该叶子结点元素个数为2符合B树规则操作很简单咱们只需要移动【11】至原来【8】的位置移动【12】至【11】的位置也就是结点中删除元素后面的元素向前移动</p>
<p>&nbsp;</p>
<p><img src="https://img2018.cnblogs.com/blog/997909/201907/997909-20190727160902710-1900336278.png" alt="" width="812" height="292" class="medium-zoom-image"></p>
<p>&nbsp;下一步删除【20】,因为【20】没有在叶子结点中而是在中间结点中找到咱们发现他的继承者【23】(字母升序的下个元素)将【23】上移到【20】的位置然后将孩子结点中的【23】进行删除这里恰好删除后该孩子结点中元素个数大于2无需进行合并操作。</p>
<p><img src="https://img2018.cnblogs.com/blog/997909/201907/997909-20190727161905571-1441211802.png" alt="" width="796" height="275" class="medium-zoom-image"></p>
<p>下一步删除【18】【18】在叶子结点中,但是该结点中元素数目为2删除导致只有1个元素已经小于最小元素数目2,而由前面我们已经知道如果其某个相邻兄弟结点中比较丰满元素个数大于ceil(5/2)-1=2则可以向父结点借一个元素然后将最丰满的相邻兄弟结点中上移最后或最前一个元素到父节点中在这个实例中右相邻兄弟结点中比较丰满3个元素大于2所以先向父节点借一个元素【23】下移到该叶子结点中代替原来【19】的位置【19】前移然【24】在相邻右兄弟结点中上移到父结点中最后在相邻右兄弟结点中删除【24】后面元素前移。</p>
<p><img src="https://img2018.cnblogs.com/blog/997909/201907/997909-20190727162837714-1263711630.png" alt="" width="792" height="301" class="medium-zoom-image"></p>
<p>最后一步删除【5】&nbsp;删除后会导致很多问题因为【5】所在的结点数目刚好达标刚好满足最小元素个数ceil(5/2)-1=2,而相邻的兄弟结点也是同样的情况<strong>,删除一个元素都不能满足条件,所以需要该节点与某相邻兄弟结点进行合并操作;首先移动父结点中的元素(该元素在两个需要合并的两个结点元素之间)下移到其子结点中,然后将这两个结点进行合并成一个结点。</strong>所以在该实例中咱们首先将父节点中的元素【4】下移到已经删除【5】而只有【6】的结点中然后将含有【4】和【6】的结点和含有【1】,【3】的相邻兄弟结点进行合并成一个结点。<br><img src="https://img2018.cnblogs.com/blog/997909/201907/997909-20190727163249830-1144020179.png" alt="" width="751" height="327" class="medium-zoom-image"></p>
<p>&nbsp;</p>
<p>也许你认为这样删除操作已经结束了其实不然在看看上图对于这种特殊情况你立即会发现父节点只包含一个元素【7】没达标因为非根节点包括叶子结点的元素K必须满足于2=&lt;K&lt;=4而此处的K=1这是不能够接受的。<span style="color: rgba(255, 0, 0, 1)">如果这个问题结点的相邻兄弟比较丰满则可以向父结点借一个元素。而此时兄弟节点元素刚好为2刚刚满足只能进行合并而根结点中的唯一元素【13】下移到子结点</span>,这样,树的高度减少一层。<br><img src="https://img2018.cnblogs.com/blog/997909/201907/997909-20190727163912012-1560407429.png" alt="" width="736" height="310" class="medium-zoom-image"></p>
<p>看完插入删除想必也把B树的特征掌握了下面普及下其他知识换个脑子</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<h3 id="磁盘io与预读"><strong>磁盘IO与预读</strong><button class="cnblogs-toc-button" title="显示目录导航" aria-expanded="false"></button></h3>
<p><strong>  </strong>计算机存储设备一般分为两种:内存储器(main memory)和外存储器(external memory)。&nbsp;</p>
<p>  内存储器为内存,内存存取速度快,但容量小,价格昂贵,而且不能长期保存数据(在不通电情况下数据会消失)。</p>
<p>  外存储器即为磁盘读取磁盘读取数据靠的是机械运动每次读取数据花费的时间可以分为寻道时间、旋转延迟、传输时间三个部分寻道时间指的是磁臂移动到指定磁道所需要的时间主流磁盘一般在5ms以下旋转延迟就是我们经常听说的磁盘转速比如一个磁盘7200转表示每分钟能转7200次也就是说1秒钟能转120次旋转延迟就是1/120/2 = 4.17ms传输时间指的是从磁盘读出或将数据写入磁盘的时间一般在零点几毫秒相对于前两个时间可以忽略不计。那么访问一次磁盘的时间即一次磁盘IO的时间约等于5+4.17 = 9ms左右听起来还挺不错的但要知道一台500 -MIPS的机器每秒可以执行5亿条指令因为指令依靠的是电的性质换句话说执行一次IO的时间可以执行40万条指令数据库动辄十万百万乃至千万级数据每次9毫秒的时间显然是个灾难。下图是计算机硬件延迟的对比图供大家参考</p>
<p><img src="https://img2018.cnblogs.com/blog/997909/201907/997909-20190727165418402-224791766.png" alt="" width="492" height="288" class="medium-zoom-image"></p>
<p>&nbsp;  考虑到磁盘IO是非常高昂的操作计算机操作系统做了一些优化当一次IO时不光把当前磁盘地址的数据而是把相邻的数据也都读取到内存缓冲区内因为局部预读性原理告诉我们当计算机访问一个地址的数据的时候与其相邻的数据也会很快被访问到。每一次IO读取的数据我们称之为一页(page)。具体一页有多大数据跟操作系统有关一般为4k或8k也就是我们读取一页内的数据时候实际上才发生了一次IO这个理论对于索引的数据结构设计非常有帮助。</p>
<p>事实1 不同容量的存储器,访问速度差异悬殊。</p>
<ul>
<li>磁盘(ms级别) &lt;&lt; 内存(ns级别) 100000倍</li>
<li>若内存访问需要1s则一次外存访问需要一天</li>
<li>为了避免1次外存访问宁愿访问内存100次...所以将<code>最常用</code>的数据存储在最快的存储器中</li>
</ul>
<p>事实2 从磁盘中读 1 B与读写 1KB 的时间成本几乎一样</p>
<p>从以上数据中可以总结出一个道理索引查询的数据主要受限于硬盘的I/O速度查询I/O次数越少速度越快所以B树的结构才应需求而生B树的每个节点的元素可以视为一次I/O读取树的高度表示最多的I/O次数在相同数量的总元素个数下<strong><span style="color: rgba(255, 0, 0, 1)">每个节点的元素个数越多高度越低查询所需的I/O次数越少</span></strong>假设一次硬盘一次I/O数据为8K索引用int(4字节)类型数据建立理论上一个节点最多可以为2000个元素2000*2000*2000=800000000080亿条的数据只需3次I/O理论值可想而知B树做为索引的查询效率有多高</p>
<p>另外也可以看出同样的总元素个数,查询效率和树的高度密切相关</p>
<p>&nbsp;</p>
<p><strong>B树的高度</strong></p>
<p>一棵含有N个总关键字数的m阶的B树的最大高度是多少</p>
<p>  logm/2(N+1)/2&nbsp;+ 1&nbsp; log以m/2为低(N+1)/2的对数再加1</p>
<p>算法如下</p>
<p><img src="https://img2018.cnblogs.com/blog/997909/201907/997909-20190727171655326-1844439947.png" alt="" width="653" height="491" class="medium-zoom-image"></p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<h3>B+树<button class="cnblogs-toc-button" title="显示目录导航" aria-expanded="false"></button></h3>
<p>&nbsp;  B+树是应文件系统所需而产生的B树的变形树那么可能一定会想到既然有了B树又出一个B+树那B+树必然是有很多优点的</p>
<p>&nbsp;</p>
<p><strong>B+树的特征:</strong></p>
<ul>
<li><span style="color: rgba(255, 0, 0, 1)">有m个子树的中间节点包含有m个元素B树中是k-1个元素每个元素不保存数据只用来索引</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大的顺序链接。&nbsp;(而B 树的叶子节点并没有包括全部需要查找的信息)</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">所有的非终端结点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键字。&nbsp;(而B 树的非终节点也包含需要查找的有效信息)</span></li>
</ul>
<p>&nbsp;</p>
<p><strong>为什么说B+树比B树更适合数据库索引</strong></p>
<p><span style="color: rgba(255, 0, 0, 1)">1B+树的磁盘读写代价更低</span></p>
<p>  <span style="color: rgba(255, 0, 0, 1)">B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B 树更小。</span>如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。<span style="color: rgba(255, 0, 0, 1)">一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了</span></p>
<p><span style="color: rgba(255, 0, 0, 1)">2B+树查询效率更加稳定</span></p>
<p><span style="color: rgba(255, 0, 0, 1)">  由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当;</span></p>
<p><span style="color: rgba(255, 0, 0, 1)">3B+树便于范围查询(最重要的原因,范围查找是数据库的常态)</span></p>
<p>  B树在提高了IO性能的同时并没有解决元素遍历的我效率低下的问题正是为了解决这个问题B+树应用而生。B+树只需要去遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的而B树不支持这样的操作或者说效率太低不懂可以看看这篇解读-》<a href="https://zhuanlan.zhihu.com/p/54102723" target="_blank" rel="noopener nofollow">范围查找</a></p>
<p>&nbsp;</p>
<p>补充B树的范围查找用的是中序遍历而B+树用的是在链表上遍历;</p>
<p>B+树如下:</p>
<p><img src="https://img2018.cnblogs.com/blog/997909/201907/997909-20190728114240297-169990922.png" alt="" class="medium-zoom-image"></p>
<p>&nbsp;<a href="https://www.cnblogs.com/wade-luffy/p/6292784.html">B+树介绍 - wade&amp;luffy - 博客园 (cnblogs.com)</a></p>
<h2 id="blogTitle0" class=" ">B+树<button class="cnblogs-toc-button" title="显示目录导航" aria-expanded="false"></button></h2>
<p class=" ">B+树和二叉树、平衡二叉树一样都是经典的数据结构。B+树由B树和索引顺序访问方法ISAM是不是很熟悉这也是MyISAM引擎最初参考的数据结构演化而来但是在实际使用过程中几乎已经没有使用B树的情况了。</p>
<p class=" ">B+树的定义十分复杂因此只简要地介绍B+树B+树是为磁盘或其他直接存取辅助设备而设计的一种平衡查找树在B+树中,所有记录节点都是按键值的大小顺序存放在同一层的叶节点中,各叶节点指针进行连接。</p>
<p class=" ">我们先来看一个B+树其高度为2每页可存放4条记录扇出fan out为5。</p>
<p><img src="https://images2015.cnblogs.com/blog/990532/201701/990532-20170117134342692-774752969.png" alt="" width="606" height="160" class="medium-zoom-image"></p>
<p>可以看出所有记录都在叶节点中并且是顺序存放的如果我们从最左边的叶节点开始顺序遍历可以得到所有键值的顺序排序5、10、15、20、25、30、50、55、60、65、75、80、85、90。</p>
<p><a name="_label0_0"></a></p>
<h3 id="blogTitle1" class=" ">B+树的插入操作<button class="cnblogs-toc-button" title="显示目录导航" aria-expanded="false"></button></h3>
<p><span style="color: rgba(255, 0, 0, 1)">B+树的插入必须保证插入后叶节点中的记录依然排序,</span>同时需要考虑插入B+树的三种情况每种情况都可能会导致不同的插入算法如表5-1所示。&nbsp;</p>
<p><img src="https://images2015.cnblogs.com/blog/990532/201701/990532-20170117134458755-1356536820.png" alt="" width="693" height="307" class="medium-zoom-image"></p>
<p>我们用实例来分析B+树的插入我们插入28这个键值发现当前Leaf Page和Index Page都没有满我们直接插入就可以了。</p>
<p><img src="https://images2015.cnblogs.com/blog/990532/201701/990532-20170117134602864-1401463251.png" alt="" width="649" height="159" class="medium-zoom-image"></p>
<p>这次我们再插入一条70这个键值这时原先的Leaf Page已经满了但是Index Page还没有满符合表5-1的第二种情况这时插入Leaf Page后的情况为50、55、60、65、70。我们根据中间的值60拆分叶节点。</p>
<p><img src="https://images2015.cnblogs.com/blog/990532/201701/990532-20170117134652286-620013872.png" alt="" width="552" height="194" class="medium-zoom-image"></p>
<p class=" ">因为图片显示的关系这次我没有能在各叶节点加上双向链表指针。最后我们来插入记录95这时符合表5-1讨论的第<span style="color: rgba(255, 0, 0, 1)">三种情况即Leaf Page和Index Page都满了这时需要做两次拆分。</span></p>
<p class=" "><img src="https://images2015.cnblogs.com/blog/990532/201701/990532-20170117134820692-680638285.png" alt="" width="518" height="234" class="medium-zoom-image"></p>
<p>&nbsp;</p>
<p class=" ">可以看到不管怎么变化B+树总是会保持平衡<span style="color: rgba(255, 0, 0, 1)">。但是为了保持平衡对于新插入的键值可能需要做大量的拆分页split操作而B+树主要用于磁盘因此页的拆分意味着磁盘的操作应该在可能的情况下尽量减少页的拆分。因此B+树提供了旋转rotation的功能。</span></p>
<p class=" ">旋转发生在Leaf Page已经满了、但是其左右兄弟节点没有满的情况下。<span style="color: rgba(255, 0, 0, 1)">这时B+树并不会急于去做拆分页的操作,而是将记录移到所在页的兄弟节点上</span>。通常情况下左兄弟被首先检查用来做旋转操作这时我们插入键值70其实B+树并不会急于去拆分叶节点而是做旋转505555旋转。</p>
<p>&nbsp;<img src="https://images2015.cnblogs.com/blog/990532/201701/990532-20170117135001442-1547516540.png" alt="" width="681" height="170" class="medium-zoom-image"></p>
<p>可以看到采用旋转操作使B+树减少了一次页的拆分操作而这时B+树的高度依然还是2。</p>
<p><a name="_label0_1"></a></p>
<h3 id="blogTitle2" class=" ">B+树的删除操作<button class="cnblogs-toc-button" title="显示目录导航" aria-expanded="false"></button></h3>
<p><span style="color: rgba(255, 0, 0, 1)">B+树使用填充因子fill factor来控制树的删除变化50%是填充因子可设的最小值。B+树的删除操作同样必须保证删除后叶节点中的记录依然排序同插入一样B+树的删除操作同样需要考虑如表5-2所示的三种情况与插入不同的是删除根据填充因子的变化来衡量。&nbsp;</span></p>
<p><img src="https://images2015.cnblogs.com/blog/990532/201701/990532-20170117135329114-221553173.png" alt="" width="778" height="212" class="medium-zoom-image"></p>
<p>首先删除键值为70的这条记录该记录符合表5-2讨论的第一种情况删除后。</p>
<p><img src="https://images2015.cnblogs.com/blog/990532/201701/990532-20170117135415692-75775186.png" alt="" width="562" height="244" class="medium-zoom-image"></p>
<p class=" ">接着我们删除键值为25的记录这也是表5-2讨论的第一种情况但是该值还是Index Page中的值因此在删除Leaf Page中25的值后还应将25的右兄弟节点的28更新到Page Index中最后可得到图。</p>
<p class=" "><img src="https://images2015.cnblogs.com/blog/990532/201701/990532-20170117135518630-1367658523.png" alt="" width="580" height="259" class="medium-zoom-image"></p>
<p>最后我们来看删除键值为60的情况删除Leaf Page中键值为60的记录后填充因子小于50%这时需要做合并操作同样在删除Index Page中相关记录后需要做Index Page的合并操作最后得到图。</p>
<p><img src="https://images2015.cnblogs.com/blog/990532/201701/990532-20170117135548927-1999158500.png" alt="" width="492" height="179" class="medium-zoom-image"></p>
<p><a href="https://zhuanlan.zhihu.com/p/79980618" rel="noopener nofollow">红黑树,超强动静图详解,简单易懂 - 知乎 (zhihu.com)</a></p>
<h2>红黑树<button class="cnblogs-toc-button" title="显示目录导航" aria-expanded="false"></button></h2>
<p>红黑树Red-Black Tree 「RBT」是一个自平衡(不是绝对的平衡)的二叉查找树(BST),树上的每个节点都遵循下面的规则:</p>
<ol>
<li><span style="color: rgba(255, 0, 0, 1)">每个节点都有红色或黑色</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">树的根始终是黑色的 (黑土地孕育黑树根, )</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">没有两个相邻的红色节点(红色节点不能有红色父节点或红色子节点,并没有说不能出现连续的黑色节点)</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">从节点包括根到其任何后代NULL节点(叶子结点下方挂的两个空节点,并且认为他们是黑色的)的每条路径都具有相同数量的黑色节点</span></li>
</ol>
<p>瞬间懵逼?了解一下印象就行,开始玩魔方都是要照着魔方公式一点点玩的,多玩几次就熟悉了。红黑树也一样,红黑树有两大操作:</p>
<ol>
<li><span style="color: rgba(255, 0, 0, 1)">recolor (重新标记黑色或红色)</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">rotation (旋转,这是树达到平衡的关键)</span></li>
</ol>
<h2 id="二-红黑树规则特点">二. 红黑树规则特点<button class="cnblogs-toc-button" title="显示目录导航" aria-expanded="false"></button></h2>
<p>红黑树具体有哪些规则特点呢?</p>
<blockquote><ol>
<li><span style="color: rgba(255, 0, 0, 1)">节点分为红色或者黑色;</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">根节点必为黑色;</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">叶子节点都为黑色且为null</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">连接红色节点的两个子节点都为黑色(红黑树不会出现相邻的红色节点);</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">从任意节点出发,到其每个叶子节点的路径中包含相同数量的黑色节点;</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">新加入到红黑树的节点为红色节点;</span></li>
</ol></blockquote>
<p>规则看着好像挺多没错因为红黑树也是均衡二叉树需要具备自动维持平衡的性质上面的6条就是红黑树给出的自动维持平衡所需要具备的规则</p>
<p>我们看一看一个典型的红黑树到底是什么样儿?</p>
<p><img src="https://img2018.cnblogs.com/blog/1677914/201907/1677914-20190717193539437-298743529.png" alt="" class="medium-zoom-image" loading="lazy"></p>
<p>首先解读一下规则,除了字面上看到的意思,还隐藏了哪些意思呢?</p>
<p><span style="color: rgba(255, 0, 0, 1)"><strong>第一. 从根节点到叶子节点的最长路径不大于最短路径的2倍</strong></span></p>
<p><strong>&emsp;&emsp;<span style="color: rgba(255, 0, 0, 1)">怎么样的路径算最短路径?</span></strong></p>
<p><span style="color: rgba(255, 0, 0, 1)">&emsp;&emsp;从规则5中我们知道从根节点到每个叶子节点的黑色节点数量是一样的那么纯由黑色节点组成的路径就是最短路径</span></p>
<p><span style="color: rgba(255, 0, 0, 1)"><strong>&emsp;&emsp;什么样的路径算是最长路径?</strong></span></p>
<p><span style="color: rgba(255, 0, 0, 1)">&emsp;&emsp;根据规则4和规则3若有红色节点则必然有一个连接的黑色节点当红色节点和黑色节点数量相同时就是最长路径也就是黑色节点或红色节点* 2</span></p>
<p><strong>第二. 为什么说新加入到红黑树中的节点为红色节点</strong></p>
<p>&emsp;<span style="color: rgba(255, 0, 0, 1)">&emsp;从规则4中知道当前红黑树中从根节点到每个叶子节点的黑色节点数量是一样的此时假如新的黑色节点的话必然破坏规则但加入红色节点却不一定除非其父节点就是红色节点因此加入红色节点破坏规则的可能性小一些</span>下面我们也会举例来说明。</p>
<p>什么情况下,红黑树的结构会被破坏呢?破坏后又怎么维持平衡,维持平衡主要通过两种方式【<strong>变色</strong>】和【<strong>旋转</strong>】,【<strong>旋转</strong>】又分【<strong>左旋</strong>】和【<strong>右旋</strong>】,两种方式可相互结合。</p>
<p>&nbsp;</p>
<p>我们会先尝试 recolor如果 recolor 不能达到红黑树的 4 点要求,然后我们尝试 rotation其实红黑树的关键玩法就是弄清楚 recolor 和 rotation 的规则,接下来看看详细的算法公式吧&nbsp;千万别着急记忆公式,有图示会逐步说明,就像魔方一样,多玩几次就懂了:<br>假设我们插入的新节点为 X</p>
<ol>
<li><span style="color: rgba(255, 0, 0, 1)">将新插入的节点标记为红色</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">如果 X 是根结点(root),则标记为黑色</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">如果 X 的 parent 不是黑色,同时 X 也不是 root:</span></li>
</ol>
<ul>
<li><span style="color: rgba(255, 0, 0, 1)">3.1 如果 X 的 uncle (叔叔) 是红色</span></li>
<ul>
<li><span style="color: rgba(255, 0, 0, 1)">3.1.1 将 parent 和 uncle 标记为黑色</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">3.1.2 将 grand parent (祖父) 标记为红色</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">3.1.3 让 X 节点的颜色与 X 祖父的颜色相同,然后重复步骤 2、3</span></li>
</ul>
</ul>
<p>话不多说,看下图</p>
<p>
<img src="https://pic4.zhimg.com/80/v2-1d35b926316397d82c267593042f8c3f_720w.jpg" width="598" class="origin_image zh-lightbox-thumb lazy medium-zoom-image" data-caption="" data-size="normal" data-rawwidth="598" data-rawheight="264" data-original="https://pic4.zhimg.com/v2-1d35b926316397d82c267593042f8c3f_r.jpg" data-actualsrc="https://pic4.zhimg.com/v2-1d35b926316397d82c267593042f8c3f_b.jpg" data-lazy-status="ok"></p>
<p>跟着上面的公式走:</p>
<ol>
<li><span style="color: rgba(255, 0, 0, 1)">将新插入的 X 节点标记为红色</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">发现 X 的 parent (P) 同样为红色,这违反了红黑树的第三条规则「不能有两个连续相邻的红色节点」</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">发现 X 的 uncle (U) 同样为红色</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">将 P 和 U 标记为黑色</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">将 X 和 X 的 grand parent (G) 标记为相同的颜色,即红色,继续重复公式 2、3</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">发现 G 是根结点,标记为黑色</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">结束</span></li>
</ol>
<p>刚刚说了 X 的 uncle 是红色的情况,接下来要说是黑色的情况</p>
<ol>
<li>如果 X 的 parent 不是黑色,同时 X 也不是 root:</li>
</ol>
<ul>
<li><span style="color: rgba(255, 0, 0, 1)">3.2 如果 X 的 uncle (叔叔) 是黑色,我们要分四种情况处理</span></li>
<ul>
<li><span style="color: rgba(255, 0, 0, 1)">3.2.1 左左 (P 是 G 的左孩子,并且 X 是 P 的左孩子)</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">3.2.2 左右 (P 是 G 的左孩子,并且 X 是 P 的右孩子)</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">3.2.3 右右 (和 3.2.1 镜像过来,恰好相反)</span></li>
<li><span style="color: rgba(255, 0, 0, 1)">3.2.4 右左 (和 3.2.2 镜像过来,恰好相反)</span></li>
</ul>
</ul>
<p>当出现 uncle 是黑色的时候我们第一步要考虑的是&nbsp;旋转&nbsp;,这里先请小伙伴不要关注红黑树的第 4 条规则,主要是为了演示如何旋转的,来一点点看,不要看图就慌,有解释的 :</p>
<h3>左左情况<button class="cnblogs-toc-button" title="显示目录导航" aria-expanded="false"></button></h3>
<p><span style="color: rgba(255, 0, 0, 1)">这种情况很简单,想象这是一根绳子,手提起 P 节点,然后变色即可</span></p>
<p>
<img src="https://pic1.zhimg.com/80/v2-9e139a0f8b4a5e00ca8e643e2130403c_720w.jpg" width="621" class="origin_image zh-lightbox-thumb lazy medium-zoom-image" data-caption="" data-size="normal" data-rawwidth="621" data-rawheight="231" data-original="https://pic1.zhimg.com/v2-9e139a0f8b4a5e00ca8e643e2130403c_r.jpg" data-actualsrc="https://pic1.zhimg.com/v2-9e139a0f8b4a5e00ca8e643e2130403c_b.jpg" data-lazy-status="ok"></p>
<h3>左右<button class="cnblogs-toc-button" title="显示目录导航" aria-expanded="false"></button></h3>
<p><span style="color: rgba(255, 0, 0, 1)">左旋: 使 X 的父节点 P 被 X 取代,同时父节点 P 成为 X 的左孩子,然后再应用&nbsp;左左情况</span></p>
<p>
<img src="https://pic4.zhimg.com/80/v2-3fb33fbb3a42e34ed8a058a047a44cc3_720w.jpg" width="762" class="origin_image zh-lightbox-thumb lazy medium-zoom-image" data-caption="" data-size="normal" data-rawwidth="762" data-rawheight="304" data-original="https://pic4.zhimg.com/v2-3fb33fbb3a42e34ed8a058a047a44cc3_r.jpg" data-actualsrc="https://pic4.zhimg.com/v2-3fb33fbb3a42e34ed8a058a047a44cc3_b.jpg" data-lazy-status="ok"></p>
<h3>右右<button class="cnblogs-toc-button" title="显示目录导航" aria-expanded="false"></button></h3>
<p><span style="color: rgba(255, 0, 0, 1)">左左情况一样,想象成一根绳子</span></p>
<p>
<img src="https://pic4.zhimg.com/80/v2-62a42ada09cb4547191aa4b9051c7c23_720w.jpg" width="646" class="origin_image zh-lightbox-thumb lazy medium-zoom-image" data-caption="" data-size="normal" data-rawwidth="646" data-rawheight="294" data-original="https://pic4.zhimg.com/v2-62a42ada09cb4547191aa4b9051c7c23_r.jpg" data-actualsrc="https://pic4.zhimg.com/v2-62a42ada09cb4547191aa4b9051c7c23_b.jpg" data-lazy-status="ok"></p>
<h3>右左<button class="cnblogs-toc-button" title="显示目录导航" aria-expanded="false"></button></h3>
<p><span style="color: rgba(255, 0, 0, 1)">右旋: 使 X 的父节点 P 被 X 取代,同时父节点 P 成为 X 的右孩子,然后再应用&nbsp;右右情况</span></p>
<p>
<img src="https://pic4.zhimg.com/80/v2-210d1400ea1b098dfe1582589a6064c3_720w.jpg" width="727" class="origin_image zh-lightbox-thumb lazy medium-zoom-image" data-caption="" data-size="normal" data-rawwidth="727" data-rawheight="312" data-original="https://pic4.zhimg.com/v2-210d1400ea1b098dfe1582589a6064c3_r.jpg" data-actualsrc="https://pic4.zhimg.com/v2-210d1400ea1b098dfe1582589a6064c3_b.jpg" data-lazy-status="ok"></p>
<p>你说的动图在哪里,你个大骗子,别着急,现在就为小伙伴们奉上动图演示,来说明公式的使用:</p>
<h3>案例一<button class="cnblogs-toc-button" title="显示目录导航" aria-expanded="false"></button></h3>
<blockquote>插入 10203015 到一个空树中</blockquote>
<ol>
<li>向空树中第一次插入数字 10肯定是 root 节点</li>
<li>root 节点标记成黑色</li>
</ol>
<div class="RichText-gifPlaceholder">
<div class="GifPlayer" data-size="normal" data-za-detail-view-path-module="GifItem"><img src="https://pic4.zhimg.com/v2-1177002a8fe1ee9d85befa063fb68db7_b.jpg" class="ztext-gif medium-zoom-image" data-thumbnail="https://pic4.zhimg.com/v2-1177002a8fe1ee9d85befa063fb68db7_b.jpg" data-size="normal"></div>
</div>
<ol>
<li>向树中插入新节点 20标记为红色</li>
<li>20 &gt; 10并发现 10 没有叶子节点,将新节点 20 作为 10 的右孩子</li>
</ol>
<div class="RichText-gifPlaceholder">
<div class="GifPlayer" data-size="normal" data-za-detail-view-path-module="GifItem"><img src="https://pic1.zhimg.com/v2-193cf644282b7a6792990c8d4e804010_b.jpg" class="ztext-gif GifPlayer-gif2mp4Image medium-zoom-image" data-thumbnail="https://pic1.zhimg.com/v2-193cf644282b7a6792990c8d4e804010_b.jpg" data-size="normal"></div>
</div>
<ol>
<li>向树中插入新节点 30标记为红色</li>
<li>30 &gt; 10查找 10 的右子树,找到 20</li>
<li>30 &gt; 20继续查找 20 的右子树,发现 20 没有叶子节点,将值插在此处</li>
<li>30 和 20 节点都为红色30 为右孩子20 也为右孩子,触发了&nbsp;右右情况</li>
<li>通过一次旋转,提起 20 节点</li>
<li>20 节点是根结点,标记为黑色</li>
</ol>
<div class="RichText-gifPlaceholder">
<div class="GifPlayer" data-size="normal" data-za-detail-view-path-module="GifItem"><img src="https://pic2.zhimg.com/v2-bded9a7bc35cfbdbf9abb46c526a9d49_b.jpg" class="ztext-gif GifPlayer-gif2mp4Image medium-zoom-image" data-thumbnail="https://pic2.zhimg.com/v2-bded9a7bc35cfbdbf9abb46c526a9d49_b.jpg" data-size="normal"></div>
</div>
<ol>
<li>向树中插入新节点 15标记为红色</li>
<li>通过比对大小和判断是否有叶子节点,最终插值为 10 节点的右孩子</li>
<li>15 和 10 节点都为红色15 的 uncle 节点 30 也为红色</li>
<li>按照公式,将 15 的 parent 10 和 uncle 30 更改为黑色</li>
<li>让 15 节点 grand parent 20 的颜色与 15 节点的颜色一样,变为红色</li>
<li>20 为根结点,将其改为黑色</li>
</ol>
<div class="RichText-gifPlaceholder">
<div class="GifPlayer" data-size="normal" data-za-detail-view-path-module="GifItem"><img src="https://pic4.zhimg.com/v2-e2efcc245d4e75aa03662886c6f49bf7_b.jpg" class="ztext-gif GifPlayer-gif2mp4Image medium-zoom-image" data-thumbnail="https://pic4.zhimg.com/v2-e2efcc245d4e75aa03662886c6f49bf7_b.jpg" data-size="normal"></div>
</div>
<blockquote>继续插入其他节点只不过反复应用上面的公式,上面应用到的红黑树工具,可以暂停动画效果,一帧一帧的看红黑树的转换过程,这样通过练习,查看公式,观察变化三管齐下,红黑树的入门理解应该完全不再是问题了</blockquote>
<blockquote>
<h2 id="四-红黑树节点删除">四. 红黑树节点删除<button class="cnblogs-toc-button" title="显示目录导航" aria-expanded="false"></button></h2>
<p>相比较于红黑树的节点插入,删除节点更为复杂,<span style="color: rgba(255, 0, 0, 1)">我们从子节点是否为null和红色为思考维度来讨论。</span></p>
<h3 id="41-子节点至少有一个为null"><span style="color: rgba(255, 0, 0, 1)">4.1 子节点至少有一个为null</span><button class="cnblogs-toc-button" title="显示目录导航" aria-expanded="false"></button></h3>
<p><span style="color: rgba(255, 0, 0, 1)">当待删除的节点的子节点至少有一个为null节点时删除了该节点后将其有值的节点取代当前节点即可若都为null则将当前节点设置为null当然如果违反规则了则按需调整如【变色】以及【旋转】。</span></p>
<p><img src="https://img2018.cnblogs.com/blog/1677914/201907/1677914-20190717193902618-444201269.png" alt="" class="medium-zoom-image" loading="lazy"></p>
<h3 id="42-子节点都是非null节点"><span style="color: rgba(255, 0, 0, 1)">4.2 子节点都是非null节点</span><button class="cnblogs-toc-button" title="显示目录导航" aria-expanded="false"></button></h3>
<p>这种情况下,</p>
<p><span style="color: rgba(255, 0, 0, 1)"><strong>第一步:找到该节点的前驱或者后继</strong></span></p>
<p>前驱:<strong>左子树中值最大的节点</strong>可得出其最多只有一个非null子节点可能都为null</p>
<p>后继:<strong>右子树中值最小的节点</strong>可得出其最多只有一个非null子节点可能都为null</p>
<p>前驱和后继都是值最接近该节点值的节点,类似于该节点.prev = 前驱,该节点.next = 后继。</p>
<p><span style="color: rgba(255, 0, 0, 1)"><strong>第二步:将前驱或者后继的值复制到该节点中,然后删掉前驱或者后继</strong></span></p>
<p>如果删除的是左节点,则将前驱的值复制到该节点中,然后删除前驱;如果删除的是右节点,则将后继的值复制到该节点中,然后删除后继;</p>
<p>这相当于是一种“取巧”的方法,我们删除节点的目的是使该节点的值在红黑树上不存在,因此专注于该目的,我们并不关注删除节点时是否真是我们想删除的那个节点,同时我们也不需考虑树结构的变化,因为树的结构本身就会因为自动平衡机制而经常进行调整。</p>
<p>前面我们已经说了,我们要删除的实际上是前驱或者后继,因此我们就以前驱为主线来讲解,后继的学习可参考前驱,包括几种情况</p>
<h4 id="421-前驱为黑色节点并且有一个非null子节点">4.2.1 前驱为黑色节点并且有一个非null子节点<button class="cnblogs-toc-button" title="显示目录导航" aria-expanded="false"></button></h4>
<p><img src="https://img2018.cnblogs.com/blog/1677914/201907/1677914-20190717193916264-216284837.png" alt="" class="medium-zoom-image" loading="lazy"></p>
<p>分析:</p>
<p>因为要删除的是左节点64<strong>找到</strong>该节点的前驱63</p>
<p>然后用前驱的值63<strong>替换</strong>待删除节点的值64此时两个节点待删除节点和前驱的值都为63</p>
<p><strong>删除</strong>前驱63此时成为上图过程中间环节但我们发现其不符合红黑树规则4因此需要进行自动平衡调整</p>
<p>这里直接通过【<strong>变色</strong>】即可完成。</p>
<h4 id="422-前驱为黑色节点同时子节点都为null"><span style="color: rgba(255, 0, 0, 1)">4.2.2 前驱为黑色节点同时子节点都为null</span><button class="cnblogs-toc-button" title="显示目录导航" aria-expanded="false"></button></h4>
<p><img src="https://img2018.cnblogs.com/blog/1677914/201907/1677914-20190717193929645-127994990.png" alt="" class="medium-zoom-image" loading="lazy"></p>
<p>分析:</p>
<p>因为要删除的是左节点64<strong>找到</strong>该节点的前驱63</p>
<p>然后用前驱的值63<strong>替换</strong>待删除节点的值64此时两个节点待删除节点和前驱的值都为63</p>
<p><strong>删除</strong>前驱63此时成为上图过程中间环节但我们发现其不符合红黑树规则5因此需要进行自动平衡调整</p>
<p>这里直接通过【<strong>变色</strong>】即可完成。</p>
<h4 id="423-前驱为红色节点同时子节点都为null"><span style="color: rgba(255, 0, 0, 1)">4.2.3 前驱为红色节点同时子节点都为null</span><button class="cnblogs-toc-button" title="显示目录导航" aria-expanded="false"></button></h4>
<p><img src="https://img2018.cnblogs.com/blog/1677914/201907/1677914-20190717193941474-1382279418.png" alt="" class="medium-zoom-image" loading="lazy"></p>
<p>分析:</p>
<p>因为要删除的是左节点64<strong>找到</strong>该节点的前驱63</p>
<p>然后用前驱的值63<strong>替换</strong>待删除节点的值64此时两个节点待删除节点和前驱的值都为63</p>
<p><strong>删除</strong>前驱63树的结构并没有打破规则。</p>
<h3 id="43-红黑树删除总结">4.3 红黑树删除总结<button class="cnblogs-toc-button" title="显示目录导航" aria-expanded="false"></button></h3>
<p>红黑树删除的情况比较多,但也就存在以下情况:</p>
<p><span style="color: rgba(255, 0, 0, 1)">删除的是根节点则直接将根节点置为null;</span></p>
<p><span style="color: rgba(255, 0, 0, 1)">待删除节点的左右子节点都为null删除时将该节点置为null;</span></p>
<p><span style="color: rgba(255, 0, 0, 1)">待删除节点的左右子节点有一个有值,则用有值的节点替换该节点即可;</span></p>
<p><span style="color: rgba(255, 0, 0, 1)">待删除节点的左右子节点都不为null则找前驱或者后继将前驱或者后继的值复制到该节点中然后删除前驱或者后继</span></p>
<p><span style="color: rgba(255, 0, 0, 1)">节点删除后可能会造成红黑树的不平衡,这时我们需通过【变色】+【旋转】的方式来调整,使之平衡,上面也给出了例子,建议大家多多练习,而不必背下来。</span></p>
<p>B*树</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;是B+树的变体在B+树的非根和非叶子结点再增加指向兄弟的指针;</p>
<p><img src="https://img2020.cnblogs.com/blog/1363376/202108/1363376-20210808113710983-2129702265.png" alt="" loading="lazy" class="medium-zoom-image"></p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><img src="https://p-blog.csdn.net/images/p_blog_csdn_net/manesking/6.JPG" alt="" class="has medium-zoom-image"></p>
<p>&nbsp;&nbsp;&nbsp;B*树定义了非叶子结点关键字个数至少为(2/3)*M即块的最低使用率为2/3</p>
<p>代替B+树的1/2</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; B+树的分裂当一个结点满时分配一个新的结点并将原结点中1/2的数据</p>
<p>复制到新结点最后在父结点中增加新结点的指针B+树的分裂只影响原结点和父</p>
<p>结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针;</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; B*树的分裂:当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分</p>
<p>数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字</p>
<p>(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之</p>
<p>间增加新结点并各复制1/3的数据到新结点最后在父结点增加新结点的指针</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;所以B*树分配新结点的概率比B+树要低,空间使用率更高;</p>
<p><strong><span style="font-size: 14pt; color: rgba(255, 0, 0, 1)">面试题:红黑树相比于BST和AVL树有什么优点</span></strong><br>&nbsp;&nbsp;&nbsp;红黑树是牺牲了严格的高度平衡的优越条件为代价,它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。<br>红黑树能够以O(log&nbsp;n)的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。<br>当然,还有一些更好的,但实现起来更复杂的数据结构能够做到一步旋转之内达到平衡,但红黑树能够给我们一个比较“便宜”的解决方案。<br><br>&nbsp;&nbsp;&nbsp;&nbsp;相比于BST因为红黑树可以能确保树的最长路径不大于两倍的最短路径的长度所以可以看出它的查找效果是有最低保证的。在最坏的情况下也可以保证O(logN)的这是要好于二叉查找树的。因为二叉查找树最坏情况可以让查找达到O(N)。红黑树的算法时间复杂度和AVL相同但统计性能比AVL树更高所以在插入和删除中所做的后期维护操作肯定会比红黑树要耗时好多但是他们的查找效率都是O(logN)所以红黑树应用还是高于AVL树的.&nbsp;实际上插入&nbsp;AVL&nbsp;树和红黑树的速度取决于你所插入的数据。如果你的数据分布较好,则比较宜于采用&nbsp;AVL树(例如随机产生系列数),但是如果你想处理比较杂乱的情况,则红黑树是比较快的。</p>
<p><strong><span style="color: rgba(255, 0, 0, 1); font-size: 18px">为什么要要用红黑树?</span></strong><br><br>1、首先红黑树是不符合AVL树的平衡条件的即每个节点的左子树和右子树的高度最多差1的二叉查找树。但是提出了为节点增加颜色红黑树是用非严格的平衡来换取增删节点时候旋转次数的降低任何不平衡都会在三次旋转之内解决而AVL是严格平衡树因此在增加或者删除节点的时候根据不同情况旋转的次数比红黑树要多。所以红黑树的插入效率更高<br><br>(更多相关面试题推荐java面试题及答案)<br><br>2、红黑树能够以O(log2&nbsp;(n))&nbsp;的时间复杂度进行搜索、插入、删除操作<br><br>3、简单来说红黑树就是为了解决二叉查找树的缺陷因为二叉查找树在某些情况下会退化成一个线性结构。<br><br>红黑树和平衡树的对比和选择<br><br>1、平衡树结构更加直观读取性能比红黑树要高增加和删除节点&nbsp;恢复平衡的性能不如红黑树<br><br>2、红黑树读取性能不如平衡树增加和删除节点&nbsp;恢复平衡性能比平衡树好<br><br>红黑树的使用场景:<br><br>TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树</p>
<h2 id="五-总结">五. 总结<button class="cnblogs-toc-button" title="显示目录导航" aria-expanded="false"></button></h2>
<p>本文主要介绍了红黑树的相关原理首先红黑树的基础二叉搜索树我们先简单说了一下二叉搜索树并且讲了一下搜索的流程然后就针对红黑树的6大规则特点红黑树的插入操作删除操作都使用了大量的图形来加以说明技术都是练出来的有时候很多似是而非的地方当动笔去写的时候其实很好理解。红黑树的使用非常广泛如TreeMap和TreeSet都是基于红黑树实现的而Jdk8中HashMap当链表长度大于8时也会转化为红黑树红黑树比较复杂本人也是还在学习过程中如果有不对的地方请批评指正望共同进步谢谢。</p>
<p>&nbsp;</p>
</blockquote>
</div>
<div id="MySignature" role="contentinfo">
<div>作者:<a href="http://www.cnblogs.com/acgoto/" target="_blank">你的雷哥</a></div>
<div>出处:<a href="http://www.cnblogs.com/acgoto/" target="_blank">https://www.cnblogs.com/henuliulei/</a></div>
<div>本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面给出原文连接,否则保留追究法律责任的权利。 </div>
</div>
<div class="clear"></div>
<div id="blog_post_info_block" role="contentinfo">
<script type="text/javascript">
currentDiggType = 0;
</script>
</div>
<div class="clear"></div>
<div id="post_next_prev">
<a href="https://www.cnblogs.com/henuliulei/p/15032154.html" class="p_n_p_prefix">« </a> 上一篇: <a href="https://www.cnblogs.com/henuliulei/p/15032154.html" data-featured-image="" title="发布于 2021-07-28 10:57">基础巩固_数据结构严蔚敏复习</a>
<br>
<a href="https://www.cnblogs.com/henuliulei/p/15120742.html" class="p_n_p_prefix">» </a> 下一篇: <a href="https://www.cnblogs.com/henuliulei/p/15120742.html" data-featured-image="" title="发布于 2021-08-09 21:25">漫画算法_小灰灰面试</a>
</div>
</div>
</div>
<div class="postDesc">posted @
<span id="post-date" data-last-update-days="0.006944444444444444" data-date-updated="2021-08-08 11:49">2021-08-08 11:39</span>&nbsp;
<a href="https://www.cnblogs.com/henuliulei">你的雷哥</a>&nbsp;
阅读(<span id="post_view_count">13316</span>)&nbsp;
评论(<span id="post_comment_count">3</span>)&nbsp;
&nbsp;
<a href="javascript:void(0)" onclick="AddToWz(15114440);return false;">收藏</a>&nbsp;
<a href="javascript:void(0)" onclick="reportManager.report({ currentUserId: '', targetType: 'blogPost', targetId: '15114440', targetLink: 'https://www.cnblogs.com/henuliulei/p/15114440.html', title: 'b树b+树b-树,红黑树详解一锅端' })">举报</a>
</div>
</div>
</div><!--end: topics 文章、评论容器-->
</div>
B 树的 **删除操作**比插入复杂一些,因为必须保持 B 树的 **平衡性**(每个节点 key 数量在 `[t-1, 2t-1]` 之间)和 **children 数量 = keys+1** 的不变量。删除操作主要分几种情况,需要按**从根到叶子**的思路递归处理。
---
## 1⃣ 删除的三种基本情况
设我们要删除 key `k`
### **情况 1key 在叶子节点上**
* 直接删除 key 即可。
* 删除后如果叶子节点仍然有 ≥ t-1 个 key则无需进一步处理。
* 如果 key 数 < t-1则需要从兄弟节点借 key 或合并节点下文详述)。
---
### **情况 2key 在内部节点上**
假设 key `k` 在内部节点 `x` `x->keys[i] = k`
* **情况 2a前驱 key 所在子树 y key t**
* 找到 k **前驱 key**左子树最大 key记作 `pred`
* `pred` 替换 `k`
* 递归删除 `pred`它一定在叶子节点或内部节点里)。
* **情况 2b后继 key 所在子树 z key t**
* 找到 k **后继 key**右子树最小 key记作 `succ`
* `succ` 替换 `k`
* 递归删除 `succ`
* **情况 2c左右子树 key 数都 = t-1**
* 合并左右子树和 key k形成一个新的节点key = 2t-1
* 递归在合并后的节点中删除 k
---
### **情况 3key 不在内部节点上**
* 找到 key 所在的子树child i)。
* **保证递归前子树至少有 t key**
1. 如果 child\[i] key = t-1需要先借 key 或合并
* **向左或右兄弟借 key**兄弟 key t
* 父节点 key 下移到 child兄弟 key 上移到父节点
* **兄弟 key = t-1**
* 与兄弟合并父节点 key 下移到合并后的节点
* 递归在 child\[i] 中删除 key
---
## 2⃣ 借 key / 合并的规则
* ** key**
* 从左兄弟借左兄弟的最大 key 上移到父节点父节点对应 key 下移到 child
* 从右兄弟借右兄弟的最小 key 上移到父节点父节点对应 key 下移到 child
* **合并**
* child 与兄弟以及父节点中间 key 合并成一个节点
* 父节点 key 数减 1child 数减 1
* 可能递归到 root如果 root key = 0根节点被删除树高度减 1
---
## 3⃣ 删除操作的递归流程
伪代码大致如下顶层函数 `deleteKey(root, k)`
```text
deleteKey(node, k):
if node is leaf:
if k in node.keys:
remove k from node.keys
return
if k in node.keys:
if left child has >= t keys:
pred = max(left child)
replace k with pred
deleteKey(left child, pred)
else if right child has >= t keys:
succ = min(right child)
replace k with succ
deleteKey(right child, succ)
else:
merge left child, k, right child
deleteKey(merged child, k)
else:
child = subtree where k should be
if child.keys.size() == t-1:
if left/right sibling has >= t keys:
borrow from sibling
else:
merge with sibling
deleteKey(child, k)
```
---
## 4⃣ 关键点总结
1. **删除前保证递归子树 key 数 ≥ t**避免递归时下溢
2. ** key 或合并节点**确保 B 树不违反最小度数限制
3. **根节点特殊处理**删除后如果 root 没有 key树高度减 1
4. **递归**是实现删除的核心思路从上到下保证每个节点在递归时合法
---
对的你理解完全正确我帮你梳理一下
---
### 在 B-树删除时,如果要删除的关键字 `x->keys[i]` 在**内部节点**
* **前驱predecessor**
* 就是 `x->children[i]` 这个子树中的最大值
* 为什么因为 `x->children[i]` 里所有关键字都 **小于** `x->keys[i]`而前驱就是其中最大的
* 找法一路往右孩子走直到叶子拿到最后一个 key
* 伪代码
```cpp
Node<T>* cur = x->children[i];
while (!cur->isLeaf) cur = cur->children.back();
T predecessor = cur->keys.back();
```
* **后继successor**
* 就是 `x->children[i+1]` 这个子树中的最小值。
* 为什么?因为 `x->children[i+1]` 里所有关键字都 **大于** `x->keys[i]`,而后继就是其中最小的。
* 找法:一路往左孩子走直到叶子,拿到第一个 key。
* 伪代码:
```cpp
Node<T>* cur = x->children[i+1];
while (!cur->isLeaf) cur = cur->children.front();
T successor = cur->keys.front();
```
---
### 删除时的策略
* 如果前驱所在子树 `x->children[i]` 有 ≥ t 个 key
* 用前驱替代 `x->keys[i]`,再递归删除前驱。
* 否则如果后继所在子树 `x->children[i+1]` 有 ≥ t 个 key
* 用后继替代 `x->keys[i]`,再递归删除后继。
* 否则(两个子树都只有 t-1 个 key
* 把 `x->keys[i]` 下移到左子树,和右子树合并,再递归删除。
---
📘 **直观理解**
`x->keys[i]` 在父节点,它左边整个子树都比它小,右边整个子树都比它大。
所以前驱就是左子树里最大值,后继就是右子树里最小值。
---
要不要我帮你画一个 **图解例子**(比如 `t=2` 的 B 树,删除一个内部节点的情况),演示「前驱替代」和「后继替代」的区别?
如果你需要,我可以帮你 **写一个完整的 C++ B 树删除函数实现**,直接和你已有的 `BTree` 类整合,并附详细注释。
你希望我帮你写吗?
---
``` html
总结:
1、删除
2、查看是否下溢出
3、下溢出
a.左右节点可以借(大于t - 1)
父亲下来,借的成为父亲
b.不可以借
和兄弟合并
总结得很到位 👍,我帮你整理一下,更系统一些,方便你以后复习 B-树删除算法。
```
---
## 🌳 B-树删除算法总结
### 1. 删除目标
* **在叶子节点找到 key**
👉 直接删除,不涉及复杂调整。
* **在内部节点找到 key**
👉 不能直接删,而是通过 **前驱 / 后继替代** 或 **合并** 来保持 B-树平衡。
---
### 2. 删除步骤总览
1. **删除目标 key**
* 在叶子:直接删。
* 在内部:找前驱或后继替换,再递归删除。
2. **检查是否下溢underflow**
* B-树的每个节点至少要有 `t - 1` 个 key除了根
* 如果某个子节点删除后不足 `t - 1`,则发生下溢。
3. **处理下溢**
* **a. 向兄弟借关键字(兄弟 ≥ t**
* 父节点“下放”一个关键字到下溢的节点,兄弟的关键字“上升”到父节点。
* 保证节点恢复到 ≥ `t - 1`。
* 📌 这是 **旋转rotation** 操作。
* **b. 无法借(兄弟也是 t - 1 → 合并**
* 将下溢节点、父节点中一个关键字、兄弟节点 **合并** 成一个节点。
* 父节点关键字数减少 1。
* 如果父节点变空,整棵树高度减 1根被替换
---
### 3. 前驱 / 后继选择
* **前驱**`x->keys[i]` 的左子树 `x->children[i]` 中的最大值。
* **后继**`x->keys[i]` 的右子树 `x->children[i+1]` 中的最小值。
* 替代规则:
* 左子树有足够关键字(≥ t用前驱替代。
* 否则若右子树有足够关键字(≥ t用后继替代。
* 否则左右子树都不够,执行合并。
---
### 📌 最简总结版(你说的很对)
1. 删除目标 key。
2. 如果节点关键字数 ≥ `t - 1`,不用处理。
3. 如果 < `t - 1`(下溢):
* **能借** → 借兄弟。
* **不能借** → 合并兄弟和父节点关键字。
---
要不要我帮你画一个小例子(`t=2` 的 B-树),演示 **借****合并** 两种下溢处理,这样你一眼就能看懂?