在 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 树从原理到实现的详细介绍,希望对你有所帮助。如果你对其他数据结构也感兴趣,也可以关注我们的后续文章哦!