2-3-4树
--简介
红黑树由2-3-4树转换而成.
2-3-4树是一种多路查找树,一个结点的孩子数目可以超过2个,
同时能像二叉搜索树一样用于搜索.
-- 2-3-4树结点的分类
2结点 ------ 包含1个元素,有两个孩子结点
左子树所有结点的键值小于 该结点的键值;右子树所有结点的键值大于 该结点的键值.
2结点要么有2个孩子,要么没有孩子
【除了最后一个约束,其它和二叉搜索树一样】
3结点 ------ 包含2个元素,有三个孩子结点(这2个元素已经排序)
左子树所有结点的键值小于 该结点较小的元素键值;
中间子树所有结点的键值介于 这两个元素键值大小之间;
右子树所有所有结点的键值大于 该结点最大的元素键值.
3结点要么有3个孩子,要么没有孩子
4结点 ------ 包含3个元素,有四个孩子结点(这3个元素已经排序)
从左到右暂时称为 A、B、C元素【A < B < C】
第一棵子树所有结点的键值小于 A键值
第二棵子树所有结点的键值介于 AB键值之间
第三棵子树所有结点的键值介于 BC键值之间
第四棵子树所有结点的键值大于 C键值.
4结点要么有4个孩子,要么没有孩子
-- 2-3-4树的性质
( 1 ) 2-3-4树的结点只有2结点/3结点/4结点.
( 2 ) 2-3-4树的叶子结点,一定都在同一层次中.
简单举例:
2-3-4树的实现
--成员属性
( 1 ) 一个结点可能存储多个元素,可以用数组来依次存储这些元素.【注意要升序】
用_num标识该结点当前存储的元素个数.
( 2 ) 孩子结点可能有多个,用数组来依次存储这些孩子结点的地址.
用_childNum标识孩子结点的个数.
下面是结点的定义代码,裂变在插入时会讲,数组均多开一个空间是为了方便实现.
ini
//2-3-4树的结点
template<class K, class V>
struct BalanceTreeNode
{
BalanceTreeNode(const pair<K, V>& kv)
{
_kv[0] = kv;
for (int i = 0; i < 5; ++i)
_child[i] = nullptr;
}
//包含的元素(最多只能包含3个元素,开4个空间方便裂变)
pair<K, V> _kv[4];
//包含的元素个数
int _num = 1;
//父亲结点
BalanceTreeNode* _parent = nullptr;
//最多可以有4个孩子结点(开多1个空间用于裂变)
BalanceTreeNode* _child[5];
//孩子结点数目
int _childNum = 0;
};
arduino
//2-3-4树
template<class K, class V>
class BalanceTree
{
typedef BalanceTreeNode<K, V> Node;
private:
Node* _root = nullptr;
};
--查找
upload.wikimedia.org/wikipedia/c...
下面是维基百科上的2-3-4树图片:
例:在上图的2-3-4树里,找到目标元素8.
cur = _root
从根结点开始找,cur目前只有一个元素,8 > 5,在cur结点继续向后找8
但此时cur后面已经没有元素,所以 目标元素8 比 cur所有元素 都大,
去 cur的最后一个孩子结点 找 目标元素
cur = cur->_child[cur->_childNum - 1 ]
cur = (7 9)这个结点指针
从cur的第一个元素7开始遍历,8 > 7,在cur结点继续向后找8.
cur的第二个元素9,8 < 9,去cur结点的第二个孩子结点找8.
cur = (8)这个结点指针
从头开始遍历,第一个元素就是8.
规律总结
【这里的孩子结点其实也能理解为子树】
( 1 )若 目标元素 比 当前结点第pos个元素 小,就去 当前结点第pos个孩子结点 找目标元素
( 2 )若 目标元素 比 当前结点第pos个元素 大,继续 在当前结点 向后找目标元素
( 3 )若 目标元素 比 当前结点的所有元素 大,就去 当前结点的最后一个孩子结点 找目标元素
2-3-4树的实现里,虽然结点存储的是pair<K,V>类型的元素,但都是用 Key 进行比较.
rust
bool find(const K& key)
{
//从根开始找
Node* cur = _root;
//直到cur为空结点
while (cur != nullptr)
{
int i = 0;
//依次与cur的所有元素比较
for (i = 0; i < cur->_num; ++i)
{
//这里是用key比较
//当 目标元素 比 cur里遍历到的元素 大,继续向后遍历cur里的元素
//当 目标元素 比 cur里遍历到的元素 小,去cur的第i个孩子结点/子树找【更新cur】
if (key > cur->_kv[i].first)
continue;
if (key < cur->_kv[i].first)
break;
if (key == cur->_kv[i].first)
return true;
}
//没有找到都会更新cur
//1 若目标元素 比 cur里面第i个元素 小
//2 若目标元素 比 cur里面所有元素 大
if (i < cur->_num)
cur = cur->_child[i];
else
{
if (cur->_childNum == 0)//cur没有孩子
return false;
else
cur = cur->_child[cur->_childNum - 1];
}
}
return false;
}
--插入
( 1 ) 空树,直接插入
ini
//空树
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
( 2 ) 先找到插入位置,准备插入在叶子结点
rust
//从根开始,找到可以插入的叶子结点
Node* cur = _root;
//2-3-4树的每个结点,要么没有孩子,要么孩子是满的
while (cur->_childNum != 0)
{
//kv 小于 当前结点的第一个元素,就去 当前结点的第一棵子树 找
//若比当前结点的所有元素要大,去最右子树找插入结点
int i = 0;
for (i = 0; i < cur->_num; ++i)
{
if (kv.first < cur->_kv[i].first)
break;
else if (kv.first == cur->_kv[i].first)//插入失败,已经有相同的key
return false;
else
continue;
}
//kv小于cur的第i个元素,去第i个子树找
//若kv大于其所有元素,去最后一棵子树找,当前i也会是最后一棵子树的位置
cur = cur->_child[i];
}
//此时cur是叶子结点,可以插入
插入元素,优先合并.
例:往空树依次插入1、2、3
(1) -> (1,2)
cur = _root,cur是叶子结点,所以2直接插入cur里【注意排序 + 更新cur的元素数目】
(1,2) -> (1,2,3)
cur = _root,cur是叶子结点,3也直接插入cur里
若cur的元素数目 = 4,发生裂变.
裂变完整的表达比较复杂,所以列出 简单到复杂的裂变情况 来解释.
简单裂变
例:根结点是(1,2,3),往(1, 2, 3)插入1.5
由于结点里的元素数组多开了一个空间,所以可以先正常合并
插入后,判断cur的元素数目,发生异常,裂变处理
将 cur的中间元素1.5或2 提取出来,跟cur的父亲结点进行合并;【这里以1.5为例】
因为这里cur没有父亲结点,就用该中间元素1.5新建一个结点,作为新的父亲结点.
用 cur的左边元素( 1 ) 单独新建一个结点,作为父亲结点第一个孩子结点.
用 cur的右边元素( 2 3 ) 单独新建一个结点. 作为父亲结点第二个孩子结点.
【记得要delete cur】
普通裂变
以维基百科的图为例:插入13.
( 1 ) 找到插入位置(一定是叶子结点,除非空树),优先合并
( 2 ) cur = (10 11 12 13),cur->_num == 4,需要裂变处理.
cur的父亲结点(7 9)不为空,因此中间元素11要和(7 9)合并
用 左边元素(10) 新建一个结点;用 右边元素(12 13) 新建一个结点,
这两个结点代替原来(10 11 12 13)的位置.
【记得delete cur】
进阶裂变
不糟蹋那张图了,往下图插入16
( 1 ) 找到插入位置,要插入到(8 10 15)这个4结点中,优先合并
( 2 ) cur->_num = 4,发生裂变.
cur有父亲结点,这次先求出cur是parent的第几个孩子.(其实上一个裂变也需要)
cur的中间元素和父亲结点合并,用 左边元素(8) 和 右边元素(15 16) 各自新建一个结点
之前的cur是parent的第 i 个孩子,那么这两个新结点就分别是parent的 第i个 和 第i+1 个孩子
父亲结点里有记录 孩子结点指针 的数组,需要把 第i+1个数组元素及后面的数组元素 往后挪动.
复杂裂变
在叶子结点发生裂变后,它的中间元素会和父亲结点进行合并.
如果中间元素和其父亲结点合并以后,父亲结点的元素 = 4,就要继续裂变处理.
但此时需要裂变的结点,不再是叶子结点.
例:向下图插入元素 49
( 1 ) cur = 叶子结点(35 39 41 49) 正常进行裂变
因为定义结点时,结点里的存储孩子结点指针的数组,多开了一个空间,方便实现.
cur裂变完成后,下图所示:
( 2 ) cur = 非叶子结点(5 19 33 39),该结点元素数目异常,需要裂变
A 记录cur是父亲结点的第几个孩子(如果父亲结点不为空)
B cur的中间元素19,和父亲结点合并(没有父亲结点就用19新建一个结点作为新的父亲结点)
C 用cur的左边元素(5),新建一个结点,
同时该新结点的孩子结点,是cur->_child[0]和cur->_child[1]
D 用cur的右边元素(33 39),新建一个结点,
同时该新结点的孩子结点,是cur->_child[2]、cur->_child[3]、cur->_child[4].
代码实现
ini
//插入
bool insert(const pair<K, V>& kv)
{
//空树
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
//从根开始,找到可以插入的叶子结点
Node* cur = _root;
//2-3-4树的每个结点,要么没有孩子,要么孩子是满的
while (cur->_childNum != 0)
{
//kv 小于 当前结点的第一个元素,就去 当前结点的第一棵子树 找
//若比当前结点的所有元素要大,去最右子树找插入结点
int i = 0;
for (i = 0; i < cur->_num; ++i)
{
if (kv.first < cur->_kv[i].first)
break;
else if (kv.first == cur->_kv[i].first)//插入失败,已经有相同的key
return false;
else
continue;
}
//kv小于cur的第i个元素,去第i个子树找
//若kv大于其所有元素,去最后一棵子树找,当前i也会是最后一棵子树的位置
cur = cur->_child[i];
}
//此时cur是叶子结点,可以插入
//往结点里插入元素
_mergeElement(cur, kv);
//若当前结点元素为4,需要裂变
while (cur->_num == 4)
{
Node* parent = _fission(cur);
cur = parent;
}
return true;
}
往结点里的元素数组里插入一个值,和顺序表的插入一样.
ini
//让一个结点node,新增一个元素
//返回合并完后,元素插入的下标位置
size_t _mergeElement(Node* node, const pair<K, V>& p)
{
assert(node);
int pos = 0;
//找到插入位置
while (pos < node->_num && p.first > node->_kv[pos].first)
++pos;
if (node->_kv[pos].first == p.first)
return -1;
//往后挪动元素
for (int begin = node->_num - 1; begin >= pos; --begin)
node->_kv[begin + 1] = node->_kv[begin];
//正式插入,同时增加node->_num
node->_kv[pos] = p;
++node->_num;
return pos;
}
裂变实现,易错点:父子关系的调整.
例:把newLeft的一个孩子设置为node->_child[0],
node->_child[0]的父亲结点也要设置为newLeft.
ini
//裂变(传的结点,元素数目必须为4),返回父亲结点
Node* _fission(Node* node)
{
//用node的左边元素 新建一个结点
Node* newLeft = new Node(node->_kv[0]);
if (node->_childNum == 0)//叶子结点发生的裂变
{
newLeft->_num = 1;
newLeft->_childNum = 0;
}
else if (node->_childNum != 0)//非叶子结点发生的裂变
{
newLeft->_child[0] = node->_child[0];
newLeft->_child[1] = node->_child[1];
newLeft->_childNum = 2;
//注意父子关系的调整
newLeft->_child[0]->_parent = newLeft;
newLeft->_child[1]->_parent = newLeft;
}
//用node的右边元素 新建一个结点
Node* newRight = new Node(node->_kv[2]);
_mergeElement(newRight,node->_kv[3]);
if (node->_childNum == 0)//叶子结点发生的裂变
{
newRight->_num = 2;
newRight->_childNum = 0;
}
else if (node->_childNum != 0)
{
newRight->_child[0] = node->_child[2];
newRight->_child[1] = node->_child[3];
newRight->_child[2] = node->_child[4];
newRight->_childNum = 3;
newRight->_child[0]->_parent = newRight;
newRight->_child[1]->_parent = newRight;
newRight->_child[2]->_parent = newRight;
}
//node的中间元素与父亲结点合并
Node* parent = node->_parent;
//父亲结点为空,新的根诞生
if ( parent == nullptr )
{
_root = new Node(node->_kv[1]);
_root->_child[0] = newLeft;
_root->_child[1] = newRight;
newLeft->_parent = _root;
newRight->_parent = _root;
_root->_childNum = 2;
delete node;
return _root;
}
//父亲结点不为空
//裂变结点的第二个元素 和 parent 合并
size_t pos = _mergeElement(parent, node->_kv[1]);
//newLeft和newRight插入在pos和pos+1的位置,pos+1及后面的孩子向后挪一格
for (int end = parent->_childNum - 1; end >= pos+1; --end)
parent->_child[end + 1] = parent->_child[end];
++parent->_childNum;
//链接parent 和 newLeft/newRight
parent->_child[pos] = newLeft;
parent->_child[pos+1] = newRight;
newLeft->_parent = parent;
newRight->_parent = parent;
return parent;
}
2-3-4树的删除
2-3-4树的删除十分复杂,所以作为一个大标题更合适
--找到要删除的元素
ini
if (_root == nullptr)
return false;
//从根开始向下找删除元素
Node* cur = _root;
while (cur != nullptr)
{
//标识是否找到元素
bool flag = false;
//对每个结点扫描其所有元素
//若 key 小于 当前结点的第pos个元素,去第pos个孩子结点找
//若 key 大于 当前结点的第pos个元素,继续在当前结点向后找
int pos = 0;
for (pos = 0; pos < cur->_num; ++pos)
{
if (key > cur->_kv[pos].first)
continue;
else if (key == cur->_kv[pos].first)//找到要删除的元素
{
flag = true;
break;
}
else if (key < cur->_kv[pos].first)//去cur->_child[pos]中找
break;
}
//找到删除元素,此时删除元素位于cur里
if (flag == true)
break;
if (cur->_childNum != 0)
{
//否则去第pos个孩子结点找
cur = cur->_child[pos];
}
else//cur是叶子结点,同时没有在cur找到要删除的元素
cur = nullptr;
}
//没找到删除元素
if (cur == nullptr) return false;
//cur就是要删除元素位于的结点
--转换成删除叶子结点的元素
如果cur是非叶子结点,不能直接删除cur里的元素
否则 cur的元素数目 和 cur的孩子结点数目 关系会被打乱.
因此要用类似于搜索二叉树的替换法删除.
以下图为例:(很糟糕的图)
删除元素9,元素所在的结点不是叶子结点
( 1 ) 元素9位于cur->_kv[0].
去cur->_child[0]子树【每个结点也能看作每棵子树的根】,
找子树的最大元素赋值给删除元素的位置
( 2 ) 此时转换成删除叶子结点里的元素8
规律总结
要删除的元素是cur->_kv[i],且cur是非叶子结点
去cur->_child[i]子树里找最大元素,赋值给cur->_kv[i],转换成删除该最大元素.
【或者去cur->_child[i+1]子树里找最小元素】
类似于搜索二叉树里找 左子树的最大值 或 右子树的最小值
rust
//如果cur不是叶子结点,用替换法转换成删除叶子结点的元素,
//把本该删除的元素用 cur->_child[pos]子树的最大元素 替换,转换成删除该最大元素
if (cur->_childNum != 0)
{
//非叶子结点,替换法转换 要删除的元素
//用cur->_child[pos]子树的最大元素替换
Node* max = cur->_child[pos];//最大元素所位于的结点用max记录
//不断去max的最右子树找
while (max->_child[max->_childNum - 1] != nullptr)
max = max->_child[max->_childNum - 1];
//此时用max最大元素 赋值给 本该删除的元素
cur->_kv[pos] = max->_kv[max->_num - 1];
//要删除的元素位于的结点更新,具体位置也更新
cur = max;
pos = max->_num - 1;
}
//此时cur是删除元素位于的结点,pos是删除元素在结点的位置
//且cur一定是叶子结点
--正式删除的情况分析
cur结点有多于1个元素
直接删除目标元素,由于cur是叶子结点,不会违反 2结点/3结点/4结点 的特征.
例:
rust
// 若cur结点有多于一个元素,可以直接删除,不需要多余处理
if (cur->_num > 1)
{
_deleteElement(cur, cur->_kv[pos]);
return true;
}
ini
//结点里删除一个元素
void _deleteElement(Node* node, const pair<K, V>& kv)
{
int search = 0;
for (search = 0; search < node->_num; ++search)
{
if (kv.first == node->_kv[search].first)
break;
}
for (int begin = search + 1; begin <= node->_num - 1; ++begin)
node->_kv[begin - 1] = node->_kv[begin];
--node->_num;
}
cur结点只有1个元素
cur是 2结点,不能直接删除该元素.
向父亲结点parent要一个元素,覆盖删除cur里的元素
此时parent对应元素位置缺失元素,向cur的一个相邻兄弟结点【任意选定一个】要元素
若该兄弟结点元素数目 > 1
删除cur兄弟结点里,被父亲结点拿走的元素,调整完成
cur的相邻兄弟结点也是叶子结点,所以它只要不是2结点,就能直接删除元素.
若该兄弟结点元素数目 = 1
在网上找的画图软件
未命名绘图 - draw.io (diagrams.net)
( 1 )删除元素1,元素1所在的叶子结点cur只有一个元素,所以向父亲结点要元素.
( 2 )父亲结点想向cur的相邻兄弟结点要,但curBother也只有一个元素,
因此父亲结点只能删除被拿走的元素,同时把cur和curBother合并(即newNode).
( 3 )但是此时parent结点元素被删空了,此时继续向GrandParent借元素,
然后和parent的兄弟结点合并【不会再出现 父亲结点 向 孩子的兄弟结点 要元素的情况】
规律总结
前提:删除元素位于叶子结点,否则用替换法转换成删除叶子结点的元素.
1 若删除元素位于的结点,元素数目 > 1,直接删除.
rust
//此时cur是删除元素位于的结点,pos是删除元素在结点的位置
//且cur一定是叶子结点
// 若cur结点有多于一个元素,可以直接删除,不需要多余处理
if (cur->_num > 1)
{
_deleteElement(cur, cur->_kv[pos]);
return true;
}
2 若删除元素位于的结点,元素数目 = 1,向父亲结点要元素
若该结点是父亲结点的第i个孩子,那就拿父亲结点的第i个元素.
若该结点是父亲结点的最后一个孩子,特殊处理,拿父亲结点的最后一个元素
【因为非叶子结点的孩子结点数目 = 元素数目 + 1】
ini
//要先知道cur是父亲结点的第几个孩子(cur向父亲结点借一个元素 ------ 借哪个元素取决于cur是第几个孩子)
Node* parent = cur->_parent;
int pos = 0;
for (pos = 0; pos < parent->_childNum; ++pos)
if (parent->_child[pos] == cur)break;
//parent是2-3-4树的非叶子结点,所以它的孩子结点数目 = 元素数目+1
//我这里借第pos个元素,如果pos正好是最后一个孩子结点位置的话,特殊处理
int lent = pos;
if (pos == parent->_childNum - 1)
--lent;
//先借父亲结点再说
//向父亲结点借的元素是parent->_kv[lent]
cur->_kv[0] = parent->_kv[lent];
( 1 ) 父亲结点第i个元素位置缺失了元素,
向cur的相邻兄弟结点拿一个元素,拿cur相邻兄弟结点的第一个元素,
交给父亲结点缺失元素的位置.
ini
//先看cur相邻兄弟结点是否有多余的元素
Node* curBother = _neighboringBother(cur);
//cur相邻兄弟结点有多余的元素,父亲结点直接借
if (curBother->_num > 1)
{
parent->_kv[lent] = curBother->_kv[0];
_deleteElement(curBother, curBother->_kv[0]);
return true;
}
ini
//求相邻的兄弟结点
Node* _neighboringBother(Node* cur)
{
Node* parent = cur->_parent;
//没有兄弟结点,cur就是根
if (parent == nullptr) return nullptr;
//cur是第pos个孩子结点
int pos = 0;
for (pos = 0; pos < parent->_childNum; ++pos)
{
if (parent->_child[pos] == cur)
break;
}
if (pos == parent->_childNum - 1)
return parent->_child[pos - 1];
else
return parent->_child[pos + 1];
}
( 2 ) 如果cur的相邻兄弟结点只有一个元素,不能给父亲结点元素.
那父亲结点只好彻底删除空掉的元素,合并cur和cur的相邻兄弟结点.
A 此时父亲结点若元素数目 > 0,调整完成.
scss
//cur相邻的兄弟结点没有多余的元素可以给父亲结点
//此时父亲结点被拿走的元素补不回来了
_deleteElement(parent, parent->_kv[lent]);
Node* newNode = _mergeNode(cur, curBother);
if (parent->_num > 0)
return true;
ini
//前提:node1和node2是兄弟结点,且node1->_kv[0] < node2->_kv[0]
//合并node1和node2,同时释放掉node2的结点空间,node1是合并的结点
//返回合并完成的结点
Node* _mergeNode(Node* node1, Node* node2)
{
Node* min = node1;
Node* max = node2;
if (min->_kv[0].first > max->_kv[0].first)
swap(min, max);
//将max的元素全部尾插到min元素后面,每次尾插都要 ++min->_num
for (int i = 0; i < max->_num; ++i)
min->_kv[min->_num++] = max->_kv[i];
//将max的孩子结点依次交给min,每次都要增加min的孩子结点数目
for (int i = 0; i < max->_childNum; ++i)
{
min->_child[min->_childNum++] = max->_child[i];
max->_child[i]->_parent = min;
}
//max和min的父亲结点,孩子需要更新
Node* parent = max->_parent;
//min和max是兄弟结点,但它们居然没有父亲结点,不可能
if (parent == nullptr) assert(false);
//先判断min是parent的第几个孩子
int pos1 = -1;
for (int i = 0; i < parent->_childNum; ++i)
{
if (parent->_child[i] == min)
{
pos1 = i;
break;
}
}
//覆盖删除pos1位置的孩子结点
for (int begin = pos1 + 1; begin <= parent->_childNum - 1; ++begin)
parent->_child[begin - 1] = parent->_child[begin];
--parent->_childNum;
//该位置的孩子结点改为合并完成的结点node1
parent->_child[pos1] = min;
delete max;
return min;
}
B 若父亲结点元素数目 = 0,更新cur和parent,cur = parent; parent = cur->_parent
接下来cur继续向父亲结点拿元素,但是父亲结点不会再向curBother拿元素,
而是合并cur和curBother【合并以后发生裂变注意处理】.
总之,重复cur里元素为空,向父亲结点拿元素,合并cur和curBother
更新cur和parent,直到cur里的元素不为空/cur == _root调整完成.
ini
//父亲结点被借空了
//cur作为被借空的结点,进行处理
cur = parent;
while (cur != _root && cur->_num == 0)//根结点被借空/cur没有被借空 退出
{
//向父亲结点借元素
parent = cur->_parent;
_lent(cur, parent);
//cur与cur相邻兄弟结点合并
curBother = _neighboringBother(cur);
Node* newNode = _mergeNode(cur, curBother);
//1 合并以后,可能会发生裂变情况,裂变完成后全部调整完成
if (newNode->_num == 4)
{
_fission(newNode);
return true;
}
//2 合并以后,没有裂变,继续看父亲结点的元素数目是否 > 0.
cur = newNode->_parent;
}
if (cur->_num > 0)
return true;
else//说明根结点的元素被借空了,需要更换根了
{
//此时cur == _root
//_root应该被修改
_root = newNode;
_root->_parent = nullptr;
delete cur;
return true;
}
ini
//cur元素为空,向parent借元素
//--parent->_num
void _lent(Node* cur, Node* parent)
{
//cur是parent的第几个孩子
int i = 0;
for (i = 0; i < parent->_childNum; ++i)
{
if (parent->_child[i] == cur)
break;
}
if (i == parent->_childNum - 1)
--i;
cur->_kv[0] = parent->_kv[i];
++cur->_num;
_deleteElement(parent, parent->_kv[i]);
}
--完整代码
写代码时cur只有一个元素,删除cur里的元素,注释用的是"借",
后面写文章觉得 cur和parent都是 借了不还的,于是文章里更多是"拿".
ini
//删除
bool erase(const K& key)
{
if (_root == nullptr)
return false;
//从根开始向下找删除元素
Node* cur = _root;
while (cur != nullptr)
{
//标识是否找到元素
bool flag = false;
//对每个结点扫描其所有元素
//若 key 小于 当前结点的第pos个元素,去第pos个孩子结点找
//若 key 大于 当前结点的第pos个元素,继续在当前结点向后找
int pos = 0;
for (pos = 0; pos < cur->_num; ++pos)
{
if (key > cur->_kv[pos].first)
continue;
else if (key == cur->_kv[pos].first)//找到要删除的元素
{
flag = true;
break;
}
else if (key < cur->_kv[pos].first)//去cur->_child[pos]中找
break;
}
//找到删除元素,此时删除元素位于cur里
if (flag == true)
break;
if (cur->_childNum != 0)
{
//否则去第pos个孩子结点找
cur = cur->_child[pos];
}
else//cur是叶子结点,同时没有在cur找到要删除的元素
cur = nullptr;
}
//没找到删除元素
if (cur == nullptr) return false;
//cur就是要删除元素位于的结点
int pos = 0;
for (pos = 0; pos < cur->_num; ++pos)
{
//找到删除元素的具体位置后停止
if (cur->_kv[pos].first == key)
break;
}
//此时删除元素的具体位置就是 cur->_kv[pos]
//如果cur不是叶子结点,用替换法转换成删除叶子结点的元素,
//把本该删除的元素用 cur->_child[pos]子树的最大元素 替换,转换成删除该最大元素
if (cur->_childNum != 0)
{
//非叶子结点,替换法转换 要删除的元素
//用cur->_child[pos]子树的最大元素替换
Node* max = cur->_child[pos];//最大元素所位于的结点用max记录
//不断去max的最右子树找
while (max->_child[max->_childNum - 1] != nullptr)
max = max->_child[max->_childNum - 1];
//此时用max最大元素 赋值给 本该删除的元素
cur->_kv[pos] = max->_kv[max->_num - 1];
//要删除的元素位于的结点更新,具体位置也更新
cur = max;
pos = max->_num - 1;
}
//此时cur是删除元素位于的结点,pos是删除元素在结点的位置
//且cur一定是叶子结点
// 若cur结点有多于一个元素,可以直接删除,不需要多余处理
if (cur->_num > 1)
{
_deleteElement(cur, cur->_kv[pos]);
return true;
}
else//若cur结点只有一个元素,不能直接删除
{
//大概思路(要画图):
//cur向父亲结点借一个元素
//1 此时父亲结点少了一个元素,可以向cur的相邻兄弟结点(一定也是叶子结点,所以被借走元素不会有影响)借一个元素
//2 如果cur的相邻兄弟结点也只有一个元素,
//父亲结点少了一个元素,意味着必须要少一个孩子,所以需要cur和cur相邻兄弟合并成一个新结点,作为父亲结点的孩子
//(1)若父亲结点此时元素个数 > 0,调整完成
//(2)若父亲结点元素个数 = 0,重复向父亲结点的父亲结点借元素,合并父亲结点和父亲结点的相邻兄弟结点(接下来不会再向兄弟结点借)
//终止条件:不会再存在某个结点数目为0
//特殊:cur是_root,且_root只有一个元素
if (cur == _root)
{
delete _root;
_root = nullptr;
return true;
}
//要先知道cur是父亲结点的第几个孩子(cur向父亲结点借一个元素 ------ 借哪个元素取决于cur是第几个孩子)
Node* parent = cur->_parent;
int pos = 0;
for (pos = 0; pos < parent->_childNum; ++pos)
if (parent->_child[pos] == cur)break;
//parent是2-3-4树的非叶子结点,所以它的孩子结点数目 = 元素数目+1
//我这里借第pos个元素,如果pos正好是最后一个孩子结点位置的话,特殊处理
int lent = pos;
if (pos == parent->_childNum - 1)
--lent;
//先借父亲结点再说
//向父亲结点借的元素是parent->_kv[lent]
cur->_kv[0] = parent->_kv[lent];
//先看cur相邻兄弟结点是否有多余的元素
Node* curBother = _neighboringBother(cur);
//cur相邻兄弟结点有多余的元素,父亲结点直接借
if (curBother->_num > 1)
{
parent->_kv[lent] = curBother->_kv[0];
_deleteElement(curBother, curBother->_kv[0]);
return true;
}
else//cur相邻的兄弟结点没有多余的元素可以给父亲结点
{
//此时父亲结点被拿走的元素补不回来了
_deleteElement(parent, parent->_kv[lent]);
Node* newNode = _mergeNode(cur, curBother);
if (parent->_num > 0)
return true;
else//父亲结点被借空了
{
//cur作为被借空的结点,进行处理
cur = parent;
while (cur != _root && cur->_num == 0)//根结点被借空/cur没有被借空 退出
{
//向父亲结点借元素
parent = cur->_parent;
_lent(cur, parent);
//cur与cur相邻兄弟结点合并
curBother = _neighboringBother(cur);
Node* newNode = _mergeNode(cur, curBother);
//1 合并以后,可能会发生裂变情况,裂变完成后全部调整完成
if (newNode->_num == 4)
{
_fission(newNode);
return true;
}
//2 合并以后,没有裂变,继续看父亲结点的元素数目是否 > 0.
cur = newNode->_parent;
}
if (cur->_num > 0)
return true;
else//说明根结点的元素被借空了,需要更换根了
{
//此时cur == _root
//_root应该被修改
_root = newNode;
_root->_parent = nullptr;
delete cur;
return true;
}
}
}
}
}
判断是否为2-3-4树
用栈深度优先遍历每个结点,如果每个结点都是合法的2/3/4结点,就是2-3-4树
ini
bool isBalanceTree()
{
if (_root == nullptr) return true;
//深度优先遍历
//每个结点的元素数目只能是1/2/3,非叶子结点对应的孩子数必须是2/3/4
stack<Node*> st;
st.push(_root);
while (!st.empty())
{
//处理当前结点
Node* top = st.top();
if (top->_num >= 4 || top->_num <= 0)
{
cout << "有结点的元素数目非法" << endl;
return false;
}
if (top->_child[0] == nullptr)//如果是叶子结点
assert(top->_childNum == 0);
else//不是叶子结点
assert(top->_childNum == top->_num + 1);
st.pop();
//从最后一个孩子开始依次入栈,保证后进先出
for (int i = top->_childNum - 1; i >= 0; --i)
st.push(top->_child[i]);
}
return true;
}
随机数测试插入删除
c
void testBalanceTree()
{
BalanceTree<int, int> bLTree;
int n = 3000;
for (int i = 0; i < n; ++i)
{
int x = rand() % n;
//cout << x << " ";
bLTree.insert(make_pair(x, i));
if (bLTree.isBalanceTree() == false)
{
cout << "插入时发生错误" << endl;
break;
}
}
cout << "元素全部插入成功"<<endl;
for (int i = 0; i < n; ++i)
{
if (bLTree.isBalanceTree() == false)
{
cout << "删除时发生错误" << endl;
break;
}
}
cout << "元素全部删除成功" << endl;
}