概述:本文介绍AVL树的实现,从零构建一颗AVL树,以及对应的插入、删除、旋转操作
什么是AVL树?
AVL树是带有平衡条件的二叉查找树,二叉查找树又区别于二叉树:保证有序
这个平衡条件是每个节点的左右子树高度差不超过 1
一、AVL树节点设计:
typedef struct node {int key; // 节点存储的数据int height; // 节点在树中的高度struct node *parent; // 指向其父节点的指针struct node *left; // 指向其左子节点struct node *right; // 指向其右子节点
} node;
这就是构建一颗AVL树的最小单位:节点
其中key是node可以存储的数据,height则用来保存节点的高度信息,其余的三个指针用来构建AVL树
此外,为了提高效率,我们维护一个结构体对象,用来指示整棵树的基本信息
struct tree{node *root; // 整棵树的根节点 node *min; // 整棵搜索树中的最小节点node *max; // 整颗搜索树中的最大节点
};
需要注意的是,每对AVL树做一次修改,都有可能改变这些指针,因此在后续的insert、delete等操作的设计时,需要考虑到这一点
二、初始化和辅助函数
对于每一个节点,它的声明周期从分配空间并初始化开始:
// 创建新节点
node* create_node(int k) {// 申请空间node* node = (struct node *)malloc(sizeof(struct node));if (!node) return NULL;node->key = k;node->parent = NULL; node->left = NULL;node->right = NULL;node->height = 1; // 默认最小高度为 1return node;
}
可以看到,创建一个节点只需要指定其key值,因为在创建之初,只有这一个属性是确定的
三个指针都应该被初始化为NULL,避免内存访问错误。
获取节点的高度:
// 节点高度
int height(node *n) { // 获取当前heightreturn n ? n->height : 0;
}// 更新节点高度
int update_height(node *n) { // 递归,一个节点的高度是其左右子节点的高度中最大值 + 1if (n != NULL) { n->height = MAX(height(n->left), height(n->right)) + 1;}return n->height;
}
找到最小节点、最大节点、根节点
node * find_min(node *root) {if (root == NULL) return root;node *cur = root;while (!(cur->left == NULL && cur->right == NULL)) { // 最小节点在左子树上cur = cur->left;}return cur;
}node * find_max(node *root) {if (root == NULL) return root;node *cur = root;while (!(cur->left == NULL && cur->right == NULL)) { // 最大节点在右子树上cur = cur->right;}return cur;
}node * find_root(node *n) {node *cur = n;while (cur->parent != NULL) {cur = cur->parent;}return cur;
}
其中根节点的查找很简单,给定任意一个节点就能找到整棵树的根节点
计算一个节点的左右子树高度差(用于判断平衡条件)
int get_balance(node *n) {return n ? update_height(n->left) - update_height(n->right) : 0;
}
高度差的计算默认使用 左子树高度 - 右子树高度
根据key查找一个节点:
node * search_node(int key) {node * temp = tree->root;while (!(temp->left == NULL && temp->right == NULL)) {if (key < temp->key)temp = temp->left;else if (key > temp->key)temp = temp->right;else return temp;}return NULL;
}
三、插入、删除、旋转
1.插入一个节点
插入一个节点有以下几种情况:
a. 根节点为空,则待插入节点为根节点
b. 根节点不为空,则待插入节点将作为叶子节点
void insert(node *root, node *node) {if (node == NULL) return;// 根节点为空的情况if (root == NULL) {root = node;tree->root = node; // 更新全局 roottree->min = node;tree->max = node;return;}// 根节点不为空struct node *cur = root;int k = node->key;// 遍历找到待插入节点的父节点while (!(cur->left == NULL && cur->right == NULL)) {if (k <= cur->key) {cur = cur->left;} else {cur = cur->right;}}// 插入到父节点下if (k <= cur->key) {cur->left = node;node->parent = cur;} else {cur->right = node;node->parent = cur;}insert_fix(node);tree->min = find_min(tree->root);tree->max = find_max(tree->root);
}
其中的insert_fix()函数是用来修正插入后造成的树不平衡问题,接下来会介绍
2.删除一个节点
删除一个节点比较复杂,可能有以下几种情况:
1.待删除节点是叶子节点,直接删除
2.待删除节点有一个子节点
3.待删除节点有左右两个子节点
注意第三种情况:
使用待删除节点的右子树的最小值或左子树的最大值来替换的原因是,右子树的最小节点一定是叶子节点,且大于待删除节点的右子节点,这个叶子节点在被替换到待删除节点的位置后,它的删除操作很简单
node* delete(node *root, node* n) {if (n == NULL) return NULL;node *temp;if (root == NULL) printf("target is not found\n");else { if (n->key < root->key)root->left = delete(root->left, n); // 在左子树递归查找删除else if (n->key > root->key)root->right = delete(root->right, n); // 在右子树递归查找删除else { // 删除当前根节点if (root->left && root->right) { // 被删除节点有左右两个子节点temp = find_min(root->right); // 用右子树的最小节点替换被删除节点root->key = temp->key; // 替换被删除节点root->right = delete(root->right, temp); // 删除用作替换的节点}else { // 被删除节点只有一个或无子节点temp = root;if (root->left == NULL) root = root->right;elseroot = root->left;free (temp); // 被删除的节点}}} update_height(root);delete_fix(find_min(tree->root));delete_fix(find_max(tree->root));tree->root = find_root(tree->root);update_height(tree->root);tree->min = find_min(tree->root);tree->max = find_max(tree->root);return root; // 删除后,返回根节点
}
左右单旋转、左右双旋转
1.右单旋
2.左单旋
3.左右双旋
4.右左双旋
先介绍右旋的实现
void right_rotate(node *up) {node *root = up->parent;node *down = up->right;if (down->left) { // 右旋时处理子节点的左子节点up->right = down->left;down->left->parent = up;}// 调换上下位置down->left = up; down->parent = root;up->parent = down;// heightup->height = update_height(up);down->height = update_height(down);
}
左旋和右旋的区别就在于,把上述代码的left和right字段调换位置
而左右和右左双旋就是进行两次旋转
因此,所有的旋转操作,只看这一段代码就够了,根据图示很好理解
修正代码:
void insert_fix(node *n) {while (n != tree->root) {update_height(n);int balance = get_balance(n);if (balance > 1) {if (get_balance(n->left) < 0) { // 考虑需要左右双旋的情况left_rotate(n->left);}right_rotate(n);} else if (balance < -1) {if (get_balance(n->right) > 0) { // 考虑右左双旋right_rotate(n->right);}left_rotate(n);}n = n->parent;}
}
delete和insert的修正代码相同
修正逻辑如下:
1.从叶子节点开始判断平衡条件
2.对于不平衡的节点,根据get_balance的值判断需要左旋还是右旋
3.判断不平衡节点的子节点是否平衡,因为上一次旋转操作可能会造成其兄弟节点不平衡
推荐学习 https://xxetb.xetslk.com/s/p5Ibb