数据结构:AVL树(C++实现)

目录

前言

1. AVL树概念

2. AVL树的实现

2.1 数据结构设计

2.2 构造,析构,赋值重载函数

2.3 插入函数

2.4 旋转处理

2.4.1 右单旋

2.4.2 左单旋

2.4.3 左右双旋

2.4.4 右左双旋

2.5 完善插入函数

2.6 AVL树的验证

2.7 测试AVL插入函数

​编辑

3. AVL树分析

AVL树的优点:

AVL树的缺点:

适用场景:

不适用场景:

总结


前言

本文延续前文二叉搜索树,开启平衡二叉搜索树的篇章。在这一篇章中,我们将重点关注AVL树,这是一种严格平衡的二叉搜索树。AVL树通过精巧的平衡机制,确保了树的高度始终保持在对数级别,从而大大提升了查找、插入和删除操作的性能。本文将详细介绍AVL树的原理、平衡调整方法以及性能分析。


1. AVL树概念

二叉树搜索树虽可以提高查找的效率,但如果数据有序或接近有序二叉搜索树退化为单支树,查找元素相当于在链表中搜索元素,时间复杂度由O(logN)级别上升到O(N)级别。

因此,为了避免这种性能退化,通常会采用自平衡二叉搜索树,如AVL树或红黑树。其中AVL树是由两位俄罗斯数学家Adelson-Velsky和Landis发明。在AVL树中,任何节点的两个子树的高度最大差别为1。一棵AVL树可以是空树,如果不是空树具有以下性质:

  • 它的左右子树都是AVL树。
  • 左右子树高度之差(简称平衡因子)的绝对值不超过1(可以是-1/0/1)。

如果一棵二叉搜索树高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在\log_{2}n,搜索时间复杂度为O(\log_{2}n)。

2. AVL树的实现

2.1 数据结构设计

AVL树结点设计跟二叉搜索树的KV模型类似,只不过key和value这两个类型合并成pair类型。

pair类将一对不同类型的的值耦合在一起,可以通过T1类型的值来访问T2类型的值。把K和V类耦合到pair里面。

 

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

	pair<K, V> _kv;
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;
	int _bf; // balance factor
};

template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
    //...
private:
    Node* _root;
}

2.2 构造,析构,赋值重载函数

  • 拷贝构造函数是按照前序创建结点。
  • 析构函数是按照后序释放结点。
template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;

public:
	AVLTree() = default;//默认构造函数

	AVLTree(const AVLTree<K, V>& t)//拷贝构造函数
	{
		_root = Copy(t._root);
	}

    //赋值重载函数
	AVLTree<K, V>&  operator=(const AVLTree<K, V> t)
	{
		swap(_root, t._root);
		return *this;
	}
    
    //析构函数
	~AVLTree()
	{
		Destroy(_root);
		_root = nullptr;
	}

private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

		_InOrder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << " ";
		_InOrder(root->_right);
	}

	void Destroy(Node* root)
	{
		if (root == nullptr)
			return;

		Destroy(root->_left);
		Destroy(root->_right);
		delete root;
	}

	Node* Copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;

		Node* newRoot = new Node(root->_kv);
		newRoot->_left = Copy(root->_left);
		newRoot->_right = Copy(root->_right);
	}
private:
    Node* _root;
}

2.3 插入函数

AVL树的平衡因子是右子树高度差减去左子树高度差。平衡因子调整后,会有如下三种情况。

  • 平衡因子为0,说明之前平衡因子为1或者-1,左右子树高度差1。此时,不需要再往上调整平衡因子。如下图所示,22结点再左子树这边插入新结点,这不是当前结点左右子树中较长的路径,算是平衡23结点的左右子树高度差,不会影响到23结点的祖先结点的左右子树的高度差。

  • 平衡因子为1或者-1,说明之前平衡因子为0,左右子树高度相同。此时,还需要继续往上调整父亲结点的平衡因子。如下图,4结点左右子树都为空,不过插入到左右孩子中的那一个,都算是较长的路径,会影响到其父亲结点。

  • 平衡因子为2或者-2时,说明之前的平衡因子为1或者-1,左右子树高度差为1,但是新增节点插入在高度较大子树上,导致高度差绝对值超过1。如下图23结点高差大于2,根据AVL树的规则,此时要进行旋转调整结点位置,降低高差。

AVL树本质也是二叉搜索树,只不过在二叉搜索树的基础上引入平衡因子。插入的步骤如下:

  • 先处理根节点为空的情况,进行特殊处理,直接让_root指向新开辟出来的结点,并返回true。
  • 按照二叉搜索树的方式插入结点,还需要知道新增节点是父亲结点的左孩子还是右孩子,故多定义一个parent指针记录cur的父亲结点。
  • 调整节点的平衡因子,按照上面分析的三种情况,先更新父亲结点的平衡因子,再判断是否继续往上更新,最多更新到根节点。如果不止这三种情况,说明平衡因子的更新有问题或者是这棵树插入操作有误,这棵树不是AVL树,直接进行断言,进行排错工作。

如果对二叉搜索树的插入操作不清楚,可以看上篇关于二叉搜索树的文章《数据结构:二叉搜索树(简单C++代码实现)》http://t.csdnimg.cn/cZjIF

#include<assert.h>

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->_parent = 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->_parent;
		}
		else if (parent->_bf == 2 || parent->_bf == -2)
		{
			//不平衡了,旋转处理
            //...
		}
        else//如果还有情况,说明出现问题,直接断言
        {
            assert(false);
        }
	}
}

2.4 旋转处理

AVL树插入新结点根二叉搜索内容类似,只不过需要维护每个结点的平衡因子。当出现某个结点的平衡因子绝对值大于1时,高度差过大,需要通过旋转降低高度差。AVL树的旋转有四种情况,下面通过图书二叉搜索树进行分析,其中的二叉搜索树可能是一个局部的子树

我们先从抽象图开始分析

2.4.1 右单旋

  • 下面的图片是一颗抽象的二叉树,代表了所有需要右单旋操作的二叉树的情况。X/Y/Z代表一颗高度为h的AVL树。新增节点插入到X二叉树下,必须让X树高度加一,这样才会出现左边高的情况。
  • 把45结点记作parent,20结点记作subL,表示为parent的左孩子结点,Y树记作subLR,表示为20结点的左孩子。
  • 首先,让subLR成为parent的左孩子结点。再让subL成为这个局部二叉树的根,让parent成为它的右孩子。通过右单旋的操作,subL和parent左右孩子高度差相同,平衡因子都为0,所以要更新平衡因子。

  • 我们在看一下具体的二叉树。当h为0,说明XYZ都是空树的情况下,此二叉树只有两个结点。当新增节点插入在7结点的左边时,出现祖先节点都为负的情况,并且有个祖先节点平衡因子绝对值大于1。进行右单旋操作,只不过7的右孩子是空指针,11结点拿到的也是空结点。

  •  当h为1时,新增结点可以插入在4结点的两边任意位置,也就是说有两种情况。旋转操作如下。

 如果h为2,那么这颗二叉树通过排列组合的数量将变多,如果h更大,具体的二叉树的数量将更多。但是我们不需要关心具体的例子,我们只需要抽象出一个代表所有情况的二叉树,就可以解决所有的情况。

 右单旋的代码如下,还需注意parent可能是根节点,或者是别人的孩子结点,需要提前记录parent的父亲结点,进行判断。

void RotateR(Node* parent)
{
    //subL是parent的左孩子
    //subLR是parent左孩子的右孩子
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

    //subLR成为parent的左孩子,如果不为空,需要修改_parent的指向
	parent->_left = subLR;
	if (subLR)
		subLR->_parent = parent;

    //提前记录parent的父亲结点
	Node* parentParent = parent->_parent;

    //parent成为subL的右孩子
	subL->_right = parent;
	parent->_parent = subL;

    //如果parent的父亲结点是为空,说明parent是根结点
	if (parentParent == nullptr)
	{
		_root = subL;
		subL->_parent = nullptr;
	}
	else
	{
        //如果parent的父亲结点不为空,还需要判断parent是其父亲结点的左孩子还是右孩子
		if (parent == parentParent->_left)
			parentParent->_left = subL;
		else
			parentParent->_right = subL;

		subL->_parent = parentParent;
	}
    //更新平衡因子
	parent->_bf = subL->_bf = 0;

}

2.4.2 左单旋

二叉搜索树是左边子树一边高,需要右单旋操作。如果需要进行左单旋操作,那么二叉搜索树就是右边子树一边高。左单旋操作跟右单旋操作类似。

  • 20结点记作parent,45结点记作subR,表示为parent的右孩子,Y树记作subRL,表示为parent左孩子的右孩子。
  • parent右指针连接subRL,subRL成为parent的右孩子。再让subR左指针连接parent,parent成为subR的左孩子。
  • 还需要判断parent的父亲结点是否为为空,如果其父亲结点为空,说明parent就是根节点,那么subR的成员变量_parent指向空;如果其父亲结点不为空,说明parent不是根节点,还有祖先,需要subR的成员变量_parent指向parent之前的父亲节点。

	void RotateL(Node* parent)
	{
        //subR是parent的右孩子
        //subRL是parent右孩子的左孩子
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

        //parent的右指针指向subRL
        //如果subRL不为空,其父亲结点需要指向parent
		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;

		//提前记录parent的父亲结点
		Node* parentParent = parent->_parent;

		subR->_left = parent;
		parent->_parent = subR;

        //如果parent的父亲结点是为空,说明parent是根结点
		if (parentParent == nullptr)
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else
		{
            //如果parent的父亲结点不为空,还需要判断parent是其父亲结点的左孩子还是右孩
			if (parent == parentParent->_left)
				parentParent->_left = subR;
			else
				parentParent->_right = subR;

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

2.4.3 左右双旋

看下图的二叉搜索树,不再全是左边高,先是11结点的左子树高,再是7结点的右子树高。

如果对平衡因子绝对值大于1的结点进行右单旋操作,会发现变成先右子树高,再左子树高。

如果再对平衡因子绝对值大于1进行左单旋操作,会发现回到之前的二叉搜索树的形状。那么我们应该怎么做呢?

  • 解决方法是,先对subL结点进行左单旋,再对parent结点进行右单旋。下面再对平衡因子进行处理。
  • 如果subL结点的平衡因子为-1,说明Y树插入新增结点,旋转完之后的根节点的左孩子平衡因子为0,右孩子平衡因子为1。
  • 如果subL结点的平衡因子为1,说明Z树插入了新增结点,旋转完之后的根节点右孩子平衡因子为0,做孩子平衡因子为-1。

  • 还有一种特殊情况是h为0。表示新增节点就是35结点,且此时35结点平衡因子为0。旋转完之后,平衡因子都为0

	void RotateLR(Node* parent)
	{
        //subL是parent的左孩子
        //subLR是parent左孩子的右孩子
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;

        //先对parent的右孩子进行左单旋
        //再对parent进行右单旋
		RotateL(parent->_left);
		RotateR(parent);

        //处理平衡因子,按照上面分析有三种情况
		if (bf == 0)
		{
			subL->_bf = 0;
			subLR->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == -1)
		{
			subL->_bf = 0;
			subLR->_bf = 0;
			parent->_bf = 1;
		}
		else if (bf == 1)
		{
			subL->_bf = -1;
			subLR->_bf = 0;
			parent->_bf = 0;
		}
		else//如果不属于这三种情况,就说平衡因子更新逻辑有问题,需要排错
		{
			assert(false);
		}
	}

2.4.4 右左双旋

右左双旋跟左右双旋类似,都是在左右两边都高的二叉树上。

  • 特殊情况是,插入35结点在45结点的左侧。旋转处理完后,平衡因子都是0。

  • 下面是一般情况,先对subR结点进行左单旋,再对parent结点进行右单旋。下面再对平衡因子进行处理。
  • 如果subR结点的平衡因子为1,说明Z树插入新增结点,旋转完之后的根节点的右孩子平衡因子为0,左孩子平衡因子为-1。
  • 如果subR结点的平衡因子为-1,说明Y树插入新增结点,旋转完之后的根节点的左孩子平衡因子为0,左孩子平衡因子为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)
		{
			subR->_bf = 0;
			subRL->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == 1)
		{
			subR->_bf = 0;
			subRL->_bf = 0;
			parent->_bf = -1;
		}
		else if (bf == -1)
		{
			subR->_bf = 1;
			subRL->_bf = 0;
			parent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

2.5 完善插入函数

加上旋转的部分,插入函数的代码才算完善。

	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->_parent = 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->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				//不平衡了,旋转处理
				if (parent->_bf == 2 && cur->_bf == 1)//左单旋
				{
					RotateL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == -1)//右单旋
				{
					RotateR(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == -1)//右左双旋
				{
					RotateRL(parent);
				}
				else//左右双旋
				{
					RotateLR(parent);
				}
				break;
			}
			else
			{
				assert(false);
			}
		}
	}

2.6 AVL树的验证

AVL树在二叉搜索树的基础上,加上了对左右子树高度差的限制。如果要验证一颗AVL树,需要分两步走:

验证其为二叉搜索树

  • 使用中序遍历,如果得到有序序列,说明是二叉搜索树。

验证其为平衡树

  • 每个节点的左右子树高度差不大于1。
  • 节点的平衡因子是否计算正确。
class AVLTree
{
public:
    int Height()
	{
		return _Height(_root);
	}

	int Size()
	{
		return _Size(_root);
	}

    bool IsBalanceTree()
	{
		return _IsBalanceTree(_root);
	}

	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

private:
    int _Size(Node* root)
	{
		if (nullptr == root)
			return 0;

		return _Size(root->_left) + _Size(root->_right) + 1;
	}
    
	int _Height(Node* root)
	{
		if (root == nullptr)
			return 0;

		int leftHeight = _Height(root->_left);
		int rightHeight = _Height(root->_right);

		return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
	}

	bool _IsBalanceTree(Node* root)
	{
		// 空树也是AVL树
		if (nullptr == root)
			return true;

		//计算root结点的平衡因子,即root左右子树的高度差
		int leftHeight = _Height(root->_left);
		int rightHeight = _Height(root->_right);
		int diff = rightHeight - leftHeight;

		// 如果计算出的平衡因子与root的平衡因子不相等
		// 或者root平衡因子的绝对值超过1,则一定不是AVL树
		//if (diff != root->_bf || (diff > 1 || diff < -1))
		if (abs(diff) > 2 || (diff > 1 || diff < -1))
			return false;
		
		return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
	}

	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

		_InOrder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << " ";
		_InOrder(root->_right);
	}
    //...
}

2.7 测试AVL插入函数

我们写一个测试函数,测试插入函数的性能和正确性。我们插入一千万个随机值到vector容器中,并使用clock函数测试插入所消耗的时间,并查看树的高度,结点个数和是否为AVL树。

void TestAVLTree()
{
	const int N = 10000000;
	vector<int> v;
	v.reserve(N);
	srand(time(0));

	for (size_t i = 0; i < N; i++)
	{
		v.push_back(rand() + i);
	}

    //用来计算程序运行时间,单位是ms
	size_t begin1 = clock();
	AVLTree<int, int> t;
	for (auto& e : v)
	{
		t.Insert({ e, e });
	}
	size_t end1 = clock();

	cout << "Insert:" << end1 - begin1 << endl;
	cout << "Height:" << t.Height() << endl;
	cout << "Size:" << t.Size() << endl;
	cout << "IsBalanceTree:" << t.IsBalanceTree() << endl;
}

运行结果如下,插入消耗了2.3秒左右,树的高度为26,大约有六百万个结点,验证过后是AVL树。

3. AVL树分析

AVL树是一种自平衡的二叉搜索树,它通过旋转操作来保持树的平衡,确保树的高度大约log(N),从而保证查找、插入和删除操作的时间复杂度为O(log N)。

AVL树的优点:

  1. 查询效率高:由于树的高度被严格控制在log(N)左右,因此查找操作非常高效。
  2. 维护有序性:作为二叉搜索树,AVL树保持了元素的有序性。

AVL树的缺点:

  1. 插入操作复杂:在插入新节点后,可能需要通过单旋转或双旋转来维持树的平衡,这增加了插入操作的复杂度。
  2. 删除操作更复杂:删除节点后,可能需要多次旋转来恢复平衡,这些旋转可能从叶子节点一直影响到根节点。

适用场景:

  • 静态数据集:如果数据集是静态的或者不经常变动,AVL树是一个很好的选择,因为一旦构建完成,它可以提供高效的查询服务。
  • 需要有序访问:当需要频繁地进行有序遍历时,AVL树也是合适的选择。

不适用场景:

  • 频繁修改:如果数据结构需要频繁的插入和删除操作,维护AVL树的平衡成本可能过高。
  • 对性能要求极高:在某些性能要求极高的场景下,即使是O(log N)的时间复杂度也可能不够,需要考虑其他数据结构,如B树或红黑树。

对于经常修改的数据结构,可以考虑以下替代方案:

  • 红黑树:它是一种近似平衡的二叉搜索树,通过限制节点的颜色和规则来维持大致平衡,虽然红黑树的平衡不如AVL树严格,但其插入和删除操作通常比AVL树更高效。
  • B树/B+树:这些树是为磁盘和其他直接访问的辅助存储设备设计的平衡树结构,它们通常用于数据库和文件系统中。


总结

通过对AVL树的学习,我们不仅深入理解了平衡二叉搜索树的原理和操作,还掌握了如何通过旋转来维护树的平衡性。这一过程加深了我们对平衡二叉树的认识。

创作不易,希望这篇文章能给你带来启发和帮助,如果喜欢这篇文章,请留下你的三连,你的支持的我最大的动力!!!

ee192b61bd234c87be9d198fb540140e.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值