B-Tree
This commit is contained in:
135
BinaryTree/BalanceTree/B-Tree/BTree.h
Normal file
135
BinaryTree/BalanceTree/B-Tree/BTree.h
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <iostream>
|
||||||
|
#include "Node.h"
|
||||||
|
#include <algorithm>
|
||||||
|
template <class T>
|
||||||
|
class BTree {
|
||||||
|
private:
|
||||||
|
Node<T>* root;
|
||||||
|
int t; //*B-<2D><><EFBFBD>Ľ<EFBFBD><C4BD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ڵ<EFBFBD><DAB5><EFBFBD><EFBFBD><EFBFBD>Ԫ<EFBFBD><D4AA><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD>룩
|
||||||
|
//<2F><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>t<EFBFBD><74><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ÿ<EFBFBD><C3BF><EFBFBD>ڵ<EFBFBD><DAB5>϶<EFBFBD><CFB6><EFBFBD>t - 1 <20><> 2t - 1
|
||||||
|
|
||||||
|
void splitChild(Node<T>* _node, int _n);
|
||||||
|
void insertNonFull(Node<T>* _node, const T& _key);
|
||||||
|
void inorder(Node<T>* _n) {
|
||||||
|
if (!_n) return;
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
//<2F><>ע<EFBFBD><D7A2><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD>inorder(children[0]) -> inorder(key[0]) -> inorder(children[1])... <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD><D6A4><EFBFBD><EFBFBD>
|
||||||
|
for (i = 0; i < _n->keys.size(); i++) {
|
||||||
|
if (i < _n->children.size()) inorder(_n->children[i]);
|
||||||
|
std::cout << _n->keys[i] << " ";
|
||||||
|
}
|
||||||
|
//<2F>Ѿ<EFBFBD>i++
|
||||||
|
if (i < _n->children.size()) inorder(_n->children[i]);
|
||||||
|
std::cout << std::endl;
|
||||||
|
}
|
||||||
|
public:
|
||||||
|
BTree() = delete;
|
||||||
|
BTree(int _t) : t(_t), root(nullptr) {}
|
||||||
|
|
||||||
|
void display() { inorder(root); }
|
||||||
|
void insert(const T& _key);
|
||||||
|
bool search(Node<T>* _node, const T& _key);
|
||||||
|
void erase(const T& _key);
|
||||||
|
};
|
||||||
|
|
||||||
|
template<class T>
|
||||||
|
inline void BTree<T>::splitChild(Node<T>* _pt, int _i)
|
||||||
|
{
|
||||||
|
//_pt-<2D><><EFBFBD>ڵ<EFBFBD>//_i-<2D>ڼ<EFBFBD><DABC><EFBFBD><EFBFBD>ӽڵ<D3BD>//t-<2D>м<EFBFBD>Ԫ<EFBFBD><D4AA>
|
||||||
|
Node<T>* p = _pt->children[_i];
|
||||||
|
Node<T>* tmp = new Node<T>(p->isLeaf);
|
||||||
|
int mid = t - 1;
|
||||||
|
//<2F><><EFBFBD><EFBFBD>mid<69>Ҳ<EFBFBD>
|
||||||
|
for (int i = 0; i < mid; i++) {
|
||||||
|
tmp->keys.push_back(p->keys[t + i]);
|
||||||
|
}
|
||||||
|
//<2F><><EFBFBD><EFBFBD>Ҷ<EFBFBD>ӽڵ<D3BD>-<2D><><EFBFBD>ƺ<EFBFBD><C6BA><EFBFBD>
|
||||||
|
if (!p->isLeaf) {
|
||||||
|
for (int i = 0; i < t; i++) {
|
||||||
|
tmp->children.push_back(p->children[i + t]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
T middlekey = p->keys[mid];
|
||||||
|
//<2F><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
p->keys.resize(mid);
|
||||||
|
if (!p->isLeaf) p->children.resize(t);//t = mid + 1
|
||||||
|
|
||||||
|
//<2F><><EFBFBD>Ӻ<EFBFBD><D3BA><EFBFBD>
|
||||||
|
_pt->children.insert(_pt->children.begin() + _i + 1, tmp);
|
||||||
|
//<2F><>ȡ<EFBFBD>м<EFBFBD>Ԫ<EFBFBD>ز<EFBFBD><D8B2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
_pt->keys.insert(_pt->keys.begin() + _i, middlekey);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<class T>
|
||||||
|
inline void BTree<T>::insertNonFull(Node<T>* _node, const T& _key)
|
||||||
|
{
|
||||||
|
int i = _node->keys.size() - 1;
|
||||||
|
//<2F><>Ҷ<EFBFBD>ӽڵ<D3BD>-<2D>Ȳ<EFBFBD><C8B2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>O(n)
|
||||||
|
if (_node->isLeaf) {
|
||||||
|
_node->keys.push_back(_key);
|
||||||
|
//<2F><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>λ
|
||||||
|
while (i >= 0 && _node->keys[i] > _key) {
|
||||||
|
_node->keys[i + 1] = _node->keys[i];
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
_node->keys[i + 1] = _key;
|
||||||
|
}
|
||||||
|
//<2F><><EFBFBD><EFBFBD>Ҷ<EFBFBD>ӽڵ㣬<DAB5>ݹ<EFBFBD><DDB9><EFBFBD><EFBFBD><EFBFBD>Ѱ<EFBFBD><D1B0>Ҷ<EFBFBD>ӽڵ<D3BD>
|
||||||
|
else {
|
||||||
|
while (i >= 0 && _node->keys[i] > _key) {
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
i++;//<2F>ҵ<EFBFBD>childrenλ<6E><CEBB>
|
||||||
|
//<2F>ڵ<EFBFBD><DAB5><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȷ<EFBFBD><C8B7><EFBFBD>
|
||||||
|
if (_node->children[i]->keys.size() == 2 * t - 1) {
|
||||||
|
splitChild(_node, i);
|
||||||
|
//<2F><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ܸı<DCB8><C4B1>ؼ<EFBFBD><D8BC><EFBFBD>λ<EFBFBD><CEBB>
|
||||||
|
if (_key > _node->keys[i]) i++;
|
||||||
|
}
|
||||||
|
insertNonFull(_node->children[i], _key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template<class T>
|
||||||
|
inline void BTree<T>::insert(const T& _key)
|
||||||
|
{
|
||||||
|
//1-<2D><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>root
|
||||||
|
if (!root) {
|
||||||
|
root = new Node<T>(true);
|
||||||
|
root->keys.push_back(_key);
|
||||||
|
}
|
||||||
|
//2-<2D><>root<6F><74><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
else {
|
||||||
|
//root<6F><74><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ѹ<EFBFBD><D1B8>ڵ<EFBFBD>
|
||||||
|
//<2F>ص<EFBFBD><D8B5><EFBFBD>ע:<3A><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><EFBFBD><F2A3ACB2>ܹؼ<DCB9><D8BC><EFBFBD><EFBFBD>Ƿ<EFBFBD><C7B7><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ڵ㣬<DAB5><E3A3AC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ڵ<EFBFBD><DAB5><EFBFBD><EFBFBD>ˣ<EFBFBD><CBA3><EFBFBD>Ҫ<EFBFBD><D2AA><EFBFBD><EFBFBD><EFBFBD>Է<EFBFBD><D4B7><EFBFBD>һ
|
||||||
|
if (root->keys.size() == 2*t - 1) {
|
||||||
|
Node<T>* _root = new Node<T>(false);
|
||||||
|
_root->children.push_back(root);
|
||||||
|
splitChild(_root, 0);
|
||||||
|
root = _root;
|
||||||
|
}
|
||||||
|
//<2F><><EFBFBD><EFBFBD>ִ<EFBFBD><D6B4>
|
||||||
|
insertNonFull(root, _key);
|
||||||
|
}
|
||||||
|
//<2F><> t = 2<><32>root Ŀǰ<C4BF><C7B0> 2 <20><>Ԫ<EFBFBD>أ<EFBFBD><D8A3><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD><EFBFBD>Ԫ<EFBFBD>غ<EFBFBD><D8BA><EFBFBD>root <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> 3 <20><>Ԫ<EFBFBD>أ<EFBFBD><D8A3><EFBFBD><EFBFBD>ڵ㣩<DAB5><E3A3A9><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѣ<EFBFBD><D1A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֻ<EFBFBD><D6BB><EFBFBD><EFBFBD>һ<EFBFBD>β<EFBFBD><CEB2>볬<EFBFBD><EBB3AC> 3 <20><>Ԫ<EFBFBD><D4AA>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
}
|
||||||
|
|
||||||
|
template<class T>
|
||||||
|
inline bool BTree<T>::search(Node<T>* _node, const T& _key)
|
||||||
|
{
|
||||||
|
//<2F><><EFBFBD>Եݹ<D4B5><DDB9><EFBFBD>
|
||||||
|
Node<T>* tmp = _node;
|
||||||
|
int i = 0;
|
||||||
|
while (tmp->keys[i] < _key && i < tmp->keys.size()) i++;
|
||||||
|
if (tmp->keys[i] == _key) return true;
|
||||||
|
if(tmp->isLeaf) return false;
|
||||||
|
return search(_node->children[i], _key);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<class T>
|
||||||
|
inline void BTree<T>::erase(const T& _key)
|
||||||
|
{
|
||||||
|
//1-ɾ<><C9BE>Ҷ<EFBFBD><D2B6>
|
||||||
|
}
|
11
BinaryTree/BalanceTree/B-Tree/Node.h
Normal file
11
BinaryTree/BalanceTree/B-Tree/Node.h
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <vector>
|
||||||
|
template <class T>
|
||||||
|
class Node {
|
||||||
|
public:
|
||||||
|
std::vector<T> keys;
|
||||||
|
std::vector<Node<T>*> children; //<2F><>Сkeys.size() + 1
|
||||||
|
bool isLeaf;
|
||||||
|
|
||||||
|
Node(bool l) : isLeaf(l) {}
|
||||||
|
};
|
574
BinaryTree/BalanceTree/B-Tree/README.MD
Normal file
574
BinaryTree/BalanceTree/B-Tree/README.MD
Normal file
@@ -0,0 +1,574 @@
|
|||||||
|
|
||||||
|
<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> </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> </p>
|
||||||
|
<p><strong>定义</strong></p>
|
||||||
|
<p>B树是一种平衡的多分树,通常我们说m阶的B树,它必须满足如下条件: </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> -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<= M <=m,M为子节点数量;包含的元素数量 1<= K <=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)<= M <=m关系式,包含元素数量M-1;包含的元素数量 (m/2)-1<= K <=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<= K <=m-1。</span></p>
|
||||||
|
<p>好了,概念已经清楚,不用着急背公式, 接着往下看</p>
|
||||||
|
<p> </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<=根节点子节点个数<=5</li>
|
||||||
|
<li>3<=内节点子节点个数<=5</li>
|
||||||
|
<li>1<=根节点元素个数<=4</li>
|
||||||
|
<li>2<=非根节点元素个数<=4</li>
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
<p><img src="https://img2018.cnblogs.com/blog/997909/201907/997909-20190727135621105-1688663582.png" alt=""> 插入8 <img src="https://img2018.cnblogs.com/blog/997909/201907/997909-20190727150240628-562673561.png" alt="" class="medium-zoom-image"></p>
|
||||||
|
<p>图(1)插入元素【8】后变为图(2),此时根节点元素个数为5,不符合 1<=根节点元素个数<=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> <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> <img src="https://img2018.cnblogs.com/blog/997909/201907/997909-20190727150403812-1196185160.png" alt="" width="429" height="217" class="medium-zoom-image"></p>
|
||||||
|
<p> </p>
|
||||||
|
<p> 接着插入元素【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> </p>
|
||||||
|
<p>插入【4】时,导致最左边的叶子结点被分裂,【4】恰好也是中间元素,上移到父节点中,然后元素【16】,【18】,【24】,【25】陆续插入不需要任何分裂操作</p>
|
||||||
|
<p> <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> </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>关键要领,元素个数小于 2(m/2 -1)就合并,大于4(m-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> </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> 下一步,删除【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】, 删除后会导致很多问题,因为【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> </p>
|
||||||
|
<p>也许你认为这样删除操作已经结束了,其实不然,在看看上图,对于这种特殊情况,你立即会发现父节点只包含一个元素【7】,没达标(因为非根节点包括叶子结点的元素K必须满足于2=<K<=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> </p>
|
||||||
|
<p> </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)。 </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> 考虑到磁盘IO是非常高昂的操作,计算机操作系统做了一些优化,当一次IO时,不光把当前磁盘地址的数据,而是把相邻的数据也都读取到内存缓冲区内,因为局部预读性原理告诉我们,当计算机访问一个地址的数据的时候,与其相邻的数据也会很快被访问到。每一次IO读取的数据我们称之为一页(page)。具体一页有多大数据跟操作系统有关,一般为4k或8k,也就是我们读取一页内的数据时候,实际上才发生了一次IO,这个理论对于索引的数据结构设计非常有帮助。</p>
|
||||||
|
<p>事实1 : 不同容量的存储器,访问速度差异悬殊。</p>
|
||||||
|
<ul>
|
||||||
|
<li>磁盘(ms级别) << 内存(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=8000000000,80亿条的数据只需3次I/O(理论值),可想而知,B树做为索引的查询效率有多高;</p>
|
||||||
|
<p>另外也可以看出同样的总元素个数,查询效率和树的高度密切相关</p>
|
||||||
|
<p> </p>
|
||||||
|
<p><strong>B树的高度</strong></p>
|
||||||
|
<p>一棵含有N个总关键字数的m阶的B树的最大高度是多少?</p>
|
||||||
|
<p> log(m/2)(N+1)/2 + 1 ,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> </p>
|
||||||
|
<p> </p>
|
||||||
|
<p> </p>
|
||||||
|
<h3>B+树<button class="cnblogs-toc-button" title="显示目录导航" aria-expanded="false"></button></h3>
|
||||||
|
<p> B+树是应文件系统所需而产生的B树的变形树,那么可能一定会想到,既然有了B树,又出一个B+树,那B+树必然是有很多优点的</p>
|
||||||
|
<p> </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)">所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大的顺序链接。 (而B 树的叶子节点并没有包括全部需要查找的信息);</span></li>
|
||||||
|
<li><span style="color: rgba(255, 0, 0, 1)">所有的非终端结点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键字。 (而B 树的非终节点也包含需要查找的有效信息);</span></li>
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
<p> </p>
|
||||||
|
<p><strong>为什么说B+树比B树更适合数据库索引?</strong></p>
|
||||||
|
<p><span style="color: rgba(255, 0, 0, 1)">1)B+树的磁盘读写代价更低</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)">2)B+树查询效率更加稳定</span></p>
|
||||||
|
<p><span style="color: rgba(255, 0, 0, 1)"> 由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当;</span></p>
|
||||||
|
<p><span style="color: rgba(255, 0, 0, 1)">3)B+树便于范围查询(最重要的原因,范围查找是数据库的常态)</span></p>
|
||||||
|
<p> B树在提高了IO性能的同时并没有解决元素遍历的我效率低下的问题,正是为了解决这个问题,B+树应用而生。B+树只需要去遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作或者说效率太低;不懂可以看看这篇解读-》<a href="https://zhuanlan.zhihu.com/p/54102723" target="_blank" rel="noopener nofollow">范围查找</a></p>
|
||||||
|
<p> </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> <a href="https://www.cnblogs.com/wade-luffy/p/6292784.html">B+树介绍 - wade&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所示。 </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> </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+树并不会急于去拆分叶节点,而是做旋转,50,55,55旋转。</p>
|
||||||
|
<p> <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所示的三种情况,与插入不同的是,删除根据填充因子的变化来衡量。 </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>  <span style="color: rgba(255, 0, 0, 1)">怎么样的路径算最短路径?</span></strong></p>
|
||||||
|
<p><span style="color: rgba(255, 0, 0, 1)">  从规则5中,我们知道从根节点到每个叶子节点的黑色节点数量是一样的,那么纯由黑色节点组成的路径就是最短路径;</span></p>
|
||||||
|
<p><span style="color: rgba(255, 0, 0, 1)"><strong>  什么样的路径算是最长路径?</strong></span></p>
|
||||||
|
<p><span style="color: rgba(255, 0, 0, 1)">  根据规则4和规则3,若有红色节点,则必然有一个连接的黑色节点,当红色节点和黑色节点数量相同时,就是最长路径,也就是黑色节点(或红色节点)* 2</span></p>
|
||||||
|
<p><strong>第二. 为什么说新加入到红黑树中的节点为红色节点</strong></p>
|
||||||
|
<p> <span style="color: rgba(255, 0, 0, 1)"> 从规则4中知道,当前红黑树中从根节点到每个叶子节点的黑色节点数量是一样的,此时假如新的黑色节点的话,必然破坏规则,但加入红色节点却不一定,除非其父节点就是红色节点,因此加入红色节点,破坏规则的可能性小一些,</span>下面我们也会举例来说明。</p>
|
||||||
|
<p>什么情况下,红黑树的结构会被破坏呢?破坏后又怎么维持平衡,维持平衡主要通过两种方式【<strong>变色</strong>】和【<strong>旋转</strong>】,【<strong>旋转</strong>】又分【<strong>左旋</strong>】和【<strong>右旋</strong>】,两种方式可相互结合。</p>
|
||||||
|
<p> </p>
|
||||||
|
<p>我们会先尝试 recolor,如果 recolor 不能达到红黑树的 4 点要求,然后我们尝试 rotation,其实红黑树的关键玩法就是弄清楚 recolor 和 rotation 的规则,接下来看看详细的算法公式吧 千万别着急记忆公式,有图示会逐步说明,就像魔方一样,多玩几次就懂了:<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 是黑色的时候我们第一步要考虑的是 旋转 ,这里先请小伙伴不要关注红黑树的第 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 的左孩子,然后再应用 左左情况</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 的右孩子,然后再应用 右右情况</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>插入 10,20,30,15 到一个空树中</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 > 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 > 10,查找 10 的右子树,找到 20</li>
|
||||||
|
<li>30 > 20,继续查找 20 的右子树,发现 20 没有叶子节点,将值插在此处</li>
|
||||||
|
<li>30 和 20 节点都为红色,30 为右孩子,20 也为右孩子,触发了 右右情况</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> 是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> </p>
|
||||||
|
<p> </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> B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3</p>
|
||||||
|
<p>(代替B+树的1/2);</p>
|
||||||
|
<p> B+树的分裂:当一个结点满时,分配一个新的结点,并将原结点中1/2的数据</p>
|
||||||
|
<p>复制到新结点,最后在父结点中增加新结点的指针;B+树的分裂只影响原结点和父</p>
|
||||||
|
<p>结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针;</p>
|
||||||
|
<p> B*树的分裂:当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分</p>
|
||||||
|
<p>数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字</p>
|
||||||
|
<p>(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之</p>
|
||||||
|
<p>间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针;</p>
|
||||||
|
<p> 所以,B*树分配新结点的概率比B+树要低,空间使用率更高;</p>
|
||||||
|
<p><strong><span style="font-size: 14pt; color: rgba(255, 0, 0, 1)">面试题:红黑树相比于BST和AVL树有什么优点</span></strong><br> 红黑树是牺牲了严格的高度平衡的优越条件为代价,它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。<br>红黑树能够以O(log n)的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。<br>当然,还有一些更好的,但实现起来更复杂的数据结构能够做到一步旋转之内达到平衡,但红黑树能够给我们一个比较“便宜”的解决方案。<br><br> 相比于BST,因为红黑树可以能确保树的最长路径不大于两倍的最短路径的长度,所以可以看出它的查找效果是有最低保证的。在最坏的情况下也可以保证O(logN)的,这是要好于二叉查找树的。因为二叉查找树最坏情况可以让查找达到O(N)。红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高,所以在插入和删除中所做的后期维护操作肯定会比红黑树要耗时好多,但是他们的查找效率都是O(logN),所以红黑树应用还是高于AVL树的. 实际上插入 AVL 树和红黑树的速度取决于你所插入的数据。如果你的数据分布较好,则比较宜于采用 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 (n)) 的时间复杂度进行搜索、插入、删除操作<br><br>3、简单来说红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。<br><br>红黑树和平衡树的对比和选择<br><br>1、平衡树结构更加直观,读取性能比红黑树要高;增加和删除节点 恢复平衡的性能不如红黑树<br><br>2、红黑树,读取性能不如平衡树;增加和删除节点 恢复平衡性能比平衡树好<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> </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>
|
||||||
|
<a href="https://www.cnblogs.com/henuliulei">你的雷哥</a>
|
||||||
|
阅读(<span id="post_view_count">13316</span>)
|
||||||
|
评论(<span id="post_comment_count">3</span>)
|
||||||
|
|
||||||
|
<a href="javascript:void(0)" onclick="AddToWz(15114440);return false;">收藏</a>
|
||||||
|
<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`:
|
||||||
|
|
||||||
|
### **情况 1:key 在叶子节点上**
|
||||||
|
|
||||||
|
* 直接删除 key 即可。
|
||||||
|
* 删除后如果叶子节点仍然有 ≥ t-1 个 key,则无需进一步处理。
|
||||||
|
* 如果 key 数 < t-1,则需要从兄弟节点借 key 或合并节点(下文详述)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **情况 2:key 在内部节点上**
|
||||||
|
|
||||||
|
假设 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。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **情况 3:key 不在内部节点上**
|
||||||
|
|
||||||
|
* 找到 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 数减 1,child 数减 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. **递归**是实现删除的核心思路,从上到下保证每个节点在递归时合法。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
如果你需要,我可以帮你 **写一个完整的 C++ B 树删除函数实现**,直接和你已有的 `BTree` 类整合,并附详细注释。
|
||||||
|
|
||||||
|
你希望我帮你写吗?
|
73
BinaryTree/BalanceTree/B-Tree/main.cpp
Normal file
73
BinaryTree/BalanceTree/B-Tree/main.cpp
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
#include <iostream>
|
||||||
|
#include "BTree.h"
|
||||||
|
using namespace std;
|
||||||
|
int main() {
|
||||||
|
BTree<int> a(2);
|
||||||
|
a.insert(10);a.display();
|
||||||
|
a.insert(3);a.display();
|
||||||
|
a.insert(6);a.display();
|
||||||
|
a.insert(9);a.display();
|
||||||
|
a.insert(2);a.display();
|
||||||
|
a.insert(5);a.display();
|
||||||
|
a.insert(8);a.display();
|
||||||
|
a.insert(1);a.display();
|
||||||
|
a.insert(4);a.display();
|
||||||
|
a.insert(7);a.display();
|
||||||
|
a.insert(11);a.display();
|
||||||
|
a.insert(15);a.display();
|
||||||
|
a.insert(17);a.display();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
10
|
||||||
|
|
||||||
|
3 10
|
||||||
|
|
||||||
|
3 6 10
|
||||||
|
|
||||||
|
3
|
||||||
|
6 9 10
|
||||||
|
|
||||||
|
2 3
|
||||||
|
6 9 10
|
||||||
|
|
||||||
|
2 3 5
|
||||||
|
6 9 10
|
||||||
|
|
||||||
|
2 3 5
|
||||||
|
6 8 9 10
|
||||||
|
|
||||||
|
1 2
|
||||||
|
3 5
|
||||||
|
6 8 9 10
|
||||||
|
|
||||||
|
1 2
|
||||||
|
3 4 5
|
||||||
|
6 8 9 10
|
||||||
|
|
||||||
|
1 2
|
||||||
|
3 4 5
|
||||||
|
6 7 8
|
||||||
|
9 10
|
||||||
|
|
||||||
|
1 2
|
||||||
|
3 4 5
|
||||||
|
|
||||||
|
6 7 8
|
||||||
|
9 10 11
|
||||||
|
|
||||||
|
|
||||||
|
1 2
|
||||||
|
3 4 5
|
||||||
|
|
||||||
|
6 7 8
|
||||||
|
9 10 11 15
|
||||||
|
|
||||||
|
|
||||||
|
1 2
|
||||||
|
3 4 5
|
||||||
|
|
||||||
|
6 7 8
|
||||||
|
9 10
|
||||||
|
11 15 17
|
||||||
|
*/
|
Reference in New Issue
Block a user