数据结构之< AVLTree >

目录

  • 前言
  • 1、AVLTree的概念
    • 1.2、AVLTree节点的定义
    • 1.2、AVLTree的插入
    • 1.3、AVLTree的旋转
    • 1.4、AVLTree的删除(了解)
    • 1.5、AVLTree的性能

前言

本篇文章进行数据结构中AVL树的学习!!!

1、AVLTree的概念

AVL树的由来:

  • 前面讲了二叉搜索树的不足之处,它虽然可以缩短搜索效率,但是最坏情况会退化成单支树。所以就有了二叉平衡搜索树(AVLTree),它解决了这个弊端,让树保持平衡

历史:

  • 俄罗斯的数学家 G.M.Adelson-Velskii 和 E.M.Landis 在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度

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

  1. 它的左右子树都是AVL树
  2. 左右子树高度之差(简称“平衡因子”)的绝对值不超过2/-2(可以为0/1/-1)
    数据结构之< AVLTree >_第1张图片

注意:

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

1.2、AVLTree节点的定义

template <typename K, typename V>
struct AVLTNode
{
	AVLTNode(const pair<K, V>& _kv)
		: kv(_kv)
		, left(nullptr)
		, right(nullptr)
		, parent(nullptr)
		, _bf(0)
	{}

	pair<K, V> kv; // 存储节点值 -- first and second
	AVLTNode<K, V>* left;
	AVLTNode<K, V>* right;
	AVLTNode<K, V>* parent;
	int _bf; // 平衡因子,控制树的平衡
};

注意:

  1. 这里的AVLTree节点存储的值是"键值对"
  2. AVLTree是通过三叉链来控制的,二叉链也可以,就是麻烦了一些
  3. _bf是每个节点的平衡因子,用来控制树的平衡

1.2、AVLTree的插入

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

  1. 按照二叉搜索树的方式插入新节点(升序:值比根小插入左边,反之右边)
  2. 调整节点的平衡因子

  • 按搜索树规则插入,插入后链接双亲节点(parent)
	bool insert(const pair<K, V>& kv)
	{
		if (root == nullptr) {
			root = new Node(kv);
			root->_bf = 0;
			return true;
		}
		Node* cur = root;
		Node* parent = nullptr;

		// 按二叉搜索树的特点进行插入
		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;
			}
		}
		Node* newNode = new Node(kv);
		cur = newNode; // 更新cur,待链接父节点
		if (parent->kv.first > kv.first) {
			parent->left = newNode;
		}
		else {
			parent->right = newNode;
		}
		cur->parent = parent;
	}

注意:按二叉搜索树规则插入,AVLTree为三叉链,链接新节点需要更新双亲节点

数据结构之< AVLTree >_第2张图片


  • 插入后调整平衡因子,平衡因子可以是0/1/-1,如果为2/-2需要旋转调整
while (parent) // 一直向上回溯调整到祖先结束
{
	// 如果插入节点在左边,则双亲节点平衡因子减1,右边加1
	if (cur == parent->left) {
		--parent->_bf;
	}
	else {
		++parent->_bf;
	}

	// 如果双亲节点平衡因子为0,则不需要调整 -- 1 or -1 > 0 -- 插入节点填补了矮的节点(插入左边,且右边有节点)
	if (parent->_bf == 0) {
		break;
	}

	// 如果父节点平衡因子为1/-1,则继续调整 -- 0 > 1 or -1插入节点导致双亲节点变更或变矮(插入左边,且右边无节点)
	else if (parent->_bf == 1 || parent->_bf == -1) 
	{
		cur = cur->parent;
		parent = parent->parent;
	}
	// 后面讲旋转会处理
	else if (parent->_bf == 2 || parent->_bf == -2) 
	{}
}

注意:

  • 如果cur插入到parent的左侧,只需给parent的平衡因子-1即可
  • 如果cur插入到parent的右侧,只需给parent的平衡因子+1即可
  • 平衡因子为1/-1时,需要更新cur和parent继续向上调整平衡因子
  • 平衡因子为0时,说明这棵树已平衡,跳出循环
    数据结构之< AVLTree >_第3张图片

1.3、AVLTree的旋转

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

  1. 新节点插入较高右子树的右侧 — 右右:左单旋(根平衡因子为2,右子树为1)
    数据结构之< AVLTree >_第4张图片

重点:展开抽象图的几种情况

数据结构之< AVLTree >_第5张图片

	// 左单旋 -- 新节点插入较高右子树的右侧 -- 右右
	void RotateL(Node* parent)
	{
		Node* subR = parent->right;
		Node* subRL = subR->left;

		parent->right = subRL;
		if (subRL != nullptr) {
			subRL->parent = parent; // 如果左节点为空,则会造成空指针访问
		}
		Node* PpNode = parent->parent; // 后面parent不为根节点时,需要保存parent01的双亲节点进行链接

		subR->left = parent;
		parent->parent = subR;

		// 一开始根节点可能是parent,旋转后要进行处理 -- 根应改为subR,根的双亲节点为nullptr
		if (parent == root) {
			root = subR;
			root->parent = nullptr;
		}
		else {
			// parent可能为整颗树的局部子树 -- 旋转后树的结构就乱了,需要将parent的双亲节点指向正确的位置
			if (parent == PpNode->left) {
				PpNode->left = subR;
			}
			else {
				PpNode->right = subR;
			}
			subR->parent = PpNode;
		}

		// 更新平衡因子
		parent->_bf = 0;
		subR->_bf = 0;
	}

注意:

  • 旋转时,需要注意双亲节点要重新调整,很多人会忘记
  • 旋转后,subRL的双亲节点需要链接parent,subRL可能为空,需要判空
  • 如果根为旋转节点,旋转后subR为根,需要更新root和它的双亲
  • 如果为局部子树,需要保存parent的父节点来链接subR,因为旋转后无法找到它
    数据结构之< AVLTree >_第6张图片

  1. 新节点插入较高左子树的左侧—左左:右单旋(根节点平衡因子为-2,左子树为-1)数据结构之< AVLTree >_第7张图片
    重点:展开抽象图的几种情况

数据结构之< AVLTree >_第8张图片

	// 右单旋 -- 新节点插入较高左子树的左侧 -- 左左
	void RotateR(Node* parent)
	{
		// 旋转方法与左单旋一致
		Node* subL = parent->left;
		Node* subLR = subL->right;

		parent->left = subLR;
		if(subLR != nullptr)
			subLR->parent = parent;

		Node* PpNode = parent->parent;

		subL->right = parent;
		parent->parent = subL;

		if (parent == root)
		{
			root = subL;
			root->parent = nullptr;
		}
		else
		{
			if (PpNode->left == parent) {
				PpNode->left = subL;
			}
			else {
				PpNode->right = subL;
			}
			subL->parent = PpNode;
		}

		parent->_bf = 0;
		subL->_bf = 0;
	}

注意:右旋与左旋差不多这里就不画图了,参考左旋

  • parent的左节点链接subLR
  • subL的右子树链接parent,其余细节问题根左旋一样

  1. 新节点插入较高左子树的右侧 — 左右:先左单旋再右单旋

数据结构之< AVLTree >_第9张图片
重点:展开抽象图的几种情况

数据结构之< AVLTree >_第10张图片

	// 左右双旋 -- 左子树高,左子树的右子树矮 subL: 1 subLR: -1
	void RotateLR(Node* parent)
	{
		// 记录平衡因子和节点,待更新
		Node* subL = parent->left;
		Node* subLR = subL->right;
		int bf = subLR->_bf;

		// 先subL为根进行旋转,然后通过parent为根进行右旋
		RotateL(parent->left);
		RotateR(parent);

		// 分三种情况,画图分析
		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 if (bf == -1) {
			parent->_bf = 1;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else {
			assert(false);
		}
	}

注意:

  • 将双旋变成单旋后再旋转,即:先对30进行左单旋,然后再对90进行右单旋,旋转完成后再考虑平衡因子的更新
  • 双旋的难点不是旋转,而是更新平衡因子,不画图容易出错

数据结构之< AVLTree >_第11张图片


  1. 新节点插入较高右子树的左侧 — 右左:先右单旋再左单旋
  • 下面直接列举旋转后更新平衡因子的三种情况:(h == 0)的就不画了
    数据结构之< AVLTree >_第12张图片
	// 右左双旋 -- subR: 1 subRL: -1
	void RotateRL(Node* parent)
	{
		// 记录平衡因子和节点,待更新
		Node * subR = parent->right;
		Node* subRL = subR->left;
		int bf = subRL->_bf;

		RotateR(parent->right);
		RotateL(parent);

		// 画图分析 -- 三种情况
		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 if (bf == -1)
		{
			parent->_bf = 0;
			subR->_bf = 1;
			subRL->_bf = 0;
		}
		else {
			assert(false);
		}
	}
  • 总结:

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

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

旋转完成后,原parent为根的子树个高度降低,已经平衡,不需要再向上更新


1.4、AVLTree的删除(了解)

  • 因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不
    错与删除不同的时,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。

  • 具体实现可参考《算法导论》或《数据结构-用面向对象方法与C++描述》殷人昆版


1.5、AVLTree的性能

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

你可能感兴趣的:(C++和高阶数据结构,数据结构,算法,b树)