017-AVL树(C++实现)

017-AVL树(C++实现)

1. AVL树的概念

二叉搜索树虽然可以缩短查找的效率,但是如果数据接近有序,二叉树将退化为单支树,查找元素相当于在链表中搜索元素,效率低下。

因此,有两位俄罗斯数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:

  • 当向二叉搜索树中插入新的节点后,如果能保证每个节点的左右子树高度之差绝对值不超过1,就可以降低树的高度,从而减少平均搜索长度,如果插入节点后有节点的左右子树高度之差绝对值超过1,则需要进行某种调整。

一颗AVL树可以是空树,或者是具有一下性质的树:

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

一颗AVL树如果有N个节点,那么它的高度可以保持在 l o g 2 N log_2N log2N,搜索的时间复杂度为O(logN)。

2. AVL树节点的定义

AVL树节点中有如下几个成员:

  • 双亲指针:指向双亲节点
  • 左孩子指针:指向左孩子
  • 右孩子指针:指向右孩子
  • 数据:存放数据
  • 平衡因子:记录以该节点为根的平衡二叉树左子树高度-右子树高度的差。
cpp 复制代码
template <typename T>
struct AVLTreeNode
{
	AVLTreeNode(const T &data): _pLeft(nullptr), _pRight(nullptr), _pParent(nullptr), _data(data), _bf(0)
	{}

	AVLTreeNode<T> *_pLeft;
	AVLTreeNode<T> *_pRight;
	AVLTreeNode<T> *_pParent;
	T _data;
	int _bf;
};

3. AVL树的插入

3.1 插入新节点的步骤

AVL树本质也是二叉搜索树,只是在此基础上引入了平衡因子,那么AVL树的插入就分为以下两步:

  1. 按照二叉树的方式插入新节点
  2. 调节节点的平衡因子,使其满足AVL树的条件

3.2 平衡因子的更新

当插入一个新节点时,该节点的平衡因子为0,因为该节点左右子树都为空,即左右子树高度均为0,那么左右子树高度差也为0。

此时需要更新的是该节点的双亲节点:

  1. 判断该节点是双亲节点的左孩子还是右孩子。
  2. 如果是左孩子,该双亲节点的左子树高度+1,所以该双亲节点的平衡因子+1。
  3. 如果是有孩子,该双亲节点的右子树高度+1,所以该双亲节点的平衡因子-1。
  4. 如果更新后该双亲节点的平衡因子由-1/1变为0,则说明以该双亲节点为根节点的二叉树高度不变,无需再向上更改,此时结束插入。
  5. 如果更新后该双亲节点的平衡因子变为0变为-1/1,则说明以该双亲节点为根节点的二叉树高度+1,需要更新该双亲节点的双亲节点,循环1~3步骤,直到最后更新的节点平衡因子不为-1/1或者最后更新的节点为根节点。
  6. 此时还有一种情况,就是平衡因子为-2/2,此时该树不满足AVL树的定义,需要进行调整。
cpp 复制代码
void _updateEquilibriumFactor(Node *child)
{
	// 更新平衡因子
	Node *parent = child->_pParent;
	if (parent->_pLeft == child) parent->_ef++;
	else parent->_ef--;
	
	// 判断平衡因子是否需要继续往上更新,如果需要,则更新
	while (parent->_ef == -1 || parent->_ef == 1)
	{
		if (parent == _root) return;
		child = parent;
		parent = parent->_pParent;
		if (parent->_pLeft == child) parent->_ef++;
		else parent->_ef--;
	}
	if (parent->_ef == 0) return;

	// 此时parent节点的平衡因子为-2/2,需要进行调整
	// TODO
}

3.3 平衡因子的调整

此时node节点的平衡因子为-2/2,那么会有以下几种情况:

  • node节点平衡因子为2,其左孩子节点平衡因子为1。

  • node节点平衡因子为-2,其右孩子节点平衡因子为-1。

  • node节点平衡因子为2,其左孩子节点平衡因子为-1。

  • node节点平衡因子为-2,其右孩子节点平衡因子为1。

接下来逐情况分析,分别需要如何解决:

  1. node节点平衡因子为2,其左孩子节点平衡因子为1:

    只需要如上图进行调整,就可以将该子树调至平衡,这个过程我们称之为右单旋。

    代码实现:

    cpp 复制代码
    void _RotateR(Node *parent)
    {
        Node *child = parent->_pLeft;
    
        // parent的左孩子更新为child的右子树,如果该右子树不为空,将其右子树的双亲指针指向parent
        parent->_pLeft = child->_pRight;
        if (parent->_pLeft) parent->_pLeft->_pParent = parent;
    
        // 将child的右孩子更新为parent,更新child的双亲指针指向parent的双亲,更新parent的双亲指针指向child
        child->_pRight = parent;
        child->_pParent = parent->_pParent;
        parent->_pParent = child;
    
        // 如果child双亲不为空,则需要更新双亲的孩子,如果为空,child为根节点,则需要更新_root
        if (child->_pParent)
        {
            if (child->_pParent->_pLeft == parent) child->_pParent->_pLeft = child;
            else child->_pParent->_pRight = child;
        }
        else _root = child;
        
        // 更新parent和child的平衡因子
    	parent->_ef = child->_ef = 0;
    }
  2. node节点平衡因子为-2,其右孩子节点平衡因子为-1:

    这种情况与上一种情况的调节方法相似,调节方向正好相反,我们称之为左单旋。

    代码实现:

    cpp 复制代码
    void _RotateL(Node *parent)
    {
        Node *child = parent->_pRight;
    
        // parent的右孩子更新为child的左子树,如果该左子树不为空,将其左子树的双亲指针指向parent
        parent->_pRight = child->_pLeft;
        if (parent->_pRight) parent->_pRight->_pParent = parent;
    
        // 将child的左孩子更新为parent,更新child的双亲指针指向parent的双亲,更新parent的双亲指针指向child
        child->_pLeft = parent;
        child->_pParent = parent->_pParent;
        parent->_pParent = child;
    
        // 如果child双亲不为空,则需要更新双亲的孩子,如果为空,child为根节点,则需要更新_root
        if (child->_pParent)
        {
            if (child->_pParent->_pLeft == parent) child->_pParent->_pLeft = child;
            else child->_pParent->_pRight = child;
        }
        else _root = child;
    
        // 更新parent和child的平衡因子
        parent->_ef = child->_ef = 0;
    
    }
  3. node节点平衡因子为2,其左孩子节点平衡因子为-1:

    这种情况要比前面两种情况复杂些,需要再往下一层找到一个节点:

    由于30节点的平衡因子为-1,所以其右子树必定存在,这里假设它的右孩子为45,此时,45节点可能是三种情况:

    • 平衡因子为0:即这个节点是最新插入的节点,h为0,a、b、c、d均为空树。
    • 平衡因子为-1:b树高度为h-1,c树高度为h。
    • 平衡因子为1:b树高度为h,c树高度为h-1。

    接下来我们需要让以30为根节点的子树进行一次左单旋,然后再对以60为根节点的子树进行一次右单旋,这个过程我们称为左右双旋:

    完成上述操作后,根据上面三种情况,我们可以分析出进行结构调整后的三个节点的平衡因子:

    • 原45节点平衡因子为0:此时三个节点的平衡因子均为0。
    • 原45节点平衡因子为-1:此时30节点的平衡因子为1,其他两个节点的平衡因子为0。
    • 原45节点平衡因子为1:此时60节点的平衡因子为-1,其他两个节点的平衡因子为0。

    代码实现:

    cpp 复制代码
    void _RotateLR(Node *parent)
    {
        // 记录左孩子节点 和 左孩子节点的右孩子节点的平衡因子,方便后续更新平衡因子
        Node *chile = parent->_pLeft;
        int ef = chile->_pRight->_ef;
        // 左单旋和右单旋直接复用前面的接口即可
        _RotateL(chile);
        _RotateR(parent);
        // 如果ef为0,什么都不用干,因为我们在左单旋函数和右单旋函数中已经将它们的平衡因子更新为0了
        // 如果ef为-1,将chile的平衡因子更新为1
        // 如果ef为1,将parent的平衡因子更新为-1
        if (ef == -1) chile->_ef = 1;
        else if (ef == 1) parent->_ef = -1;
    }
  4. node节点平衡因子为-2,其右孩子节点平衡因子为1:

    这种情况与上面的情况类似,只是正好相反,需要先进行右旋,然后进行左旋,这也被称为右左双旋:

    这里也需要分为三种情况,和上面几乎一样,只是结构正好相反,这里就不再赘述。

    代码实现:

    cpp 复制代码
    void _RotateRL(Node *parent)
    {
        // 记录左孩子节点 和 左孩子节点的右孩子节点的平衡因子,方便后续更新平衡因子
        Node *chile = parent->_pRight;
        int ef = chile->_pLeft->_ef;
        // 左单旋和右单旋直接复用前面的接口即可
        _RotateR(chile);
        _RotateL(parent);
        // 如果ef为0,什么都不用干,因为我们在左单旋函数和右单旋函数中已经将它们的平衡因子更新为0了
        // 如果ef为-1,将parent的平衡因子更新为1
        // 如果ef为1,将chile的平衡因子更新为-1
        if (ef == -1) parent->_ef = 1;
        else if (ef == 1) chile->_ef = -1;
    }

补充插入节点的函数和更新调整平衡因子函数的代码:

cpp 复制代码
void _updateEquilibriumFactor(Node *child)
{
	// 更新平衡因子
	Node *parent = child->_pParent;
	if (parent->_pLeft == child) parent->_ef++;
	else parent->_ef--;
	
	// 判断平衡因子是否需要继续往上更新,如果需要,则更新
	while (parent->_ef == -1 || parent->_ef == 1)
	{
		if (parent == _root) return;
		child = parent;
		parent = parent->_pParent;
		if (parent->_pLeft == child) parent->_ef++;
		else parent->_ef--;
	}
	if (parent->_ef == 0) return;

	// 此时parent节点的平衡因子为-2/2,需要进行调整
	if (parent->_ef == 2)
	{
		// parent节点平衡因子为2,其左孩子节点平衡因子为1:右单旋
		// parent节点平衡因子为2,其左孩子节点平衡因子为-1:左右双旋
		if (parent->_pLeft->_ef == 1) _RotateR(parent);
		else _RotateLR(parent);
	}
	else
	{
		// parent节点平衡因子为-2,其右孩子节点平衡因子为-1:左单旋
		// parent节点平衡因子为-2,其右孩子节点平衡因子为1:右左双旋
		if (parent->_pRight->_ef == -1) _RotateL(parent);
		else _RotateRL(parent);
	}
}

bool insert(const T &data)
{
	// 插入新节点
	if (_root == nullptr)
	{
		_root = new Node(data);
		return true;
	}
	Node *parent = nullptr;
	Node *cur = _root;
	while (cur)
	{
		if (cur->_data == data) return false;
		parent = cur;
		if (cur->_data > data) cur = cur->_pLeft;
		else cur = cur->_pRight;
	}
	Node *node = new Node(data);
	if (parent->_data > data) parent->_pLeft = node;
	else parent->_pRight = node;
	node->_pParent = parent;

	// 调整平衡因子
	_updateEquilibriumFactor(node);

    // _size用于记录树中节点的数量
    _size++;
    
	return true;
}

4. AVL树的验证

验证AVL树可以分两步:

  1. 验证是否满足二叉平衡树的规则:中序遍历后有序
  2. 验证是否满足每个节点的左右子树高度差绝对值是否不超过1

代码实现:

cpp 复制代码
// 中序遍历
void _InOrder(Node *root, std::vector<T> &v)
{
	if (root == nullptr) return;
	_InOrder(root->_pLeft);
	v.push_back(root->_data);
	_InOrder(root->_pRight);
}

// 判断是否是二叉平衡树
bool _isBSTree()
{
	// 借助一个vector存储中序遍历的结果
	std::vector<T> v;
	v.reserve(_size); // 预留空间,不用多次扩容,提高效率

	// 中序遍历
	_InOrder(_root, v);

	// 判断该序列是否为升序
	bool ret = true;
	for (int i = 1; i < _size; i++)
	{
		if (v[i] <= v[i - 1])
		{
			ret = false;
			break;
		}
	}
	return ret;
}

// 如果是AVL树,返回当前树高度,如果不是,返回-1
int _isAVLTree(Node* root)
{
	// 空树也是AVL树
	if (root == nullptr) return 0;

	int ltree = _isAVLTree(root->_pLeft);
	int rtree = _isAVLTree(root->_pRight);

	// 如果左子树和右子树中有不是AVL树的,则该树不是AVL树
	if (ltree == -1 || rtree == -1) return -1;

	// 计算平衡因子
	int ef = ltree - rtree;
	// 如果平衡因子与当前节点平衡因子不符,说明当前树存在逻辑错误,需要检查之前的代码是否有误
	if (ef != root->_ef) return -1;
	// 如果平衡因子绝对值大于1,则当前树不是AVL树
	if (ef < -1 || ef > 1) return -1;

	// 如果当前平衡因子绝对值小于等于1,且当前左子树和右子树都是AVL树,则该树是AVL树,树的高度为左右子树中最高的高度+1
	return std::max(ltree, rtree) + 1;
}

bool isAVLTree()
{
	return _isBSTree() && _isAVLTree(_root) != -1;
}

5. AVL树的删除

AVL树的删除,可以先按照二叉平衡树的规则来删除:

  1. 找到需要删除的节点
  2. 如果该节点是叶子节点,则直接删除。
  3. 如果该节点不是叶子节点,如果有左孩子,那么找到左子树中最大的那个节点node,将node节点的值赋给需要删除的节点,因为node节点是左子树中最大的节点,所以它一定没有右孩子,只需要将它的左孩子链接到它的双亲节点然后删除node即可。
  4. 通过该节点不是叶子节点,且没有左孩子,直接将右孩子链接到双亲节点,然后删除该节点。
  5. 需要注意的是,删除节点后,平衡可能被破坏,需要调整平衡因子,调整平衡因子也是通过上面的旋转来进行调整的。

这里就不实现了,感兴趣的可以自己尝试实现一下。

6. AVL树的性能

AVL树是一颗绝对平衡的二叉树,这样可以保证查询的高效性,查询的时间复杂度为O(logN),但是如果要对该树进行频繁插入和删除,将会非常影响效率,因为可能需要对该树进行频繁的结构调整(旋转),如果需要一种查询高效且有序的数据结构,且数据个数为静态(几乎不需要进行修改),就可以考虑AVL树,但是如果需要频繁的增删数据,就不太适合使用AVL树。

7. 完整代码

cpp 复制代码
#pragma once

#include <vector>
#include <algorithm>

template <typename T>
struct AVLTreeNode
{
	AVLTreeNode(const T &data): _pLeft(nullptr), _pRight(nullptr), _pParent(nullptr), _data(data), _ef(0)
	{}

	AVLTreeNode<T> *_pLeft;
	AVLTreeNode<T> *_pRight;
	AVLTreeNode<T> *_pParent;
	T _data;
	int _ef; // equilibrium factor:平衡因子
};

template <typename T>
struct AVLTree
{
private:
	using Node = AVLTreeNode<T>;

	void _destroyTree(Node *root)
	{
		if (root == nullptr) return;
		_destroyTree(root->_pLeft);
		_destroyTree(root->_pRight);
		delete root;
	}

	void _RotateR(Node *parent)
	{
		Node *child = parent->_pLeft;

		// parent的左孩子更新为child的右子树,如果该右子树不为空,将其右子树的双亲指针指向parent
		parent->_pLeft = child->_pRight;
		if (parent->_pLeft) parent->_pLeft->_pParent = parent;

		// 将child的右孩子更新为parent,更新child的双亲指针指向parent的双亲,更新parent的双亲指针指向child
		child->_pRight = parent;
		child->_pParent = parent->_pParent;
		parent->_pParent = child;

		// 如果child双亲不为空,则需要更新双亲的孩子,如果为空,child为根节点,则需要更新_root
		if (child->_pParent)
		{
			if (child->_pParent->_pLeft == parent) child->_pParent->_pLeft = child;
			else child->_pParent->_pRight = child;
		}
		else _root = child;

		// 更新parent和child的平衡因子
		parent->_ef = child->_ef = 0;
	}

	void _RotateL(Node *parent)
	{
		Node *child = parent->_pRight;

		// parent的右孩子更新为child的左子树,如果该左子树不为空,将其左子树的双亲指针指向parent
		parent->_pRight = child->_pLeft;
		if (parent->_pRight) parent->_pRight->_pParent = parent;

		// 将child的左孩子更新为parent,更新child的双亲指针指向parent的双亲,更新parent的双亲指针指向child
		child->_pLeft = parent;
		child->_pParent = parent->_pParent;
		parent->_pParent = child;

		// 如果child双亲不为空,则需要更新双亲的孩子,如果为空,child为根节点,则需要更新_root
		if (child->_pParent)
		{
			if (child->_pParent->_pLeft == parent) child->_pParent->_pLeft = child;
			else child->_pParent->_pRight = child;
		}
		else _root = child;

		// 更新parent和child的平衡因子
		parent->_ef = child->_ef = 0;
	}

	void _RotateLR(Node *parent)
	{
		// 记录左孩子节点 和 左孩子节点的右孩子节点的平衡因子,方便后续更新平衡因子
		Node *chile = parent->_pLeft;
		int ef = chile->_pRight->_ef;
		// 左单旋和右单旋直接复用前面的接口即可
		_RotateL(chile);
		_RotateR(parent);
		// 如果ef为0,什么都不用干,因为我们在左单旋函数和右单旋函数中已经将它们的平衡因子更新为0了
		// 如果ef为-1,将chile的平衡因子更新为1
		// 如果ef为1,将parent的平衡因子更新为-1
		if (ef == -1) chile->_ef = 1;
		else if (ef == 1) parent->_ef = -1;
	}

	void _RotateRL(Node *parent)
	{
		// 记录左孩子节点 和 左孩子节点的右孩子节点的平衡因子,方便后续更新平衡因子
		Node *chile = parent->_pRight;
		int ef = chile->_pLeft->_ef;
		// 左单旋和右单旋直接复用前面的接口即可
		_RotateR(chile);
		_RotateL(parent);
		// 如果ef为0,什么都不用干,因为我们在左单旋函数和右单旋函数中已经将它们的平衡因子更新为0了
		// 如果ef为-1,将parent的平衡因子更新为1
		// 如果ef为1,将chile的平衡因子更新为-1
		if (ef == -1) parent->_ef = 1;
		else if (ef == 1) chile->_ef = -1;
	}

	void _updateEquilibriumFactor(Node *child)
	{
		// 更新平衡因子
		Node *parent = child->_pParent;
		if (parent->_pLeft == child) parent->_ef++;
		else parent->_ef--;
		
		// 判断平衡因子是否需要继续往上更新,如果需要,则更新
		while (parent->_ef == -1 || parent->_ef == 1)
		{
			if (parent == _root) return;
			child = parent;
			parent = parent->_pParent;
			if (parent->_pLeft == child) parent->_ef++;
			else parent->_ef--;
		}
		if (parent->_ef == 0) return;
	
		// 此时parent节点的平衡因子为-2/2,需要进行调整
		if (parent->_ef == 2)
		{
			// parent节点平衡因子为2,其左孩子节点平衡因子为1:右单旋
			// parent节点平衡因子为2,其左孩子节点平衡因子为-1:左右双旋
			if (parent->_pLeft->_ef == 1) _RotateR(parent);
			else _RotateLR(parent);
		}
		else
		{
			// parent节点平衡因子为-2,其右孩子节点平衡因子为-1:左单旋
			// parent节点平衡因子为-2,其右孩子节点平衡因子为1:右左双旋
			if (parent->_pRight->_ef == -1) _RotateL(parent);
			else _RotateRL(parent);
		}
	}

	// 中序遍历
	void _InOrder(Node *root, std::vector<T> &v)
	{
		if (root == nullptr) return;
		_InOrder(root->_pLeft, v);
		v.push_back(root->_data);
		_InOrder(root->_pRight, v);
	}

	// 判断是否是二叉平衡树
	bool _isBSTree()
	{
		// 借助一个vector存储中序遍历的结果
		std::vector<T> v;
		v.reserve(_size); // 预留空间,不用多次扩容,提高效率

		// 中序遍历
		_InOrder(_root, v);

		// 判断该序列是否为升序
		bool ret = true;
		for (int i = 1; i < _size; i++)
		{
			if (v[i] <= v[i - 1])
			{
				ret = false;
				break;
			}
		}
		return ret;
	}

	// 如果是AVL树,返回当前树高度,如果不是,返回-1
	int _isAVLTree(Node *root)
	{
		// 空树也是AVL树
		if (root == nullptr) return 0;

		int ltree = _isAVLTree(root->_pLeft);
		int rtree = _isAVLTree(root->_pRight);

		// 如果左子树和右子树中有不是AVL树的,则该树不是AVL树
		if (ltree == -1 || rtree == -1) return -1;

		// 计算平衡因子
		int ef = ltree - rtree;
		// 如果平衡因子与当前节点平衡因子不符,说明当前树存在逻辑错误,需要检查之前的代码是否有误
		if (ef != root->_ef) return -1;
		// 如果平衡因子绝对值大于1,则当前树不是AVL树
		if (ef < -1 || ef > 1) return -1;

		// 如果当前平衡因子绝对值小于等于1,且当前左子树和右子树都是AVL树,则该树是AVL树,树的高度为左右子树中最高的高度+1
		return std::max(ltree, rtree) + 1;
	}
public:
	AVLTree(): _root(nullptr), _size(0)
	{}

	~AVLTree()
	{
		_destroyTree(_root);
	}

	bool insert(const T &data)
	{
		// 插入新节点
		if (_root == nullptr)
		{
			_root = new Node(data);
			return true;
		}
		Node *parent = nullptr;
		Node *cur = _root;
		while (cur)
		{
			if (cur->_data == data) return false;
			parent = cur;
			if (cur->_data > data) cur = cur->_pLeft;
			else cur = cur->_pRight;
		}
		Node *node = new Node(data);
		if (parent->_data > data) parent->_pLeft = node;
		else parent->_pRight = node;
		node->_pParent = parent;

		// 调整平衡因子
		_updateEquilibriumFactor(node);

		_size++;

		return true;
	}

	bool isAVLTree()
	{
		return _isBSTree() && _isAVLTree(_root) != -1;
	}
private:
	Node *_root; // 指向树的根节点
	size_t _size; // 记录树中由多少节点
};
相关推荐
数智工坊1 小时前
【数据结构-队列】3.2 队列的顺序-链式实现-双端队列
数据结构
你真是饿了2 小时前
1.C++入门基础
开发语言·c++
天天进步20152 小时前
Python全栈项目:实时数据处理平台
开发语言·python
Tipriest_2 小时前
Python中is关键字详细说明,比较的是地址还是值
开发语言·python
sheji34162 小时前
【开题答辩全过程】以 基于Python的餐饮统计系统的设计和实 现为例,包含答辩的问题和答案
开发语言·python
elseif1232 小时前
【C++】并查集&家谱树
开发语言·数据结构·c++·算法·图论
catchadmin2 小时前
2026 年 PHP 前后端分离后台管理系统推荐 企业级方案
开发语言·php
凯子坚持 c2 小时前
C++基于微服务脚手架的视频点播系统---客户端(4)
数据库·c++·微服务
LGL6030A2 小时前
Java学习历程26——线程安全
java·开发语言·学习