AVL树【C++实现】

news2024/6/30 0:19:46

文章目录

    • AVL树的概念
    • AVL树节点的定义
    • AVL树的插入
    • AVL树的旋转
        • 新节点插入较高右子树的右侧---右右:左单旋
        • 新节点插入较高左子树的左侧---左左:右单旋
        • 新节点插入较高左子树的右侧---左右:先左单旋再右单旋
        • 新节点插入较高右子树的左侧---右左:先右单旋再左单旋
    • AVL树的验证
    • AVL树的查找
    • AVL树的修改
        • 修改函数一
        • 修改函数二
        • 重载[ ]
    • AVL树的删除
    • AVL树的性能

AVL树的概念

  • AVL树(英语:AVL tree)是计算机科学中最早被发明的自平衡二叉查找树。在AVL树中,任一节点对应的两棵子树的最大高度差为1,因此它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下的时间复杂度都是 𝑂(log𝑛)。增加和删除元素的操作则可能需要借由一次或多次树旋转,以实现树的重新平衡。AVL树得名于它的发明者格奥尔吉·阿杰尔松-韦利斯基和叶夫根尼·兰迪斯,他们在1962年的论文《An algorithm for the organization of information》中公开了这一数据结构。

  • 节点的平衡因子是它的左子树的高度减去它的右子树的高度(有时相反)。带有平衡因子1、0或 -1的节点被认为是平衡的。带有平衡因子 -2或2的节点被认为是不平衡的,并需要重新平衡这个树。平衡因子可以直接存储在每个节点中,或从可能存储在节点中的子树高度计算出来。

一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

  • 它的左右子树都是AVL树

  • 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
    如:

  • 平衡因子 = 右子树-左子树

非AVL树:
在这里插入图片描述

  • 在平衡之后:

在这里插入图片描述

在这里插入图片描述

  • 如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在
    O ( l o g 2 n ) O(log_2 n) O(log2n),搜索时间复杂度O( l o g 2 n log_2 n log2n)。

AVL树节点的定义

template<class K, class V>
struct AVLTreeNode
{
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parnet;
	pair<K, V> _kv;

	int _bf; // balance factor

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

AVL树的插入

AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么
AVL树的插入过程可以分为两步:

  1. 按照二叉搜索树的方式插入新节点
  2. 调整节点的平衡因子
    • 插入到父亲的左边,父亲平衡因子--
    • 插入到父亲的右边,父亲平衡因子++
    • 父亲 bf 平衡因子 == 0,父亲所在子树高度不变,不用继续往上更新,插入结束
    • 父亲 bf 平衡因子 == 1 或者 -1,父亲所在子树高度变了,必须往上更新
    • 父亲 bf 更新后 == 2 或者 -2 ,父亲所在的子树已经不平衡,需要旋转处理
  • 更新后不可能出现其他值,插入之前树是AVL树,平衡因子要么是1,-1,0,++,--,
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->_right;
		}
		else if (cur->kv.first > kv.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else
		{
			return false;
		}
	}

	// 插入节点
	cur = new Node(kv);
	if (parent->_kv.first < kv.first)
		parent->_right = cur;
	else
		parent->_left = cur;
	cur->_parnet = parent;
	
	// 更新平衡因子
	while (parent)
	{
		if (cur == parent->_left)
			parent->_bf--;
		else
			parent->_bf++;
	
		if (parent->_bf == 0) // 更新结束
			break;
		else if (parent->_bf == 1 || parent->_bf == -1)
		{
			// 继续更新
			cur = parent;
			parent = parent->_parnet;
		}
		else if (parent->_bf == -2 || parent->_bf == 2)
		{
			// 需要旋转
			// ...
			break;
		}
		else
		{
			assert(false);
		}
	}

	return true;
}

AVL树的旋转

如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种

假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑

  1. pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR
    • 当pSubR的平衡因子为1时,执行左单旋
    • 当pSubR的平衡因子为-1时,执行右左双旋
  2. pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL
    • 当pSubL的平衡因子为-1是,执行右单旋
    • 当pSubL的平衡因子为1时,执行左右双旋

在这里插入图片描述

  • 旋转完成后,原pParent为根的子树个高度降低,已经平衡,不需要再向上更新。
新节点插入较高右子树的右侧—右右:左单旋

在这里插入图片描述

左单旋的步骤如下:

  1. 让subR的左子树作为parent的右子树。
  2. 让parent作为subR的左子树。
  3. 让subR作为整个子树的根。
  4. 更新平衡因子。

在这里插入图片描述

//左单旋
void RotateL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	Node* parentParent = parent->_parent;

	//1、建立subR和parent之间的关系
	parent->_parent = subR;
	subR->_left = parent;

	//2、建立parent和subRL之间的关系
	parent->_right = subRL;
	if (subRL)
		subRL->_parent = parent;

	//3、建立parentParent和subR之间的关系
	if (parentParent == nullptr)
	{
		_root = subR;
		subR->_parent = nullptr; //subR的_parent指向需改变
	}
	else
	{
		if (parent == parentParent->_left)
		{
			parentParent->_left = subR;
		}
		else //parent == parentParent->_right
		{
			parentParent->_right = subR;
		}
		subR->_parent = parentParent;
	}

	//4、更新平衡因子
	subR->_bf = parent->_bf = 0;
}
新节点插入较高左子树的左侧—左左:右单旋

在这里插入图片描述

  • 上图在插入前,AVL树是平衡的,新节点插入到30的左子树(注意:此处不是左孩子)中,30左
    子树增加了一层,导致以60为根的二叉树不平衡,要让60平衡,只能将60左子树的高度减少一层,右子树增加一层,即将左子树往上提,这样60转下来,因为60比30大,只能将其放在30的右子树,而如果30有右子树,右子树根的值一定大于30,小于60,只能将其放在60的左子树,旋转完成后,更新节点的平衡因子即可。在旋转过程中,有以下几种情况需要考虑:
  1. 30节点的右孩子可能存在,也可能不存在
  2. 60可能是根节点,也可能是子树
  • 如果是根节点,旋转完成后,要更新根节点
  • 如果是子树,可能是某个节点的左子树,也可能是右子树
    在这里插入图片描述

右单旋的步骤如下:

  1. 让subL的右子树作为parent的左子树。
  2. 让parent作为subL的右子树。
  3. 让subL作为整个子树的根。
  4. 更新平衡因子。
// 右单旋
void RotateR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	Node* parentParent = parent->_parent;

	//1、建立subL和parent之间的关系
	subL->_right = parent;
	parent->_parent = subL;

	//2、建立parent和subLR之间的关系
	parent->_left = subLR;
	if (subLR)
		subLR->_parent = parent;

	//3、建立parentParent和subL之间的关系
	if (parentParent == nullptr)
	{
		_root = subL;
		_root->_parent = nullptr;
	}
	else
	{
		if (parent == parentParent->_left)
		{
			parentParent->_left = subL;
		}
		else //parent == parentParent->_right
		{
			parentParent->_right = subL;
		}
		subL->_parent = parentParent;
	}

	//4、更新平衡因子
	subL->_bf = parent->_bf = 0;
}
新节点插入较高左子树的右侧—左右:先左单旋再右单旋

在这里插入图片描述

  • 将双旋变成单旋后再旋转,即:先对30进行左单旋,然后再对90进行右单旋,旋转完成后再考虑平衡因子的更新。

左右双旋的步骤如下:

  1. 以subL为旋转点进行左单旋。
  2. 以parent为旋转点进行右单旋。
  3. 更新平衡因子。
//左右双旋
void RotateLR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	int bf = subLR->_bf; //subLR不可能为nullptr,因为subL的平衡因子是1

	//1、以subL为旋转点进行左单旋
	RotateL(subL);

	//2、以parent为旋转点进行右单旋
	RotateR(parent);

	//3、更新平衡因子
	if (bf == 1)
	{
		subLR->_bf = 0;
		subL->_bf = -1;
		parent->_bf = 0;
	}
	else if (bf == -1)
	{
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 1;
	}
	else if (bf == 0)
	{
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 0;
	}
	else
	{
		assert(false); //在旋转前树的平衡因子就有问题
	}
}
新节点插入较高右子树的左侧—右左:先右单旋再左单旋

在这里插入图片描述

右左双旋的步骤如下:

  1. 以subR为旋转点进行右单旋。
  2. 以parent为旋转点进行左单旋。
  3. 更新平衡因子。
//右左双旋
void RotateRL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	int bf = subRL->_bf;

	//1、以subR为轴进行右单旋
	RotateR(subR);

	//2、以parent为轴进行左单旋
	RotateL(parent);

	//3、更新平衡因子
	if (bf == 1)
	{
		subRL->_bf = 0;
		parent->_bf = -1;
		subR->_bf = 0;
	}
	else if (bf == -1)
	{
		subRL->_bf = 0;
		parent->_bf = 0;
		subR->_bf = 1;
	}
	else if (bf == 0)
	{
		subRL->_bf = 0;
		parent->_bf = 0;
		subR->_bf = 0;
	}
	else
	{
		assert(false); //在旋转前树的平衡因子就有问题
	}
}

AVL树的验证

AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:

  1. 验证其为二叉搜索树如果中序遍历可得到一个有序的序列,就说明为二叉搜索树。
  2. 验证其为平衡树每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)节点的平衡因子是否计算正确。
//判断是否为AVL树
bool IsAVLTree()
{
	int hight = 0; //输出型参数
	return _IsBalanced(_root, hight);
}
//检测二叉树是否平衡
bool _IsBalanced(Node* root, int& hight)
{
	if (root == nullptr) //空树是平衡二叉树
	{
		hight = 0; //空树的高度为0
		return true;
	}
	//先判断左子树
	int leftHight = 0;
	if (_IsBalanced(root->_left, leftHight) == false)
		return false;
	//再判断右子树
	int rightHight = 0;
	if (_IsBalanced(root->_right, rightHight) == false)
		return false;
	//检查该结点的平衡因子
	if (rightHight - leftHight != root->_bf)
	{
		cout << "平衡因子设置异常:" << root->_kv.first << endl;
	}
	//把左右子树的高度中的较大值+1作为当前树的高度返回给上一层
	hight = max(leftHight, rightHight) + 1;
	return abs(rightHight - leftHight) < 2; //平衡二叉树的条件
}

AVL树的查找

AVL树的查找函数与二叉搜索树的查找方式一模一样,逻辑如下:

  1. 若树为空树,则查找失败,返回nullptr。
  2. 若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。
  3. 若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。
  4. 若key值等于当前结点的值,则查找成功,返回对应结点。
//查找函数
Node* Find(const K& key)
{
	Node* cur = _root;
	while (cur)
	{
		if (key < cur->_kv.first) //key值小于该结点的值
		{
			cur = cur->_left; //在该结点的左子树当中查找
		}
		else if (key > cur->_kv.first) //key值大于该结点的值
		{
			cur = cur->_right; //在该结点的右子树当中查找
		}
		else //找到了目标结点
		{
			return cur; //返回该结点
		}
	}
	return nullptr; //查找失败
}

AVL树的修改

修改函数一
//修改函数
bool Modify(const K& key, const V& value)
{
	Node* ret = Find(key);
	if (ret == nullptr) //未找到指定key值的结点
	{
		return false;
	}
	ret->_kv.second = value; //修改结点的value
	return true;
}
修改函数二
  • 若待插入结点的键值key在map当中不存在,则结点插入成功,并返回插入后结点的指针和true。
  • 若待插入结点的键值key在map当中已经存在,则结点插入失败,并返回map当中键值为key的结点的指针和false。
//插入函数
pair<Node*, bool> Insert(const pair<K, V>& kv)
{
	if (_root == nullptr) //若AVL树为空树,则插入结点直接作为根结点
	{
		_root = new Node(kv);
		return make_pair(_root, true); //插入成功,返回新插入结点和true
	}
	//1、按照二叉搜索树的插入方法,找到待插入位置
	Node* cur = _root;
	Node* parent = nullptr;
	while (cur)
	{
		if (kv.first < cur->_kv.first) //待插入结点的key值小于当前结点的key值
		{
			//往该结点的左子树走
			parent = cur;
			cur = cur->_left;
		}
		else if (kv.first > cur->_kv.first) //待插入结点的key值大于当前结点的key值
		{
			//往该结点的右子树走
			parent = cur;
			cur = cur->_right;
		}
		else //待插入结点的key值等于当前结点的key值
		{
			//插入失败(不允许key值冗余)
			return make_pair(cur, false); //插入失败,返回已经存在的结点和false
		}
	}

	//2、将待插入结点插入到树中
	cur = new Node(kv); //根据所给值构造一个新结点
	Node* newnode = cur; //记录新插入的结点
	if (kv.first < parent->_kv.first) //新结点的key值小于parent的key值
	{
		//插入到parent的左边
		parent->_left = cur;
		cur->_parent = parent;
	}
	else //新结点的key值大于parent的key值
	{
		//插入到parent的右边
		parent->_right = cur;
		cur->_parent = parent;
	}

	//3、更新平衡因子,如果出现不平衡,则需要进行旋转
	while (cur != _root) //最坏一路更新到根结点
	{
		if (cur == parent->_left) //parent的左子树增高
		{
			parent->_bf--; //parent的平衡因子--
		}
		else if (cur == parent->_right) //parent的右子树增高
		{
			parent->_bf++; //parent的平衡因子++
		}
		//判断是否更新结束或需要进行旋转
		if (parent->_bf == 0) //更新结束(新增结点把parent左右子树矮的那一边增高了,此时左右高度一致)
		{
			break; //parent树的高度没有发生变化,不会影响其父结点及以上结点的平衡因子
		}
		else if (parent->_bf == -1 || parent->_bf == 1) //需要继续往上更新平衡因子
		{
			//parent树的高度变化,会影响其父结点的平衡因子,需要继续往上更新平衡因子
			cur = parent;
			parent = parent->_parent;
		}
		else if (parent->_bf == -2 || parent->_bf == 2) //需要进行旋转(此时parent树已经不平衡了)
		{
			if (parent->_bf == -2)
			{
				if (cur->_bf == -1)
				{
					RotateR(parent); //右单旋
				}
				else //cur->_bf == 1
				{
					RotateLR(parent); //左右双旋
				}
			}
			else //parent->_bf == 2
			{
				if (cur->_bf == -1)
				{
					RotateRL(parent); //右左双旋
				}
				else //cur->_bf == 1
				{
					RotateL(parent); //左单旋
				}
			}
			break; //旋转后就一定平衡了,无需继续往上更新平衡因子(旋转后树高度变为插入之前了)
		}
		else
		{
			assert(false); //在插入前树的平衡因子就有问题
		}
	}

	return make_pair(newnode, true); //插入成功,返回新插入结点和true
}
重载[ ]
  1. 调用插入函数插入键值对。
  2. 拿出从插入函数获取到的结点。
  3. 返回该结点value的引用。
  • 如果key不在树中,则先插入键值对<key, V()>,然后返回该键值对中value的引用。
  • 如果key已经在树中,则返回键值为key结点value的引用。
//operator[]重载
V& operator[](const K& key)
{
	//调用插入函数插入键值对
	pair<Node*, bool> ret = Insert(make_pair(key, V()));
	//拿出从插入函数获取到的结点
	Node* node = ret.first;
	//返回该结点value的引用
	return node->_kv.second;
}

AVL树的删除

  • 删除方法和二叉搜索树相同
  1. 先找到待删除的结点。
  2. 若找到的待删除结点的左右子树均不为空,则需要使用替换法进行删除。
//删除函数
bool Erase(const K& key)
{
	//用于遍历二叉树
	Node* parent = nullptr;
	Node* cur = _root;
	//用于标记实际的删除结点及其父结点
	Node* delParentPos = nullptr;
	Node* delPos = nullptr;
	while (cur)
	{
		if (key < cur->_kv.first) //所给key值小于当前结点的key值
		{
			//往该结点的左子树走
			parent = cur;
			cur = cur->_left;
		}
		else if (key > cur->_kv.first) //所给key值大于当前结点的key值
		{
			//往该结点的右子树走
			parent = cur;
			cur = cur->_right;
		}
		else //找到了待删除结点
		{
			if (cur->_left == nullptr) //待删除结点的左子树为空
			{
				if (cur == _root) //待删除结点是根结点
				{
					_root = _root->_right; //让根结点的右子树作为新的根结点
					if (_root)
						_root->_parent = nullptr;
					delete cur; //删除原根结点
					return true; //根结点无祖先结点,无需进行平衡因子的更新操作
				}
				else
				{
					delParentPos = parent; //标记实际删除结点的父结点
					delPos = cur; //标记实际删除的结点
				}
				break; //删除结点有祖先结点,需更新平衡因子
			}
			else if (cur->_right == nullptr) //待删除结点的右子树为空
			{
				if (cur == _root) //待删除结点是根结点
				{
					_root = _root->_left; //让根结点的左子树作为新的根结点
					if (_root)
						_root->_parent = nullptr;
					delete cur; //删除原根结点
					return true; //根结点无祖先结点,无需进行平衡因子的更新操作
				}
				else
				{
					delParentPos = parent; //标记实际删除结点的父结点
					delPos = cur; //标记实际删除的结点
				}
				break; //删除结点有祖先结点,需更新平衡因子
			}
			else //待删除结点的左右子树均不为空
			{
				//替换法删除
				//寻找待删除结点右子树当中key值最小的结点作为实际删除结点
				Node* minParent = cur;
				Node* minRight = cur->_right;
				while (minRight->_left)
				{
					minParent = minRight;
					minRight = minRight->_left;
				}
				cur->_kv.first = minRight->_kv.first; //将待删除结点的key改为minRight的key
				cur->_kv.second = minRight->_kv.second; //将待删除结点的value改为minRight的value
				delParentPos = minParent; //标记实际删除结点的父结点
				delPos = minRight; //标记实际删除的结点
				break; //删除结点有祖先结点,需更新平衡因子
			}
		}
	}
	if (delParentPos == nullptr) //delParentPos没有被修改过,说明没有找到待删除结点
	{
		return false;
	}

	//记录待删除结点及其父结点(用于后续实际删除)
	Node* del = delPos;
	Node* delP = delParentPos;

	//更新平衡因子
	while (delPos != _root) //最坏一路更新到根结点
	{
		if (delPos == delParentPos->_left) //delParentPos的左子树高度降低
		{
			delParentPos->_bf++; //delParentPos的平衡因子++
		}
		else if (delPos == delParentPos->_right) //delParentPos的右子树高度降低
		{
			delParentPos->_bf--; //delParentPos的平衡因子--
		}
		//判断是否更新结束或需要进行旋转
		if (delParentPos->_bf == 0)//需要继续往上更新平衡因子
		{
			//delParentPos树的高度变化,会影响其父结点的平衡因子,需要继续往上更新平衡因子
			delPos = delParentPos;
			delParentPos = delParentPos->_parent;
		}
		else if (delParentPos->_bf == -1 || delParentPos->_bf == 1) //更新结束
		{
			break; //delParent树的高度没有发生变化,不会影响其父结点及以上结点的平衡因子
		}
		else if (delParentPos->_bf == -2 || delParentPos->_bf == 2) //需要进行旋转(此时delParentPos树已经不平衡了)
		{
			if (delParentPos->_bf == -2)
			{
				if (delParentPos->_left->_bf == -1)
				{
					Node* tmp = delParentPos->_left; //记录delParentPos右旋转后新的根结点
					RotateR(delParentPos); //右单旋
					delParentPos = tmp; //更新根结点
				}
				else if(delParentPos->_left->_bf == 1)
				{
					Node* tmp = delParentPos->_left->_right; //记录delParentPos左右旋转后新的根结点
					RotateLR(delParentPos); //左右双旋
					delParentPos = tmp; //更新根结点
				}
				else //delParentPos->_left->_bf == 0
				{
					Node* tmp = delParentPos->_left; //记录delParentPos右旋转后新的根结点
					RotateR(delParentPos); //右单旋
					delParentPos = tmp; //更新根结点
					//平衡因子调整
					delParentPos->_bf = 1;
					delParentPos->_right->_bf = -1;
					break; //更正
				}
			}
			else //delParentPos->_bf == 2
			{
				if (delParentPos->_right->_bf == -1)
				{
					Node* tmp = delParentPos->_right->_left; //记录delParentPos右左旋转后新的根结点
					RotateRL(delParentPos); //右左双旋
					delParentPos = tmp; //更新根结点
				}
				else if(delParentPos->_right->_bf == 1)
				{
					Node* tmp = delParentPos->_right; //记录delParentPos左旋转后新的根结点
					RotateL(delParentPos); //左单旋
					delParentPos = tmp; //更新根结点
				}
				else //delParentPos->_right->_bf == 0
				{
					Node* tmp = delParentPos->_right; //记录delParentPos左旋转后新的根结点
					RotateL(delParentPos); //左单旋
					delParentPos = tmp; //更新根结点
					//平衡因子调整
					delParentPos->_bf = -1;
					delParentPos->_left->_bf = 1;
					break; //更正
				}
			}
			//delParentPos树的高度变化,会影响其父结点的平衡因子,需要继续往上更新平衡因子
			delPos = delParentPos;
			delParentPos = delParentPos->_parent;
			//break; //error
		}
		else
		{
			assert(false); //在删除前树的平衡因子就有问题
		}
	}
	//进行实际删除
	if (del->_left == nullptr) //实际删除结点的左子树为空
	{
		if (del == delP->_left) //实际删除结点是其父结点的左孩子
		{
			delP->_left = del->_right;
			if (del->_right)
				del->_right->_parent = delP;
		}
		else //实际删除结点是其父结点的右孩子
		{
			delP->_right = del->_right;
			if (del->_right)
				del->_right->_parent = delP;
		}
	}
	else //实际删除结点的右子树为空
	{
		if (del == delP->_left) //实际删除结点是其父结点的左孩子
		{
			delP->_left = del->_left;
			if (del->_left)
				del->_left->_parent = delP;
		}
		else //实际删除结点是其父结点的右孩子
		{
			delP->_right = del->_left;
			if (del->_left)
				del->_left->_parent = delP;
		}
	}
	delete del; //实际删除结点
	return true;
}

AVL树的性能

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

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

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

相关文章

springboot停车微信小程序小程序-计算机毕业设计源码92714

摘 要 在信息飞速发展的今天&#xff0c;网络已成为人们重要的信息交流平台。每天都有大量的农产品需要通过网络发布&#xff0c;为此&#xff0c;本人开发了一个基于springboot停车微信小程序小程序。 对于本停车微信小程序的设计来说&#xff0c;它主要是采用后台采用java语…

因你而变 共赴新程 | AidLux全新版本震撼发布!

历经400多个日夜&#xff0c;AidLux 2.0&#xff08;基础版&#xff09;终于要与大家见面了。 开发者们问过无数次&#xff0c;新版本何时发布&#xff0c;期待的功能何时上线……在此&#xff0c;让我先真诚地感谢大家长期以来的期待与关心&#xff01; 一年多以来&#xff…

k8s-部署对象存储minio

环境信息 minio版本 :最新 k8s 版本1.22 使用nfs作为共享存储 一.单节点安装包部署 脚本部署&#xff0c;一键部署&#xff0c;单节点应用于数据量小&#xff0c;一些缓存存储&#xff0c;比如gitlab-runner的产物数据&#xff0c;maven的打包依赖数据 #!/bin/bash# 步骤…

019、有序集合_命令

它保留了集合不能有重复,有序集合中的元素可以排序。 但是它和列表使用索引下标作为排序依据不同的是,它给每个元素设置一个分数(score)作为排序的依据。如图 该有序集合包含kris、mike、frank、tim、martin、tom,它们的分数分别是1、91、200、220、250、251,有序集合提…

【C++】STL中List的基本功能的模拟实现

前言&#xff1a;在前面学习了STL中list的使用方法&#xff0c;现在我们就进一步的讲解List的一些基本功能的模拟实现&#xff0c;这一讲博主认为是最近比较难的一个地方&#xff0c;各位一起加油。 &#x1f496; 博主CSDN主页:卫卫卫的个人主页 &#x1f49e; &#x1f449; …

紧急通知!Win10将于2025年终止支持,快来升级Win11吧!

近期&#xff0c;微软更新了官方支持页面&#xff0c;我们可以看到微软官方在2025年10月14日之后&#xff0c;不再为 Win10系统提供安全更新或技术支持。到这个时间&#xff0c;Win10用户的电脑仍可正常工作&#xff0c;但建议Win10用户都升级为Win11版本&#xff0c;接下来小编…

分水岭算法分割和霍夫变换识别图像中的硬币

首先解释一下第一种分水岭算法&#xff1a; 一、分水岭算法 分水岭算法是一种基于拓扑学的图像分割技术&#xff0c;广泛应用于图像处理和计算机视觉领域。它将图像视为一个拓扑表面&#xff0c;其中亮度值代表高度。算法的目标是通过模拟雨水从山顶流到山谷的过程&#xff0…

工具-金舟投屏软件: 手机如何投屏到电脑上 / Wi-Fi / USB

金舟安卓/iOS苹果投屏-正版软件下载中心 方法一、金舟投屏软件-wifi 1.1、准备工作 确保苹果手机和Windows电脑都连接到同一个Wi-Fi网络。 在Windows电脑上安装并打开金舟投屏软件。 1.2、操作步骤 在金舟投屏软件上选择“苹果手机投屏”功能。 在苹果手机上下滑屏幕&am…

Parallels Desktop for Mac 19.4.0更新了哪些内容?有什么改进?

带来了重新设计的共享 Mac 文件夹版本&#xff0c;这些文件夹现在是符号链接&#xff0c;像指针一样指向您的 Mac 文件夹中的文件&#xff0c;同时仍然显示在 Windows 的本地磁盘上。 修复了由于共享文件夹问题导致 NinjaTrader 无法正常启动的问题。 修复了由于共享文件夹问…

C++ AVL树 详细讲解

目录 一、AVL树的概念 二、AVL树的实现 1.AVL树节点的定义 2.AVL树的插入 3.AVL树的旋转 4.AVL树的验证 三、AVL树的性能 四、完结撒❀ 一、AVL树的概念 二叉搜索树虽可以缩短查找的效率&#xff0c;但 如果数据有序或接近有序二叉搜索树将退化为单支树&#xff0c;查 …

【多目标跟踪】《FlowMOT: 3D Multi-Object Tracking by Scene Flow Association》论文阅读笔记

0.论文 论文地址链接:https://arxiv.org/pdf/2012.07541v1 通过流的方式跟踪是一个比较新颖的点,所以这里比较关注运动跟踪,是如果做到流的跟踪来预测目标的位置以及ID绑定的。 FlowMOT的框架结构如下所示,本中会主要关注下运动跟踪、数据关联、ID分配、新生/消亡…

python基础实例

下一个更大的数 定义一个Solution类&#xff0c;用于实现next_great方法 class Solution: def next_great(self, nums1, nums2): # 初始化一个空字典answer&#xff0c;用于存储答案 answer {} # 初始化一个空列表stack&#xff0c;用于存储待比较的数字 stack [] # 遍历nu…

香橙派Orange AI Pro / 华为昇腾310芯片 部署自己训练的yolov8模型进行中国象棋识别

香橙派Orange AI Pro / 华为昇腾310芯片 部署自己训练的yolov8模型进行中国象棋识别 一、香橙派简介1.1、香橙派 AI Pro 硬件资源介绍1.2、华为昇腾310&#xff08;Ascend310&#xff09; 简介1.3、 昇腾310AI能力和CANN 简介昇腾310 NPU简介 二、远程环境配置2.1、ssh2.2、vnc…

开发一个Dapp需要多少?

区块链开发一个Dapp要多少钱&#xff1f; 开发一个去中心化应用&#xff08;Dapp&#xff09;的成本取决于多个因素&#xff0c;包括Dapp的复杂性、功能需求、区块链平台以及开发团队的经验水平。以下是一些主要的影响因素&#xff1a; 1. 区块链平台&#xff1a;不同区块链…

Python中的@staticmethod和@classmethod装饰器

名词解释 本文主要介绍静态方法staticmethod和类方法classmethod在类中的应用&#xff0c;在介绍这两个函数装饰器之前&#xff0c;先介绍类中的几个名词&#xff0c;便于后面的理解&#xff1a; 类对象&#xff1a;定义的类就是类对象 类属性&#xff1a;定义在__init__ 外…

哈希经典题目(C++)

文章目录 前言一、两数之和1.题目解析2.算法原理3.代码编写 二、判定是否互为字符重排1.题目解析2.算法原理3.代码编写 三、 字⺟异位词分组1.题目解析2.算法原理3.代码编写 总结 前言 哈希表是一个存储数据的容器&#xff0c;我们如果想要快速查找某个元素&#xff0c;就可以…

定个小目标之每天刷LeetCode热题(12)

这是一道简单题&#xff0c;使用位运算中的异或运算即可&#xff0c;异或运算有以下性质&#xff1a; 1、任何数异或 0 结果仍然是原来的数&#xff0c;即 a⊕0a 2、任何数和其自身做异或运算&#xff0c;结果是 0 所以我们只需要让数组里的所有元素进行异或运算得到的结果就…

对话Nature子刊论文作者:DiG如何揭示蛋白质秘密

编者按&#xff1a;尽管当前利用人工智能技术预测生物分子结构的模型已经可以精确预测包括蛋白质、核酸、小分子、离子和修饰残基在内的复合物结构&#xff0c;但对于科学家们来说仅了解分子的微观结构还远远不够&#xff0c;因为分子的宏观属性和功能往往取决于分子结构在平衡…

游戏盾之应用加速,何为应用加速

在数字化时代&#xff0c;用户对于应用程序的防护要求以及速度和性能要求越来越高。为了满足用户的期望并提高业务效率&#xff0c;应用加速成为了不可忽视的关键。 应用加速是新一代的智能分布式云接入系统&#xff0c;采用创新级SD-WAN跨域技术&#xff0c;针对高防机房痛点进…

ES 8的向量检索性能调优实践

前言 ES的官方实验室曾发布过一篇博客,介绍了使ES向量检索性能获得显著提升的技术要点与展望: 多线程搜索能力的利用:Lucene 的分段架构允许实现多线程搜索能力。Elasticsearch 通过同时搜索多个段来提高性能,使用所有可用的 CPU 核心的计算能力显著减少了单个搜索的延迟。…