C++ AVL树

目录

​编辑

0.前言

1.AVL树的概念

1.1 平衡因子

1.2 AVL树的性质

2.AVL树节点的定义

3.AVL树的插入

4.AVL树的旋转

4.1 左单旋(LL旋转)

4.2 右单旋(RR旋转)

4.3 右左旋(RL旋转)

4.4 左右旋(LR旋转)

5.AVL树的验证

6.AVL树的删除

7.AVL树的性能

7.1时间复杂度

7.1.1 查找操作

7.1.2 插入操作

7.1.3.删除操作

7.2空间复杂度

7.2.1存储空间

7.2.2递归调用栈空间

8.完整代码

9.小结


(图像由AI生成) 

0.前言

在计算机科学中,二叉搜索树是一种常用的数据结构,主要用于快速查找、插入和删除操作。然而,当插入的数据是有序或接近有序时,普通的二叉搜索树可能会退化成链表,从而降低操作效率。为了解决这一问题,1962年,两位俄罗斯数学家G.M. Adelson-Velskii和E.M. Landis发明了AVL树。AVL树是一种自平衡的二叉搜索树,通过对节点的高度进行调整,保证了查找、插入和删除操作的高效性。本文将详细介绍AVL树的概念、节点定义、插入、旋转、验证、删除以及其性能。

1.AVL树的概念

AVL树是一种自平衡的二叉搜索树,由G.M. Adelson-Velskii和E.M. Landis在1962年发明。其主要目的是解决普通二叉搜索树在插入数据有序或接近有序时可能退化成链表的问题。AVL树通过在每次插入或删除节点后自动调整树的结构,使得任何节点的左右子树高度之差(平衡因子)的绝对值不超过1,从而保证了树的高度尽可能小,进而提高查找、插入和删除操作的效率。

1.1 平衡因子

平衡因子(Balance Factor, BF)是AVL树中每个节点的一个重要属性,用于衡量节点的左右子树的高度差。具体定义如下:

BF=右子树的高度−左子树的高度BF=右子树的高度−左子树的高度

对于AVL树中的每个节点,其平衡因子的取值只能是-1、0或1:

  • 平衡因子为0:表示节点的左子树和右子树高度相同。
  • 平衡因子为1:表示节点的右子树高度比左子树高1。
  • 平衡因子为-1:表示节点的左子树高度比右子树高1。

1.2 AVL树的性质

一棵AVL树具有以下性质:

  1. 二叉搜索树性质:对于每个节点,其左子树的所有节点值都小于该节点的值,右子树的所有节点值都大于该节点的值。
  2. 自平衡性质:对于每个节点,其左右子树的高度差(即平衡因子)的绝对值不超过1。具体来说,一棵AVL树要么是空树,要么是具有以下性质的二叉搜索树:
    • 它的左右子树都是AVL树。
    • 左右子树高度之差的绝对值不超过1。

通过保持上述性质,AVL树在最坏情况下的高度也能控制在 O(logn) 级别,从而确保了查找、插入和删除操作的时间复杂度也维持在 O(logn)。

2.AVL树节点的定义

AVL树的每个节点包含存储数据的字段以及指向其子节点和父节点的指针。除此之外,每个节点还有一个平衡因子,用于记录右子树和左子树的高度差,以便在需要时进行旋转操作来维持AVL树的平衡。

以下是AVL树节点的C++模板定义:

template <class T>
struct AVLTreeNode {
    T _data;  // 节点存储的数据
    AVLTreeNode<T>* _left;  // 指向左子节点的指针
    AVLTreeNode<T>* _right;  // 指向右子节点的指针
    AVLTreeNode<T>* _parent;  // 指向父节点的指针
    int _bf;  // 平衡因子,右子树高度减去左子树高度

    // 构造函数,初始化节点数据及指针
    AVLTreeNode(const T& x = T())
        : _data(x), _left(NULL), _right(NULL), _parent(NULL), _bf(0) {}
};

节点属性说明

  • _data:节点存储的数据,可以是任何类型。
  • _left:指向左子节点的指针。如果左子节点不存在,则为NULL。
  • _right:指向右子节点的指针。如果右子节点不存在,则为NULL。
  • _parent:指向父节点的指针。如果节点是根节点,则为NULL。
  • _bf:平衡因子,表示右子树的高度减去左子树的高度。平衡因子用于AVL树的平衡维护,其值只能为-1、0或1。

3.AVL树的插入

AVL树是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。AVL树的插入过程可以分为两步:1. 按照二叉搜索树的方式插入新节点;2. 调整节点的平衡因子。

1. 按照二叉搜索树的方式插入新节点

首先,按照二叉搜索树的规则找到新节点应插入的位置,并将其插入树中。具体过程如下:

  • 从根节点开始,比较新节点的值与当前节点的值。
  • 如果新节点的值小于当前节点的值,则移动到左子节点;否则,移动到右子节点。
  • 重复上述步骤,直到找到一个空位置,将新节点插入该位置。

2. 调整节点的平衡因子

新节点插入后,需要调整路径上所有节点的平衡因子。具体过程如下:

  1. 如果新节点插入到父节点的左侧,只需将父节点的平衡因子减1。
  2. 如果新节点插入到父节点的右侧,只需将父节点的平衡因子加1。

此时,父节点的平衡因子可能有三种情况:0,±1,±2。

  • 平衡因子为0:说明插入之前父节点的平衡因子为±1,插入后被调整成0,此时满足AVL树的性质,插入成功。
  • 平衡因子为±1:说明插入前父节点的平衡因子一定为0,插入后被更新成±1,此时以父节点为根的树的高度增加,需要继续向上更新。
  • 平衡因子为±2:则父节点的平衡因子违反平衡树的性质,需要对其进行旋转处理(具体旋转处理代码在此省略)。

以下是AVL树插入操作的伪代码,省略了具体的旋转处理部分:

template <class T>
class AVLTree {
public:
    bool Insert(const T& data) {
        if (_root == nullptr) {
            _root = new AVLTreeNode<T>(data);
            return true;
        }

        AVLTreeNode<T>* pParent = nullptr;
        AVLTreeNode<T>* pCur = _root;

        // 按照二叉搜索树的方式找到插入位置
        while (pCur) {
            pParent = pCur;
            if (data < pCur->_data) {
                pCur = pCur->_left;
            } else if (data > pCur->_data) {
                pCur = pCur->_right;
            } else {
                return false; // 树中已存在相同的值
            }
        }

        // 创建新节点并插入
        pCur = new AVLTreeNode<T>(data);
        if (data < pParent->_data) {
            pParent->_left = pCur;
        } else {
            pParent->_right = pCur;
        }
        pCur->_parent = pParent;

        // 调整平衡因子
        while (pParent) {
            if (pCur == pParent->_left) {
                pParent->_bf -= 1;
            } else {
                pParent->_bf += 1;
            }

            if (pParent->_bf == 0) {
                break; // 插入前平衡因子为±1,插入后被调整成0
            } else if (pParent->_bf == 1 || pParent->_bf == -1) {
                pCur = pParent;
                pParent = pParent->_parent;
            } else {
                // 需要旋转处理
                // 省略具体的旋转处理代码
                break;
            }
        }

        return true;
    }

private:
    AVLTreeNode<T>* _root = nullptr;
};

在上面的伪代码中,Insert 函数首先按照二叉搜索树的规则找到插入位置,并将新节点插入树中。接着,通过调整路径上所有节点的平衡因子,维护AVL树的平衡性。如果发现某个节点的平衡因子为±2,则需要进行旋转处理以恢复平衡(具体旋转处理代码在此省略)。 

4.AVL树的旋转

在AVL树中,为了保持平衡性,需要在插入或删除节点后进行旋转操作。旋转的目的是调整节点的高度和结构,使得每个节点的平衡因子在[-1, 1]之间。下面详细介绍左单旋、右单旋、右左旋、左右旋的原理和实现,并通过复杂的例子进行说明。

4.1 左单旋(LL旋转)

左单旋用于修正因为在右子树中插入节点导致的不平衡情况。

插入之前的树:

    A
     \
      B
       \
        C

插入节点D导致A的平衡因子为2,需要进行左单旋。

旋转后的树:

    B
   / \
  A   C

 下面是左单旋的代码实现:

void RotateL(pNode parent) {
    pNode subR = parent->_right;
    pNode subRL = subR->_left;

    parent->_right = subRL;
    if (subRL)
        subRL->_parent = parent;

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

    subR->_parent = parentParent;
    if (parentParent == nullptr)
        _root = subR;
    else {
        if (parentParent->_left == parent)
            parentParent->_left = subR;
        else
            parentParent->_right = subR;
    }
    parent->_bf = subR->_bf = 0;
}

4.2 右单旋(RR旋转)

右单旋用于修正因为在左子树中插入节点导致的不平衡情况。

插入之前的树:

      C
     /
    B
   /
  A

插入节点A导致C的平衡因子为-2,需要进行右单旋。

旋转后的树:

    B
   / \
  A   C

  

下面是右单旋的代码实现:

void RotateR(pNode parent) {
    pNode subL = parent->_left;
    pNode subLR = subL->_right;

    parent->_left = subLR;
    if (subLR)
        subLR->_parent = parent;

    subL->_right = parent;
    pNode parentParent = parent->_parent;
    parent->_parent = subL;

    subL->_parent = parentParent;
    if (parentParent == nullptr)
        _root = subL;
    else {
        if (parentParent->_left == parent)
            parentParent->_left = subL;
        else
            parentParent->_right = subL;
    }
    parent->_bf = subL->_bf = 0;
}

4.3 右左旋(RL旋转)

右左旋用于修正因为在右子树的左子树中插入节点导致的不平衡情况。

插入之前的树:

    A
     \
      C
     /
    B

插入节点B导致A的平衡因子为2,且C的平衡因子为-1,需要进行右左旋。

旋转后的树:

    B
   / \
  A   C

 

下面是右左双旋的代码实现:

void RotateRL(pNode parent) {
    pNode subR = parent->_right;
    pNode subRL = subR->_left;

    int bf = subRL->_bf;
    RotateR(parent->_right);
    RotateL(parent);
    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 { // bf == 0
        parent->_bf = subR->_bf = subRL->_bf = 0;
    }
}

4.4 左右旋(LR旋转)

左右旋用于修正因为在左子树的右子树中插入节点导致的不平衡情况。

插入之前的树:

    C
   /
  A
   \
    B

插入节点B导致C的平衡因子为-2,且A的平衡因子为1,需要进行左右旋。

旋转后的树:

    B
   / \
  A   C

 下面是左右双旋的代码实现:

void RotateLR(pNode parent) {
    pNode subL = parent->_left;
    pNode subLR = subL->_right;

    int bf = subLR->_bf;
    RotateL(parent->_left);
    RotateR(parent);
    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 { // bf == 0
        parent->_bf = subL->_bf = subLR->_bf = 0;
    }
}

5.AVL树的验证

在AVL树中,验证树的平衡性是确保其高效操作的关键步骤。验证的目的是检查每个节点的平衡因子是否在[-1, 1]之间,并且树的高度是否符合平衡二叉搜索树的性质。通过验证,可以确认树在插入和删除操作后仍然保持平衡。

验证的步骤

  1. 遍历整棵树:使用递归或迭代的方法遍历树的所有节点。
  2. 计算平衡因子:对于每个节点,计算其左子树和右子树的高度差,得到平衡因子。
  3. 检查平衡因子:验证每个节点的平衡因子是否在[-1, 1]之间。
  4. 验证递归结果:对于每个子树,递归地检查其平衡性。

验证的伪代码

以下是验证AVL树的平衡性的伪代码:

template <class T>
class AVLTree {
public:
    bool IsBalanced() {
        return IsBalanced(_root);
    }

private:
    struct AVLTreeNode {
        T _data;
        AVLTreeNode* _left;
        AVLTreeNode* _right;
        AVLTreeNode* _parent;
        int _bf; // balance factor

        AVLTreeNode(const T& x = T())
            : _data(x), _left(nullptr), _right(nullptr), _parent(nullptr), _bf(0) {}
    };

    AVLTreeNode* _root = nullptr;

    bool IsBalanced(AVLTreeNode* node) {
        if (node == nullptr)
            return true;

        int leftHeight = GetHeight(node->_left);
        int rightHeight = GetHeight(node->_right);
        int balanceFactor = rightHeight - leftHeight;

        if (balanceFactor < -1 || balanceFactor > 1)
            return false;

        return IsBalanced(node->_left) && IsBalanced(node->_right);
    }

    int GetHeight(AVLTreeNode* node) {
        if (node == nullptr)
            return 0;
        return 1 + std::max(GetHeight(node->_left), GetHeight(node->_right));
    }
};

详细解释

  1. IsBalanced() 方法:这是验证树是否平衡的入口方法。它调用私有方法 IsBalanced(AVLTreeNode* node) 对根节点进行验证。
  2. IsBalanced(AVLTreeNode node) 方法*:这是递归验证每个节点是否平衡的方法。
    • 如果节点为空,返回 true
    • 计算节点的左子树和右子树的高度。
    • 计算平衡因子 balanceFactor
    • 如果平衡因子超出 [-1, 1],返回 false
    • 递归验证左右子树。
  3. GetHeight(AVLTreeNode node) 方法*:这是计算节点高度的辅助方法。
    • 如果节点为空,返回 0。
    • 递归计算左右子树的高度,并返回较大者加 1。

6.AVL树的删除

AVL树的删除操作相较于普通二叉搜索树的删除,需要额外考虑树的平衡性问题。删除节点后,必须检查并调整树的平衡,以确保AVL树的性质不被破坏。

AVL树的删除过程可以分为三个主要步骤:

  1. 找到要删除的节点:按照二叉搜索树的删除规则,找到并删除指定节点。
  2. 更新平衡因子:删除节点后,沿路径向上更新所有相关节点的平衡因子。
  3. 旋转调整:如果某个节点的平衡因子超出[-1, 1]范围,则需要通过旋转操作来恢复树的平衡。

删除节点的详细步骤

  1. 找到并删除节点

    • 如果要删除的节点没有子节点,直接删除该节点。
    • 如果要删除的节点只有一个子节点,用该子节点替代被删除节点。
    • 如果要删除的节点有两个子节点,用其右子树的最小节点(或左子树的最大节点)替代被删除节点,并递归删除该最小节点(或最大节点)。
  2. 更新平衡因子

    • 从删除节点的位置向上,逐层更新节点的平衡因子。
    • 如果某个节点的平衡因子变为±2,则需要进行旋转操作来恢复平衡。
  3. 旋转调整

    • 根据节点的平衡因子值和子树的平衡因子,选择合适的旋转操作进行调整。
    • 旋转操作包括单旋转(左旋和右旋)和双旋转(左右旋和右左旋)。

删除操作的伪代码

以下是AVL树删除操作的伪代码:

template <class T>
class AVLTree {
public:
    bool Delete(const T& data) {
        return Delete(_root, data);
    }

private:
    AVLTreeNode* _root = nullptr;

    bool Delete(AVLTreeNode*& root, const T& data) {
        if (root == nullptr)
            return false;

        if (data < root->_data) {
            if (!Delete(root->_left, data))
                return false;
        } else if (data > root->_data) {
            if (!Delete(root->_right, data))
                return false;
        } else {
            if (root->_left == nullptr || root->_right == nullptr) {
                AVLTreeNode* temp = root;
                root = (root->_left) ? root->_left : root->_right;
                if (root)
                    root->_parent = temp->_parent;
                delete temp;
            } else {
                AVLTreeNode* minNode = FindMin(root->_right);
                root->_data = minNode->_data;
                Delete(root->_right, minNode->_data);
            }
        }

        // 更新平衡因子并进行旋转调整
        if (root == nullptr)
            return true;

        UpdateBalanceFactor(root);
        return Balance(root);
    }

    AVLTreeNode* FindMin(AVLTreeNode* node) {
        while (node->_left)
            node = node->_left;
        return node;
    }

    void UpdateBalanceFactor(AVLTreeNode* node) {
        int leftHeight = GetHeight(node->_left);
        int rightHeight = GetHeight(node->_right);
        node->_bf = rightHeight - leftHeight;
    }

    int GetHeight(AVLTreeNode* node) {
        if (node == nullptr)
            return 0;
        return 1 + std::max(GetHeight(node->_left), GetHeight(node->_right));
    }

    bool Balance(AVLTreeNode*& node) {
        if (node->_bf == -2) {
            if (node->_left->_bf <= 0)
                RotateR(node);
            else
                RotateLR(node);
        } else if (node->_bf == 2) {
            if (node->_right->_bf >= 0)
                RotateL(node);
            else
                RotateRL(node);
        }

        return true;
    }

};

7.AVL树的性能

AVL树是一种自平衡的二叉搜索树,其主要特点是保持树的高度尽可能低,从而保证查找、插入和删除操作的高效性。下面将详细介绍AVL树在时间复杂度和空间复杂度方面的性能。

7.1时间复杂度

7.1.1 查找操作

在AVL树中,查找操作的时间复杂度与树的高度密切相关。由于AVL树始终保持平衡,其高度始终在 O(logn) 级别,因此查找操作的时间复杂度为:O(logn)

7.1.2 插入操作

插入操作包括按照二叉搜索树的规则插入节点和调整树的平衡。具体步骤如下:

  1. 按照二叉搜索树的规则找到插入位置并插入节点,时间复杂度为 O(logn)。
  2. 插入后,从插入点向上回溯,更新平衡因子并进行必要的旋转操作。每次旋转操作的时间复杂度为常数 O(1),最多进行 O(logn) 次旋转。

因此,插入操作的总体时间复杂度为:O(logn)

7.1.3.删除操作

删除操作包括按照二叉搜索树的规则删除节点和调整树的平衡。具体步骤如下:

  1. 按照二叉搜索树的规则找到并删除节点,时间复杂度为 O(logn)。
  2. 删除后,从删除点向上回溯,更新平衡因子并进行必要的旋转操作。每次旋转操作的时间复杂度为常数 O(1),最多进行O(logn) 次旋转。

因此,删除操作的总体时间复杂度为:O(logn)

7.2空间复杂度

7.2.1存储空间

AVL树的节点结构与普通二叉搜索树类似,但每个节点增加了一个平衡因子(或高度信息)。因此,AVL树的空间复杂度主要由节点数量决定,为:O(n)

7.2.2递归调用栈空间

在插入、删除和查找操作中,递归调用栈的最大深度等于树的高度。由于AVL树的高度始终在 O(logn) 级别,因此递归调用栈的空间复杂度为:O(logn)

8.完整代码

#include <iostream>
using namespace std;

template <class T>
struct AVLTreeNode
{
	T _data;
	AVLTreeNode<T>* _left;
	AVLTreeNode<T>* _right;
	AVLTreeNode<T>* _parent;
	int _bf;//balance factor

	AVLTreeNode(const T& x=T())
		:_data(x)
		, _left(NULL)
		, _right(NULL)
		, _parent(NULL)
		, _bf(0)
	{}
};

template <class T>
class AVLTree
{
	typedef AVLTreeNode<T> Node;
	typedef Node* pNode;
private:
	pNode _root;
public:
	AVLTree() :_root(nullptr) {}
	~AVLTree()
	{
		Destroy();
	}
	void RotateL(pNode parent)
	{
		pNode subR = parent->_right;
		pNode subRL = subR->_left;

		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;

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

		subR->_parent = parentParent;
		if (parentParent == nullptr)
			_root = subR;
		else
		{
			if (parentParent->_left == parent)
				parentParent->_left = subR;
			else
				parentParent->_right = subR;
		}
		parent->_bf = subR->_bf = 0;
	}

	void RotateR(pNode parent)
	{
		pNode subL = parent->_left;
		pNode subLR = subL->_right;

		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;

		subL->_right = parent;
		pNode parentParent = parent->_parent;
		parent->_parent = subL;

		subL->_parent = parentParent;
		if (parentParent == nullptr)
			_root = subL;
		else
		{
			if (parentParent->_left == parent)
				parentParent->_left = subL;
			else
				parentParent->_right = subL;
		}
		parent->_bf = subL->_bf = 0;
	}

	void RotateRL(pNode parent)
	{
		pNode subR = parent->_right;
		pNode subRL = subR->_left;

		int bf = subRL->_bf;
		RotateR(parent->_right);
		RotateL(parent);
		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 //bf == 0
		{
			parent->_bf = subR->_bf = subRL->_bf = 0;
		}
	}
	void RotateLR(pNode parent)
	{
		pNode subL = parent->_left;
		pNode subLR = subL->_right;

		int bf = subLR->_bf;
		RotateL(parent->_left);
		RotateR(parent);
		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 //bf == 0
		{
			parent->_bf = subL->_bf = subLR->_bf = 0;
		}
	}
	void _Destroy(pNode node) {
		if (node) {
			_Destroy(node->_left);
			_Destroy(node->_right);
			delete node;
		}
	}

	void Destroy() {
		_Destroy(_root);
		_root = nullptr;
	}

	bool Insert(const T& data)
	{
		if (_root == nullptr)
		{
			_root = new Node(data);
			return true;
		}
		pNode parent = nullptr;
		pNode cur = _root;
		while (cur)
		{
			if (cur->_data == data)
				return false;
			parent = cur;
			if (cur->_data > data)
				cur = cur->_left;
			else
				cur = cur->_right;
		}
		cur = new Node(data);
		if (parent->_data > data)
			parent->_left = cur;
		else
			parent->_right = cur;
		cur->_parent = parent;

		//更新平衡因子
		while (parent)
		{
			if (parent->_left == cur)
				parent->_bf--;
			else
				parent->_bf++;
			if (parent->_bf == 0)
				break;
			if (parent->_bf == 1 || parent->_bf == -1)
			{
				cur = parent;
				parent = parent->_parent;
			}
			else
			{
				if (parent->_bf == 2)
				{
					if (cur->_bf == 1)
						RotateL(parent);
					else
						RotateRL(parent);
				}
				else
				{
					if (cur->_bf == -1)
						RotateR(parent);
					else
						RotateLR(parent);
				}
				break;
			}
		}
		return true;
	}
	pNode Find(const T& data)
	{
		pNode cur = _root;
		while (cur)
		{
			if (cur->_data == data)
				return cur;
			else if (cur->_data > data)
				cur = cur->_left;
			else
				cur = cur->_right;
		}
		return nullptr;
	}

	void _InOrder(pNode p)const//从根节点开始中序遍历
	{
		if (p)
		{
			_InOrder(p->_left);
			cout << p->_data << " ";
			_InOrder(p->_right);
		}
	}

	void InOrder()const
	{
		_InOrder(_root);
	}
};

9.小结

本文详细介绍了AVL树的概念、节点定义、插入、旋转、验证、删除及性能。AVL树通过严格维护节点的平衡因子,在插入和删除操作后通过旋转调整保持树的高度在 O(logn),从而保证查找、插入和删除操作的高效性,适用于频繁动态操作的数据管理场景。

  • 9
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值