2-3-4树 C++实现

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;
}
相关推荐
T0uken2 分钟前
【QT Quick】C++交互:调用QML函数
c++·qt·交互
Two_brushes.23 分钟前
C++ list 容器类的模拟实现
开发语言·c++·list
小懒编程日记38 分钟前
【数据结构与算法】B树
java·数据结构·b树·算法
王俊山IT38 分钟前
C++学习笔记----8、掌握类与对象(五)---- 嵌套类与类中枚举
开发语言·c++·笔记·学习
Beginner_bml42 分钟前
C语言---链表
c语言·数据结构
心怀花木1 小时前
【算法】双指针
c++·算法
闫铁娃1 小时前
二分解题的奇技淫巧都有哪些,你还不会吗?
c语言·数据结构·c++·算法·leetcode
Y_3_71 小时前
【回溯数独】有效的数独(medium)& 解数独(hard)
java·数据结构·windows·算法·dfs·回溯
T0uken2 小时前
【QT Quick】C++交互:QML对象操作
c++·qt·交互
小码狐2 小时前
并查集【数据结构与算法】【C语言版-笔记】
数据结构·笔记·并查集·c语言版