【前置知识】二叉树和二叉排序树
目录
前言
对于大量的数据而言,链表的线性访问时间太慢,不宜使用。本章节将会介绍一种简单的数据结构:树(tree),其大部分操作的运行时间平均为O(logN)。在数据结构中树是非常有用的抽象概念,在本篇中我们将讨论二叉平衡树,为后续高阶搜索树打下基础。
1.什么是AVL树
AVL(Adelson-Velskii and Landis)树即二叉平衡树,是历史上第一棵自平衡树,本质上是一棵带有平衡条件的二叉排序树。
想要让AVL树保持平衡的条件有很多种,但这些平衡条件必须容易保持,而且必须保证树的深度是O(logN),最简单的实现是要求左右子树的高度差的绝对值不超过1;
2.怎么平衡
我们总是要对一棵树进行增删查改的操作,但是增加或者删除一个节点可能会破坏现有的平衡性质,因此我们更关心的是如何让AVL树保持一个平衡的状态,事实上,这总是可以通过对树进行简单的修正来做到,这个操作我们称为旋转(rotation)。
①左旋
对于如下所示的一棵现在是平衡的AVL树,我们要插入一个新的节点99,根据标准的二叉排序树(以下简称BST)的插入步骤, 节点99会被插入到根节点66的右子树上;
此时我们会发现,根节点66的左子树的高度是1,而它的右子树的高度此时变成了3,显然3与1的差值是大于1的,是不平衡的状态;
对于节点66,我们称之为失衡节点。 而我们要做的:
- 将失衡节点A的右孩子B替换到失衡节点的位置;
- 如果此时右孩子B还有左孩子C,就把C变成失衡节点A的右孩子;
- 失衡节点A变成原先右孩子B的左子树;
我们对失衡节点66执行上述步骤后,得到如下的树,此时树又回到了平衡的状态;
为了方便记忆,对于这种情况我们先称为:右孩子的右子树(节点99插在失衡节点66的右孩子77的右子树88上)。
代码实现也很简单只需按照上述步骤写就行了;
avlnode* k2 = node->right;
node->right = k2->left;
k2->left = node;
②右旋
同样的当出现左孩子的左子树的情况时,我们也只需要简单的将上述的步骤反过来,即右旋。
- 将失衡节点A的左孩子B替换到失衡节点的位置;
- 如果此时左孩子B还有右孩子C,就把C变成失衡节点A的左孩子;
- 失衡节点A变成原先左孩子B的右子树;
avlnode* k2 = node->left;
node->left = k2->right;
k2->right = node;
③先左旋再右旋
但是单一的左旋与单一的右旋一定不是万能的。如果出现了例如左孩子的右子树的情况,我们尝试用单一的右旋操作,是无法实现平衡的(可以自己动手画画),因为这里并没有减少右子树的高度,这时候我们就要先对失衡节点的左孩子左旋,降低右子树的高度,再对失衡节点右旋;
至于旋转操作和上面是一样的这里就不再赘述;
④先右旋再左旋
如果出现的是右孩子的左子树的情况,就先对失衡节点的右孩子右旋,降低左子树的高度,再对失衡节点左旋;
最后别忘记了,每一次旋转后都要重新计算一下树和每个节点的高度。这里代码实现的大致思路就是通过递归使每个节点自下往上分别+1,而叶子节点高度为0 ,这样我们在确定一个树的节点时只需要知道它子节点的中最大的高度并+1就好。
旋转的完整代码如下:
#define HEIGHT(node) ((node == NULL) ? 0 : (((avlnode*)(node))->height))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
//获取节点高度
int getNode_Height(avlnode* node)
{
return HEIGHT(node);
}
//右旋LL
avltree left_left_rotation(avltree node)//node是失衡节点
{
avlnode* k2 = node->left;
node->left = k2->right;
k2->right = node;
//所有的旋转操作都要改变树的高度
node->height = MAX(getNode_Height(node->left), getNode_Height(node->right)) + 1;
k2->height = MAX(getNode_Height(node->left), getNode_Height(node->right)) + 1;
return k2;
}
//左旋RR
avltree right_right_rotation(avltree node)//node是失衡节点
{
avlnode* k2 = node->right;
node->right = k2->left;
k2->left = node;
//所有的旋转操作都要改变树的高度
node->height = MAX(getNode_Height(node->left), getNode_Height(node->right)) + 1;
k2->height = MAX(getNode_Height(node->left), getNode_Height(node->right)) + 1;
return k2;
}
//先左旋再右旋LR
avltree left_right_rotation(avltree node)
{
node->left = right_right_rotation(node->left);
node = left_left_rotation(node);
return node;
}
//先右旋再左旋RL
avltree right_left_rotation(avltree node)
{
node->right = left_left_rotation(node->right);
node = right_right_rotation(node);
return node;
}
插入的实现按照BST的标准插入步骤小于插入左边大于插入右边即可,但是同时需要注意判断插入之后当前节点可能存在失衡的情况,这时就要用旋转的操作及时修正;
这里同样是通过递归更新每一个节点的高度。
avltree insert(avltree node, int key)
{
//当根节点为NULL
if (node == NULL)
{
//创建节点
avlnode* root = create(key, NULL, NULL);
node = root;
}
//如果说不为空 考虑往哪边插入
//往左子树插入
else if (key < node->data)
{
//递归去寻找待插节点的位置
node->left = insert(node->left, key);
//因为是左边插入 如果有失衡就一定是左边失衡
//高度差等于2 就是最小失衡树
if (HEIGHT(node->left) - HEIGHT(node->right) == 2)
{
//判断此时是属于哪一种失衡
//如果插入的值是在节点的左子树 那么就是左子树的左子树LL的情况
if (key < node->left->data)
{
left_left_rotation(node);
}
else
{
//否则就是左子树的右子树LR的情况
left_right_rotation(node);
}
}
}
else if (key > node->data)
{
//递归去寻找要插入的位置
node->right = insert(node->right, key);
//往右边插入 如果有失衡那么一定是右边失衡
if (HEIGHT(node->right) - HEIGHT(node->left) == 2)
{
//判断属于哪一种情况
//RR
if (key > node->right->data)
{
right_right_rotation(node);
}
//RL
else
{
right_left_rotation(node);
}
}
}
//重新调整二叉树的深度
//树的深度取决于大的那一个
//通过这个操作把每一个节点的高度都更新了 因为上面有递归访问了每一个节点
node->height = MAX(getNode_Height(node->left), getNode_Height(node->right)) + 1;//插入了一个新节点 所以加1
return node;
}
删除的操作就是单纯的对树进行BST的删除,与插入不同的是,插入只可能引起某一个节点的失衡,而删除操作有可能会导致多个节点都失衡,这时候就需要向上检索失衡并修正。
和BST的标准删除一样,删除分为三种情况:
- 删除的是叶子;
- 删除的节点只有一个孩子;
- 删除的节点既有左子树又有右子树;
AVL树中的难点就在于删除的操作(情况多且复杂),这里一定一定要先把逻辑理顺了,才去结合代码。
//查找操作
avltree searchNode(avltree node, int key)
{
//如果是空树或者要删除的值就是根节点
if (node == NULL || node->data == key)
{
return node;
}
else if(key < node->data)
{
searchNode(node->left, key);
}
else if (key > node->data)
{
searchNode(node->right, key);
}
}
//寻找左子树的最大值
avltree findMax(avltree tree)
{
if (tree != NULL)
{
while (tree->right)
{
tree = tree->right;
}
}
return tree;
}
//删除操作
avltree deleteNode(avltree tree, int key)
{
//node是找到的要删除的那一个节点
avlnode* node = searchNode(tree, key);
if (node == NULL || tree == NULL)
{
//如果要删除的节点为空或者是空树
return tree;
}
//还是要去判断属于哪一种失衡情况
//要删除的节点在左子树
if (key < tree->data)
{
tree->left = deleteNode(tree->left, key);
//判断有没有失衡
//和插入正好相反!如果是在左边删除导致的失衡 那么一定是右子树高
if (HEIGHT(tree->right) - HEIGHT(tree->left) == 2)
{
//如果失衡且右子树的左子树存在 那么就是RL的情况
if (tree->right->left)
{
tree = right_left_rotation(tree);
}
else
{
tree = right_right_rotation(tree);
}
}
}
//要删除的节点在右子树
else if (key > tree->data)
{
tree->right = deleteNode(tree->right, key);
//如果是在右边删除导致的失衡 那么一定是左子树高
if (HEIGHT(tree->left) - HEIGHT(tree->right) == 2)
{
//如果失衡且左子树的右子树存在 那么就是LR的情况
if (tree->left->right)
{
tree = left_right_rotation(tree);
}
else
{
tree = left_left_rotation(tree);
}
}
}
//找到了待删除的节点 就删除
else
{
//保证二叉排序树的有序性
//如果左右孩子都有的情况 找左孩子的最大值替换要删除的节点
if (tree->left && tree->right)
{
avlnode* left_max_node = findMax(tree->left);
tree->data = left_max_node->data;
//还要删除原先的最大值节点
tree->left = deleteNode(tree->left, left_max_node->data);
}
else
{
//独子或者无子的情况
tree = tree->left ? tree->left : tree->right;
}
}
if(tree)
{
tree->height = MAX(getNode_Height(tree->left), getNode_Height(tree->right)) + 1;//重置每一个节点高度
}
return tree;
}
最后附上main函数和测试结果:
int main()
{
avltree tree = NULL;
int arr[] = { 12, 9, 17, 6, 11, 13, 18, 4, 15 };
int size = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < size; i++)
{
tree = insert(tree, arr[i]);
}
//prev_order(tree);
in_order(tree);
printf("\n");
tree = deleteNode(tree, 18);
in_order(tree);
system("pause");
return 0;
}
如果代码有疑问的同学可以私信问我,一定要先把逻辑理顺!一定要先把逻辑理顺!一定要先把逻辑理顺!重要的话说三遍(;д;),不然后面的高阶搜索树会很吃亏的!