新手如何实现从零开始 C++---- 十【C++ 数据结构】AVL 树详解:从原理到实现

在 C++ 的数据结构里,AVL 树就像是一颗闪耀的明星,它以其独特的平衡特性在众多数据结构中脱颖而出。今天,我们就来深入探讨一下 AVL 树,从它的原理到具体实现,带你揭开这颗明星的神秘面纱。

一、什么是 AVL 树

1.1 定义

AVL 树是一种自平衡的二叉搜索树,由苏联数学家 Georgy Adelson - Velsky 和 Evgenii Landis 在 1962 年提出,这也是它名字的由来。二叉搜索树(BST)是一种每个节点的左子树节点值都小于该节点值,右子树节点值都大于该节点值的树结构。而 AVL 树在 BST 的基础上,增加了一个重要的特性:每个节点的左右子树的高度差不超过 1。这个特性保证了 AVL 树的高度始终保持在 O (logn ) 的级别,从而使得插入、删除和查找操作的时间复杂度也都是 O (logn)。

1.2 为什么需要 AVL 树

普通的二叉搜索树在某些情况下可能会退化为链表,比如插入的数据是有序的。这时候,插入、删除和查找操作的时间复杂度就会退化为 O (n),效率大大降低。而 AVL 树通过自平衡的机制,避免了这种情况的发生,保证了操作的高效性。

二、AVL 树的原理

2.1 平衡因子

为了实现自平衡,AVL 树引入了平衡因子(Balance Factor)的概念。平衡因子是指一个节点的左子树的高度减去右子树的高度。对于 AVL 树中的任意节点,其平衡因子只能是 -1、0 或 1。如果某个节点的平衡因子的绝对值大于 1,那么这棵树就失去了平衡,需要进行调整。

2.2 旋转操作

当 AVL 树失去平衡时,需要通过旋转操作来恢复平衡。旋转操作主要有四种:左旋、右旋、左 - 右旋和右 - 左旋。

2.2.1 右旋

右旋操作主要用于处理左左情况,即某个节点的左子树的左子树导致了树的不平衡。下面是右旋的具体步骤:

  • 以当前不平衡节点为根节点,将其左子节点作为新的根节点。
  • 原根节点的左子节点的右子树成为原根节点的左子树。
  • 原根节点成为新根节点的右子节点。

cpp

// 右旋操作 Node* rightRotate(Node* y) { Node* x = y->left; Node* T2 = x->right; // 执行旋转 x->right = y; y->left = T2; // 更新高度 y->height = max(height(y->left), height(y->right)) + 1; x->height = max(height(x->left), height(x->right)) + 1; // 返回新的根节点 return x; }

2.2.2 左旋

左旋操作主要用于处理右右情况,即某个节点的右子树的右子树导致了树的不平衡。左旋的步骤与右旋类似,只是方向相反:

  • 以当前不平衡节点为根节点,将其右子节点作为新的根节点。
  • 原根节点的右子节点的左子树成为原根节点的右子树。
  • 原根节点成为新根节点的左子节点。

cpp

// 左旋操作 Node* leftRotate(Node* x) { Node* y = x->right; Node* T2 = y->left; // 执行旋转 y->left = x; x->right = T2; // 更新高度 x->height = max(height(x->left), height(x->right)) + 1; y->height = max(height(y->left), height(y->right)) + 1; // 返回新的根节点 return y; }

2.2.3 左 - 右旋

左 - 右旋操作是先对不平衡节点的左子节点进行左旋,然后再对不平衡节点进行右旋,主要用于处理左右情况,即某个节点的左子树的右子树导致了树的不平衡。

cpp

// 左 - 右旋操作 Node* leftRightRotate(Node* z) { z->left = leftRotate(z->left); return rightRotate(z); }

2.2.4 右 - 左旋

右 - 左旋操作是先对不平衡节点的右子节点进行右旋,然后再对不平衡节点进行左旋,主要用于处理右左情况,即某个节点的右子树的左子树导致了树的不平衡。

cpp

// 右 - 左旋操作 Node* rightLeftRotate(Node* z) { z->right = rightRotate(z->right); return leftRotate(z); }

三、AVL 树的实现

3.1 节点结构

首先,我们需要定义 AVL 树的节点结构。每个节点包含一个键值、左右子节点指针和节点的高度。

cpp

// 定义 AVL 树的节点结构 struct Node { int key; Node* left; Node* right; int height; Node(int k) : key(k), left(nullptr), right(nullptr), height(1) {} };

3.2 辅助函数

为了方便操作,我们还需要一些辅助函数,比如获取节点的高度、计算平衡因子等。

cpp

// 获取节点的高度 int height(Node* node) { return node ? node->height : 0; } // 获取平衡因子 int getBalance(Node* node) { return node ? height(node->left) - height(node->right) : 0; }

3.3 插入操作

插入操作是 AVL 树的核心操作之一。在插入一个新节点后,需要更新节点的高度,并检查树是否失去平衡。如果失去平衡,需要根据具体情况进行相应的旋转操作。

cpp

// 插入操作 Node* insert(Node* node, int key) { // 标准的 BST 插入操作 if (!node) return new Node(key); if (key < node->key) node->left = insert(node->left, key); else if (key > node->key) node->right = insert(node->right, key); else return node; // 更新节点的高度 node->height = 1 + max(height(node->left), height(node->right)); // 获取平衡因子 int balance = getBalance(node); // 左左情况 if (balance > 1 && key < node->left->key) return rightRotate(node); // 右右情况 if (balance < -1 && key > node->right->key) return leftRotate(node); // 左右情况 if (balance > 1 && key > node->left->key) return leftRightRotate(node); // 右左情况 if (balance < -1 && key < node->right->key) return rightLeftRotate(node); return node; }

3.4 删除操作

删除操作同样需要更新节点的高度,并检查树的平衡。在删除一个节点后,如果树失去平衡,也需要进行相应的旋转操作。

cpp

// 找到最小键值的节点 Node* minValueNode(Node* node) { Node* current = node; while (current->left != nullptr) current = current->left; return current; } // 删除操作 Node* deleteNode(Node* root, int key) { // 标准的 BST 删除操作 if (!root) return root; if (key < root->key) root->left = deleteNode(root->left, key); else if (key > root->key) root->right = deleteNode(root->right, key); else { if ((root->left == nullptr) || (root->right == nullptr)) { Node* temp = root->left ? root->left : root->right; if (temp == nullptr) { temp = root; root = nullptr; } else *root = *temp; delete temp; } else { Node* temp = minValueNode(root->right); root->key = temp->key; root->right = deleteNode(root->right, temp->key); } } if (root == nullptr) return root; // 更新节点的高度 root->height = 1 + max(height(root->left), height(root->right)); // 获取平衡因子 int balance = getBalance(root); // 左左情况 if (balance > 1 && getBalance(root->left) >= 0) return rightRotate(root); // 左右情况 if (balance > 1 && getBalance(root->left) < 0) return leftRightRotate(root); // 右右情况 if (balance < -1 && getBalance(root->right) <= 0) return leftRotate(root); // 右左情况 if (balance < -1 && getBalance(root->right) > 0) return rightLeftRotate(root); return root; }

3.5 查找操作

查找操作与普通的二叉搜索树类似,根据键值的大小递归地在左子树或右子树中查找。

cpp

// 查找操作 Node* search(Node* root, int key) { if (root == nullptr || root->key == key) return root; if (root->key < key) return search(root->right, key); return search(root->left, key); }

四、AVL 树的应用场景

AVL 树由于其平衡特性,在需要高效查找、插入和删除操作的场景中非常有用。比如在数据库索引中,AVL 树可以帮助快速定位数据;在编译器的符号表管理中,也可以使用 AVL 树来提高查找效率。

AVL 树作为一种自平衡的二叉搜索树,通过引入平衡因子和旋转操作,保证了树的高度始终保持在 O (logn ) 的级别,从而使得插入、删除和查找操作的时间复杂度也都是 O (logn)。在实现 AVL 树时,需要注意节点结构的定义、辅助函数的编写以及插入和删除操作中的平衡调整。

AVL 树虽然在大多数情况下表现良好,但也有一些缺点。比如在频繁插入和删除的场景中,旋转操作会带来一定的开销。不过,这并不影响它在很多场景中的应用。

希望通过本文的介绍,你对 AVL 树有了更深入的了解,并且能够在实际编程中灵活运用。如果你在学习过程中遇到任何问题,欢迎在评论区留言讨论。让我们一起在 C++ 的数据结构世界里继续探索吧!

参考文章链接:

https://github.com/madebi12w3/tech-fymiwlpv/blob/main/README.md

https://github.com/madebi12w3/tech-auawpppt/blob/main/README.md

https://github.com/madebi12w3/tech-exrycwfn/blob/main/README.md

https://github.com/madebi12w3/tech-xsvpztuk/blob/main/README.md

以上就是关于 AVL 树从原理到实现的详细介绍,希望对你有所帮助。如果你对其他数据结构也感兴趣,也可以关注我们的后续文章哦!