DS进阶:AVL树和红黑树

news2024/12/28 14:19:14

一、AVL树

1.1 AVL树的概念

        二叉搜索树(BST)虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
(1)它的左右子树都是AVL树
(2)左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
 

AVL树有多种实现版本,但是我们采用平衡因子的版本来模拟实现AVL树。

1.2 AVL树的节点定义

        AVL树每插入或者删除一个节点,都有可能会影响高度,所以大多数情况下都需要向上调整平衡因子,所以我们的实现采用三叉链的形式(left、right、parent),方便我们找父节点,并且引入平衡因子,为的就是通过对平衡因子的调整和判断该树是否需要进行旋转。

template<class K, class V>
struct AVLTreeNode
{
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;
	pair<K, V> _kv;
	int _bf;//平衡因子 balance factor

	AVLTreeNode(const pair<K, V>& kv)
		: _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _bf(0)
	{}
};

      平衡因子的大小=右子树的高度-左子树的高度。 

1.3 AVL树的插入

        首先AVL树本质上也是BST的逻辑,只不过增加了平衡因子来控制高度。所以在按照BST的逻辑插入节点之后,我们要去向上调整平衡因子。逻辑:如果我(cur)是你(parent)的左子树,那么你的bf就-- ,如果我是你的右子树,那么你的bf就++(因为平衡因子的大小=右子树的高度-左子树的高度。

       而现在需要思考的问题就是,什么情况下需要去进行调整。因此我们需要分情况讨论

1、如果调整过后,平衡因子的绝对值为1,说明调整之前的平衡因子为0,即左右高度是相等的,此时变成1说明树的高度变了,因此需要继续向上调整。

2、如果调整过后,平衡因子的绝对值为2,说明调整之前的平衡因子绝对值为1,说明子树已经严重不平衡并且破坏了AVL树的规则,此时我们就要进行旋转。旋转过后就可以结束循环了

3、如果调整过后,平衡因子的绝对值是0,说明调整之前的平衡因子的绝对值是1,这说明之前的高度是不平衡的,插入之后反而变得更平衡了,此时就可以结束循环了。

       根据上面的这些规则,我们现将整个架子先搭建起来,然后再去研究当bf的绝对值为2的时候应该怎么去进行旋转。

	bool insert(const pair<K, V>& kv)
	{
		//如果为空树,新节点就是根
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}
		//如果不为空树
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first > kv.first) //如果我比你大,到左子树去
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (cur->_kv.first < kv.first) //比你小,你去右子树
			{
				parent = cur;
				cur = cur->_right;
			}
			else return false;//相等 
		}
		//此时肯定是对应地接在parent的后面
		cur = new Node(kv);
		if (kv.first < parent->_kv.first)   parent->_left =cur;                //比父亲小连左边
		else  parent->_right = cur; //比父亲大连右边
		//别忘了父亲指针
		cur->_parent = parent;
      //调整平衡因子
		while (parent)
		{
			//首先调整因子,如果我是你的左孩子,这时你的左边变高了,所以要--
			//如果我是你的右孩子,此时你的右边变高了,所以要++
			if (cur == parent->_left) --parent->_bf;
			else ++parent->_bf;
         //更新之后,我们要根据当前多的情况,决定是向上更新,还是旋转,还是退出
         if (abs(parent->_bf) == 1)  //更新之后变成1,说明之前是左右相等,现在高度变了
         {
				parent = parent->_parent;//继续向上更新
				cur = cur->_parent;
          }
         // 更新之后变成0,说明之前是1或者 - 1,插入后更加平衡了,此时就不需要进行下去了
        else if (abs(parent->_bf) == 0)  break;
        else if (abs(parent->_bf) == 2) //说明子树不平衡,需要对子树进行旋转处理
        {
				//……………………… (旋转)
				break;
         }
   

1.4 AVL树的旋转(重点)

每一个模块都分别画了抽象图和具象图

1.4.1 新节点插入较高左子树的左侧 (左左->右单旋)

1.4.2 新节点插入较高右子树的右侧 (右右->左单旋)

1.4.3 新节点插入较高左子树的右侧 (左右->左右双旋)

1.4.4 新节点插入较高右子树的左侧 (右左->右左双旋)

 1.4.5 旋转的代码完善

        分析完上面的四种情况,我们通过抽象图去判断具体应该怎么旋转,分别封装对应的旋转函数,然后再根据图去看看哪一些需要去调整平衡因子。统一放在我们的代码逻辑里面。 

       平时我们记忆该选择哪一种旋转方式,可以先画出折线图再一步一步去分析。

	void RotateR(Node *parent)
	{
	   //旋转前,先记录对应的节点
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		Node* ppnode = parent->_parent;//子树的前驱节点
		//先让b变成60的左边
		parent->_left = subLR;
		if (subLR) subLR->_parent = parent;
		//让60变成30的右边
		subL->_right = parent;
		parent->_parent = subL;
		//此时与前驱节点连接起来 如果前驱节点为空,直接改变根
		if (ppnode==nullptr)
		{
			_root = subL;
			_root->_parent = nullptr;
		}
		//如果前驱节点不为空,此时要根据之前paernt的情况决定插在哪边
		else
		{
			if (ppnode->_left == parent) ppnode->_left = subL;
			else ppnode->_right = subL;
			//向上连接
			subL->_parent = ppnode;
		}
		//旋转完后,别忘记调节平衡因子,按图,都为0
		subL->_bf = parent->_bf = 0;
	}


	void RotateL(Node* parent)
	{
		//旋转前,先记录对应的节点
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		Node* ppnode = parent->_parent;//子树的前驱节点
		//先让b变成30的边
		parent->_right = subRL;
		if (subRL) subRL->_parent = parent;
		//让30变成60的左边
		subR->_left = parent;
		parent->_parent = subR;
		//此时与前驱节点连接起来 如果前驱节点为空,直接改变根
		if (ppnode == nullptr)
		{
			_root = subR;
			_root->_parent = nullptr;
		}
		//如果前驱节点不为空,此时要根据之前paernt的情况决定插在哪边
		else
		{
			if (ppnode->_left == parent) ppnode->_left = subR;
			else ppnode->_right = subR;
			//向上连接
			subR->_parent = ppnode;
		}
		//旋转完后,别忘记调节平衡因子,按图,都为0
		subR->_bf = parent->_bf = 0;
	}


	void RotateLR (Node* parent)
	{
	   //旋转前,先记录相应的节点
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;
		//先对30左旋,再对90右旋
		RotateL(subL);
		RotateR(parent);
		//此时要调整平衡因子,但是有三种情况
		if (bf == -1)
		{
			parent->_bf = 1;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else if (bf == 0)
		{
			parent->_bf = 0;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else if (bf == 1)
		{
			parent->_bf = 0;
			subL->_bf = -1;
			subLR->_bf = 0;
		}
		else assert(false);
	}

	void RotateRL(Node* parent)
	{
		//旋转前,先记录相应的节点
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;
		//先对90右旋,再对30左旋
		RotateR(subR);
		RotateL(parent);
		//此时要调整平衡因子,但是有三种情况
		if (bf == -1)
		{
			parent->_bf = 0;
			subR->_bf = 1;
			subRL->_bf = 0;
		}
		else if (bf == 0)
		{
			parent->_bf = 0;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == 1)
		{
			parent->_bf = -1;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else assert(false);
	}


};

然后我们接着去根据4种旋转方式,判断insert需要调用哪一个类型的函数,完善我们的insert代码

bool insert(const pair<K, V>& kv)
{
	//如果为空树,新节点就是根
	if (_root == nullptr)
	{
		_root = new Node(kv);
		return true;
	}
	//如果不为空树
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
		if (cur->_kv.first > kv.first) //如果我比你大,到左子树去
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (cur->_kv.first < kv.first) //比你小,你去右子树
		{
			parent = cur;
			cur = cur->_right;
		}
		else return false;//相等 
	}
	//此时肯定是对应地接在parent的后面
	cur = new Node(kv);
	if (kv.first < parent->_kv.first)   parent->_left =cur;                //比父亲小连左边
	else  parent->_right = cur; //比父亲大连右边
	//别忘了父亲指针
	cur->_parent = parent;
  //调整平衡因子
	while (parent)
	{
		//首先调整因子,如果我是你的左孩子,这时你的左边变高了,所以要--
		//如果我是你的右孩子,此时你的右边变高了,所以要++
		if (cur == parent->_left) --parent->_bf;
		else ++parent->_bf;

		//更新之后,我们要根据当前多的情况,决定是向上更新,还是旋转,还是退出
		if (abs(parent->_bf) == 1)  //更新之后变成1,说明之前是左右相等,现在高度变了
		{
			parent = parent->_parent;//继续向上更新
			cur = cur->_parent;
		}
		// 更新之后变成0,说明之前是1或者 - 1,插入后更加平衡了,此时就不需要进行下去了
		else if (abs(parent->_bf) == 0)  break;
		else if (abs(parent->_bf) == 2) //说明子树不平衡,需要对子树进行旋转处理
		{
			//旋转处理 1、让子树平衡(降低高度) 2、旋转同时保证是搜索树的逻辑。
			//此时会出现四种情况,左单旋 右单旋 左右双旋转  右左双旋
		
			//情况1 右单旋   较高左子树的左侧
			if (parent->_bf == -2 && cur->_bf == -1)  RotateR(parent);
			//情况2 左单旋   较高右子树的右侧
			else if (parent->_bf == 2 && cur->_bf == 1)  RotateL(parent);
			//情况3 左右双旋  较高左子树的右侧
			else if (parent->_bf == -2 && cur->_bf == 1) RotateLR(parent);
		    //情况4  右左双旋  较高右子树的左侧
			else if (parent->_bf == 2 && cur->_bf == -1) RotateRL(parent);
			else assert(false);//加个断言防止旋转出问题
			//旋转处理完之后,可以直接break
			break;
		}
		else  assert(false);//加个断言,防止平衡因子出问题
	}
	
	return true;
}

1.5 AVL树的验证

1.5.1 验证其为二叉搜索树

        这个比较容易,我们可以直接通过一个中序遍历,如果打印出来之后得到的是一个有序序列,说明这就是一个二叉搜索树。但是接下来我们还得判断他是否是平衡树。

void Inorder()//中序遍历接口
{
	_Inorder(_root);
	cout << endl;
}

void _Inorder(Node*root)
{
	if (root == nullptr) return;
	_Inorder(root->_left);
	cout << root->_kv.first << " ";
	_Inorder(root->_right);
}

1.5.2 验证节点的平衡因子是否等于右子树的高度-左子树的高度

     首先我们需要封装一个计算树高度的函数。需要用到后序遍历。

int Height()  //搜索树的高度
{
	return _Height(_root);
}
int _Height(Node* root)
{
	if (root == nullptr) return 0;
	int leftH = _Height(root->_left);
	int rightH = _Height(root->_right);
	return leftH > rightH ? leftH + 1 : rightH + 1;
}

       然后看看我们遍历到当前节点的时候,我们用该函数算出左子树的高度和右子树的高度,看看右子树-左子树是否等于平衡因子。 

1.5.3 以每个节点为根的树他的左右子树是否严格遵守高度差

      但是平衡因子是我们自己去调整的,所以我们最关键的还是去判断我们的左右子树的高度差绝对值是否<2 

     需要注意的是,我们必须确保每一个子树都满足AVL树的性质,所以调用完一次isbalance之后,还得去继续判断他的左子树和右子树!!!  这样才能说明他是一颗平衡树!!   

bool IsBalance()
{
	return _IsBalance(_root);
}

bool _IsBalance(Node* root)
{
	if (root == nullptr) return true;
	int leftH = _Height(root->_left);
	int rightH = _Height(root->_right);

	if (rightH - leftH != root->_bf) //检查平衡因子是否符合要求
	{
	  cout<< root->_kv.first << "节点平衡因子异常" << endl;
	  return false;
	}


	return abs(leftH - rightH) < 2 && _IsBalance(root->_left) && _IsBalance(root->_right);
}

 1.6 AVL树的性能

      AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log2N。但是如果要对AVL树做一些结构修改的操
作,性能非常低下
,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,
有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数
据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

     为了解决这个问题,红黑树出现了!!

二、红黑树

2.1 红黑树的概念

        红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或
Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的(通过几条规则达到近似平衡)。

    总的来说,BST可能会退化成单支树,而AVL旋转过于严格,RB就是一种退而求其次的解决方法。 比如下面这颗树,AVL树一定旋转,但是RB不一定旋转。

 2.2 红黑树的性质

    接下来我们就来探究,大佬是如何通过几条规则来让红黑树确保没有一条路径会比其他路径长出俩倍的。

1. 每个结点不是红色就是黑色
2. 根节点是黑色的
3. 如果一个节点是红色的,则它的两个孩子结点是黑色的
4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
 

 思考:为什么满足上面的性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点
个数的两倍?

 2.3 红黑树节点的定义

和将AVL树的平衡因此换成颜色

enum Colour
{
	RED,
	BLACK,
};

template<class K, class V>
struct RBTreeNode
{
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	pair<K, V> _kv;
	Colour _col;
	RBTreeNode(const pair<K, V>& kv)
		: _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _col(RED)
	{}

};

思考:为什么我们默认要把颜色设置成红色??

      要分析究竟设置成红色还是黑色好一点,取决于以上的两条规则(1)如果一个节点是红色的,则它的两个孩子结点是黑色的 。(2)对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点

      首先我们要知道,插入这个节点之前该树一定是保持红黑树的性质,即满足上面的两个规则,如果我们新插入的节点默认选择黑色的话,那么凭空多出来的黑色节点必然会导致规则2被破坏,也就是说我们每插入一次就要去调整。如果我们选择的是红色的话,我们红色的父亲节点是黑色,那么就不需要进行调整,如果父亲节点是红色,我们才要进行调整!!!总的来说就是黑色是必然会破坏规则的,但是红色不一定,所以我们默认选择设置成红色,这样需要考虑的情况会更少一些。

2.4 红黑树的插入和旋转(重点)

情况1: 首先因为我们把默认节点设置为红色,所以如果被插入位置的父亲节点是黑色的话,就不需要进行调整了。

情况2:如果待插入前是空树, 那么新插入元素自动成为根,并且将其设置为黑色

      前两个情况比较简单,而后三个情况就是红黑树的非常关键的逻辑!!首先我们要知道我们调整的前提是当前待插入节点的父节点也是红色。

      如上图我们可以知道,因为待插入节点的父节点必然是红色,所以其祖父节点g必然是黑色,而我们下面情况的分析就是取决于u节点 

情况3:u存在且为红

 情况4:u存在且为黑,由情况3变化而来,插入在较高子树的同一侧(单旋)

 情况5:u存在且为黑,由情况3变化而来,插入在较高子树的另一侧(双旋) 

 总结:

 2.5 红黑树旋转和插入代码实现


	//旋转代码和AVL树是一样的,只不过不需要搞平衡因子
	void RotateL(Node* parent)
	{
		//旋转前,先记录对应的节点
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		Node* ppnode = parent->_parent;//子树的前驱节点
		//先让b变成30的边
		parent->_right = subRL;
		if (subRL) subRL->_parent = parent;
		//让30变成60的左边
		subR->_left = parent;
		parent->_parent = subR;
		//此时与前驱节点连接起来 如果前驱节点为空,直接改变根
		if (ppnode == nullptr)
		{
			_root = subR;
			_root->_parent = nullptr;
		}
		//如果前驱节点不为空,此时要根据之前paernt的情况决定插在哪边
		else
		{
			if (ppnode->_left == parent) ppnode->_left = subR;
			else ppnode->_right = subR;
			//向上连接
			subR->_parent = ppnode;
		}
	}


	void RotateR(Node* parent)
	{
		//旋转前,先记录对应的节点
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		Node* ppnode = parent->_parent;//子树的前驱节点
		//先让b变成60的左边
		parent->_left = subLR;
		if (subLR) subLR->_parent = parent;
		//让60变成30的右边
		subL->_right = parent;
		parent->_parent = subL;
		//此时与前驱节点连接起来 如果前驱节点为空,直接改变根
		if (ppnode == nullptr)
		{
			_root = subL;
			_root->_parent = nullptr;
		}
		//如果前驱节点不为空,此时要根据之前paernt的情况决定插在哪边
		else
		{
			if (ppnode->_left == parent) ppnode->_left = subL;
			else ppnode->_right = subL;
			//向上连接
			subL->_parent = ppnode;
		}
	}


//先用搜索树的逻辑插入节点,然后再去更新平衡因子。
bool Insert(const pair<K, V>& kv)
{
	//如果为空树,新节点就是根
	if (_root == nullptr)
	{
		_root = new Node(kv);
		_root->_col=BLACK;
		return true;
	}
	//如果不为空树
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
		if (cur->_kv.first > kv.first) //如果我比你大,到左子树去
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (cur->_kv.first < kv.first) //比你小,你去右子树
		{
			parent = cur;
			cur = cur->_right;
		}
		else return false;//相等 
	}
	//此时肯定是对应地接在parent的后面
	cur = new Node(kv);
	if (kv.first < parent->_kv.first)   parent->_left = cur;                //比父亲小连左边
	else  parent->_right = cur; //比父亲大连右边
	//别忘了父亲指针
	cur->_parent = parent;
	
	
	while (parent && parent->_col == RED)
	{
		Node* grandfather = parent->_parent;
		//情况1,如果u为存在且为红
		if (grandfather->_left == parent)//如果p是g的左边,u就在右边
		{
			Node* uncle = grandfather->_right;
			//情况1,如果u为存在且为红 p u变黑,g变红 向上调整
			if (uncle && uncle->_col == RED)
			{
				parent->_col = BLACK;
				uncle->_col = BLACK;
				grandfather->_col = RED;
				//继续向上调整
				cur = grandfather;
				parent = cur->_parent;
			}
			else //情况2或者情况3, u为黑或者不存在   旋转+变色
			{ 
				if (cur == parent->_left) //情况2 右单旋+p变黑 g变红
				{
					//      g
					//   p    u
					// c
					RotateR(grandfather);
					parent->_col = BLACK;
					grandfather->_col = RED;
				}
				else  //情况3 右左双旋  c变黑 g变红
				{
					//          g
						//   p     u
						//     c
					RotateL(parent);
					RotateR(grandfather);
					cur->_col = BLACK;
					grandfather->_col = RED;
				}
				break;//情况2和情况3都要跳出循环
			}
		
		}
		else//if (grandfather->_right == parent)//如果p是g的右边,u就在左边    几乎一样,就是旋转的逻辑不同
		{
				Node* uncle = grandfather->_left;
				//情况1,如果u为存在且为红 p u变黑,g变红 向上调整
				if (uncle && uncle->_col == RED)
				{
					parent->_col = BLACK;
					uncle->_col = BLACK;
					grandfather->_col = RED;
					//继续向上调整
					cur = grandfather;
					parent = cur->_parent;
				}
				else//情况2或者情况3, u为黑或者不存在   旋转+变色
				{
					if (cur == parent->_right) //情况2 左单旋+p变黑 g变红
					{
						//      g
						//   p    u
						//          c
						RotateL(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else  //情况3 左右双旋  c变黑 g变红
					{
						//          g
							//   p     u
							//       c
						RotateR(parent);
						RotateL(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;//情况2和情况3都要跳出循环
				}
		}
	}
	_root->_col = BLACK; //预防情况1出现 parent就是根的情况 此时无论如何_root变成黑,总没错
	return true;
}

 2.6 红黑树的验证规则

bool IsBalance()
{
	if (_root && _root->_col == RED)
	{
		cout << "根节点颜色是红色" << endl;
		return false;
	}
	int benchmark = 0;//找到一条路径作为基准值 然后看看其他路径是否相等
	Node* cur = _root;
	while (cur)
	{
		if (cur->_col == BLACK)
			++benchmark;
		cur = cur->_left;
	}

	// 连续红色节点
	return _Check(_root, 0, benchmark);
}
bool _Check(Node* root, int blackNum, int benchmark)
{
	if (root == nullptr)
	{
		if (benchmark != blackNum)
		{
			cout << "某条路径黑色节点的数量不相等" << endl;
			return false;
		}

		return true;
	}

	if (root->_col == BLACK)
	{
		++blackNum;
	}

	if (root->_col == RED
		&& root->_parent
		&& root->_parent->_col == RED)
	{
		cout << "存在连续的红色节点" << endl;
		return false;
	}

	return _Check(root->_left, blackNum, benchmark)
		&& _Check(root->_right, blackNum, benchmark);
}

2.7 RB和AVL的比较

1、红黑树不追求"完全平衡",即不像AVL那样要求节点的高度差 <= 1,它只要求部分达到平衡,但是提出了为节点增加颜色,红黑树是用非严格的平衡来换取增删节点时候旋转次数的降低。

2、就插入节点导致树失衡的情况,AVL和RB-Tree都是最多两次树旋转来实现复衡rebalance,旋转的量级是O(1),删除节点导致失衡,AVL需要维护从被删除节点到根节点root这条路径上所有节点的平衡,旋转的量级为O(logN),而RB-Tree最多只需要旋转3次实现复衡,只需O(1),所以说RB-Tree删除节点的rebalance的效率更高,开销更小!

 总结:

( 1 )AVL更平衡,结构上更加直观,时间效能针对读取而言更高(搜索效率高);维护稍慢,空间开销较大。
( 2 ) 红黑树,读取略逊于AVL,维护强于AVL(复衡效率高),空间开销与AVL类似,内容极多时略优于AVL,维护优于AVL。

(3)总体来说RB的整体性能高于AVL,因此在实际应用中基本上都是用的RB。

2.8 红黑树的实际应用

1. C++ STL库 -- map/set、mutil_map/mutil_set
2. Java 库
3. linux内核
4. 其他一些库

     在后面关于封装map和set的过程中,会再次用到红黑树的知识,因为STL底层的架子就是用的红黑树。

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

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

相关文章

为什么深度学习模型在 GPU 上运行得更快:CUDA 编程简介

如今,当我们谈论深度学习时,通常会将其实现与利用 GPU 来提高性能联系起来。 GPU(图形处理单元)最初设计用于加速图像、2D 和 3D 图形的渲染。然而,由于它们能够执行许多并行操作,因此它们的实用性超出了深度学习等应用程序。 GPU 在深度学习模型中的使用始于 2000 年代…

Unity读书系列《Unity高级编程:主程手记》——架构

文章目录 前言一、架构的意义1、承载力2、可扩展性3、易用性4、可伸缩性5、容错性以及错误的感知力 二、软件架构的思维方式二、构建Unity项目1、前端和后端架构之间2、培养架构设计思路3、Unity项目的分层设计 总结 前言 这篇文章是《Unity高级编程&#xff1a;主程手记》的第…

【源码】WHMCS 虚拟主机计费系统 易支付插件 USDT收款插件 支付宝 微信收款

【源码介绍】 WHMCS 虚拟主机计费系统 易支付插件 USDT收款插件 支付宝 微信收款 【源码说明】 WHMCS是一个国外的专业虚拟主机计费系统&#xff0c;功能很强大&#xff0c;这里分享一个7、8版本都可用的易支付 需要对接USDT可以谷歌下载易支付USDT插件&#xff0c;主机对接…

【R语言实战】——kNN和朴素贝叶斯方法实战

&#x1f349;CSDN小墨&晓末:https://blog.csdn.net/jd1813346972 个人介绍: 研一&#xff5c;统计学&#xff5c;干货分享          擅长Python、Matlab、R等主流编程软件          累计十余项国家级比赛奖项&#xff0c;参与研究经费10w、40w级横向 文…

wifi可以连接但是上不了网该怎么解决?

上网的过程中&#xff0c;我们有时候会遇到wifi可以连接但是上不了网的情况&#xff0c;打开电脑浏览器&#xff0c;显示域名解析错误。遇到这种情况&#xff0c;一般说明IP与站点的解析过程出现了错误。 在网络中的主机都是IP地址来标识的&#xff0c;如果在浏览器输入此IP地…

美国言语听力学会(ASHA)关于非处方 (OTC) 助听器的媒体声明(翻译稿)

美国国会于 2021 年 4 月 13 日批准美国听力学会积极提供建议&#xff0c;并一直积极参与制定FDA关于非处方助听器销售的拟议法规。根据2017年通过的立法授权。学院积极参与帮助塑造授权立法&#xff0c;并就即将出台的条例分享了建议。 根据美国卫生与公众服务部NIH / NIDCD的…

数据分析:扩增子-16s rRNA分析snakemake流程

介绍 扩增子测序是分析环境微生物的常见手段&#xff0c;通常使用的是16s rRNA片段。16srRNA分析主要有质控、去冗余、聚类OTU、去嵌合体、生成OTU表和物种注释等步骤。更多知识分享请到 https://zouhua.top/。 先看看前期数据处理的可视化图。 数据 18份来自宏基因组公众号…

C# WinForm —— 08 Form初始化、布局、注册事件

Form 初始化 Form初始化的时候会调用 Designer.cs 里的 InitializeComponent(); 函数&#xff0c;在InitializeComponent(); 函数里面有Load Form语句时会调用 FrmLogin_Load()函数 Form布局 两种方式&#xff1a; 拖控件到窗体&#xff0c;设置属性在Load事件中写代码添加…

线性神经网络示例

通过5个条件判定一件事情是否会发生&#xff0c;5个条件对这件事情是否发生的影响力不同&#xff0c;计算每个条件对这件事情发生的影响力多大&#xff0c;写一个线性神经网络模型pytorch程序,最后打印5个条件分别的影响力。 一 在这个场景中&#xff0c;一个线性神经网络&…

knife4j swagger 使用笔记

1.接口访问的端口跟后台设置的不一致&#xff0c;接口请求无反应 处理办法 2.响应参数不显示问题 &#xff08;1&#xff09;返回的参数里面一定要有响应的参数对象&#xff0c;如下&#xff1a; &#xff08;2&#xff09;TableDataInfo 定义成泛型类 TableDataInfo package…

Int4:Lucene 中的更多标量量化

作者&#xff1a;来自 Elastic Benjamin Trent, Thomas Veasey 在 Lucene 中引入 Int4 量化 在之前的博客中&#xff0c;我们全面介绍了 Lucene 中标量量化的实现。 我们还探索了两种具体的量化优化。 现在我们遇到了一个问题&#xff1a;int4 量化在 Lucene 中是如何工作的以…

软件需求管理规程(Word原件2024)

软件开发人员及用户往往容易忽略信息沟通&#xff0c;这导致软件开发出来后不能很好地满足用户的需要&#xff0c;从而造成返工。而返工不仅在技术上给开发人员带来巨大的麻烦&#xff0c;造成人力、物力的浪费&#xff0c;而且软件的性能也深受影响。所以在软件项目开发周期的…

单片机为什么有多组VDD?

以前我在画尺寸小的PCB时&#xff0c;比较头痛&#xff0c;特别是芯片引脚又多的&#xff0c;芯片底下&#xff0c;又不能打太多过孔。 可能有些老铁也比较好奇&#xff0c;为什么一个单片机芯片&#xff0c;有这么多组VDD和VSS。 比如下面这个100个引脚的STM32单片机。 有5组…

前端实现将当前页面内容下载成图片(图片可做到高清画质)

插件背景&#xff1a; html2canvas可以把你想要转变的元素变为图片&#xff0c;使用file-saver下载图片。 1、安装html2canvas、file-saver npm install html2canvasnpm install file-saver --save 2、在Vue组件中引入并使用html2canvas、file-saver import html2canvas fro…

智慧旅游开启智慧出行新时代,科技引领旅行新风尚:以科技为引领,推动旅游业智慧化升级,为旅行者提供更加便捷、高效的旅行服务

一、引言 随着信息技术的飞速发展&#xff0c;智慧旅游作为一种全新的旅游形态&#xff0c;正逐渐改变着人们的出行方式。它利用现代科技手段&#xff0c;实现旅游资源的智能化管理、旅游信息的智能化传播和旅游服务的智能化提供&#xff0c;为旅行者带来更加便捷、高效的旅行…

Qt下使用OpenCV截取图像并在QtableWidget表格上显示

文章目录 前言一、在QLabel上显示图片并绘制矩形框二、保存矩形框数据为CSV文件三、保存截取图像四、将截取图像填充到表格五、图形视图框架显示图像六、示例完整代码总结 前言 本文主要讲述了在Qt下使用OpenCV截取绘制的矩形框图像&#xff0c;并将矩形框数据保存为CSV文件&a…

气膜仓库:现代化仓储新选择—轻空间

气膜仓库&#xff0c;作为现代化仓储的新选择&#xff0c;越来越受到人们的青睐。相比传统料仓&#xff0c;气膜仓库具有诸多优势&#xff0c;使其成为各行各业的首选储存解决方案。 1. 高效节能 气膜仓库的建设周期短&#xff0c;基础简单&#xff0c;安装快捷&#xff0c;能耗…

C#命名空间常用函数

在C#中&#xff0c;不同命名空间下有各种常用函数&#xff0c;下面列举一些常见的函数及其对应的命名空间&#xff1a; System命名空间&#xff1a; Console.WriteLine()&#xff1a;用于向控制台输出信息。Convert.ToInt32()&#xff1a;用于将其他数据类型转换为整数类型。 S…

Kafka 3.x.x 入门到精通(05)——对标尚硅谷Kafka教程

Kafka 3.x.x 入门到精通&#xff08;05&#xff09;——对标尚硅谷Kafka教程 2. Kafka基础2.1 集群部署2.2 集群启动2.3 创建主题2.4 生产消息2.5 存储消息2.6 消费消息2.6.1 消费消息的基本步骤2.6.2 消费消息的基本代码2.6.3 消费消息的基本原理2.6.3.1消费者组2.6.3.1.1 消费…

凹凸技术揭秘·羚珑智能设计平台·逐梦设计数智化

从技术和功能形态层面&#xff0c;我们把设计数智化分成了两个方向&#xff0c;一个方向是「模板化设计」&#xff0c;另一个方向是「程序化设计」。 2、模板化设计— 「模板化设计」的核心目标&#xff1a;是实现线下设计物料的数字化&#xff0c;在数字化设计资产的基础之上…