文章目录
如发现不足之处或者差错,欢迎指出,大家共同进步!!!
平衡搜索二叉树(AVL)
- 特性
- 与搜索树类似,满足 根节点的值 >左子树的值 && 根节点的值< 右子树的值
- 子树的高度差 <= 1
例如下图
思考: 我们知道一般树的相关操作通常 为 O(logn),AVL 也一样,不同的是 AVL 的 插入 操作有些奇特: 插入一个节点可能破坏其特性,那么我们如何来完成对特性的修复呢?
方法:旋转大法
- 单旋转
- 双旋转
一、观察破坏特性的情形以及相应的解决方案
我们把必须重新平衡的节点称之为 α 。对于任意二叉树的节点来说,每个节点至多有两个孩子,故当插入一个新的节点,高度不平衡的情形为 左右子树高度差 为 2,容易看出分为以下四种情形:
1.1 情形
① ‘’ 外边 ‘’
- 对 α 的左孩子 的 左子树 插入
- 对 α 的右孩子 的 右子树 插入
② ‘’ 内边 ‘’
- 对 α 的左孩子 的 右子树 插入
- 对 α 的右孩子 的 左子树 插入
可以看出,上述情形 两两呈镜面对称。
- 对于 ”外边“,我们采用 单旋
- 对于 ”内边“,我们采用 双旋
1.2 解决方案
① 单旋转
- 对 α 的左孩子 的 左子树 插入
如图,插入节点后, 节点 k2 的 左子树的深度 比 右子树的深度 多 2 层,为使树恢复平衡,我们需要 把 x上移一层,而把 z下移一层。那么如何实现移动呢?通俗来说,就是重新构造一棵 新的满足条件的 AVL。
我们知道 在中序遍历中, k1的右孩子 必比 k1 大,而比 k2 小,即 k1 的右孩子为 k1 的后继,k2 的前驱,k1 本身又是比 k2 小的左孩子。故此,只需将 k1 作为新的根,k2作为k1的右子树,k1的右孩子作为 k2的左子树即可。
正如下图那样,经过重新构造后,我们得到了一棵新的AVL。旋转大法第一式完成!😃
- 对 α 的右孩子 的 右子树 插入
同上,插入新的节点后,节点 k2的 右子树的深度 比 左子树的深度 多 2 层,为使树恢复平衡,我们需要把 x上移一层,而把 z 下移一层。
不同的是,由于镜像对称 ,我们此时,用 k1 作为新的根,而k2作为 k1的左子树,k1的左孩子作为k2的右子树即可,具体原理与上述类似。旋转大法第二式完成!😺
② 双旋转
引言:当我们尝试对 "内边"插入节点后的树按照 单旋转 的思路 进行重新构造树,会发现,单旋转并不能减低它的深度。正如下图那样,因为 k1的 右孩子 y 太深了。故此,我们需要另辟蹊径,重新设计算法分析。具体情况如何,且听以下分解😏
- 对 α 的左孩子 的 右子树 插入
在上图中,我们已经对子树 y 插入了一个新的节点,这保证它是非空的状态。因此,我们不妨假设它有 一个根 和两棵子树。于是可以把整棵是看成是四棵子树由 3 个节点连接。如下图所示,恰好 树 B 或 树 C 中有 一棵 比树 D 深两层(除非它们都是空的),但我们不能具体是哪一棵。
为了重新平衡,我们不能继续让 k2 作为根了。又由 引言 所知,k1 和 k2 之间的旋转已经解决不了问题了,惟一的选择只能是把 k3 作为新的根,迫使 k1 是 k3 的左孩子,k2是 k3 的右孩子,k3 的右孩子作为 k2 的左孩子,从而完全确定树的最终结构。结果如下图所示。
- 对 α 的右孩子 的 左子树 插入
同上,插入新的节点后,节点 k2的 右子树的深度 比 左子树的深度 多 2 层,为使树恢复平衡,我们不能再让 k2 作为根了。同样由于镜像对称,我们迫使 k1作为 k3 的 右孩子,而 k2 作为 k3 的左孩子,k3 的 左孩子作为 k2 的右孩子,从而完全确定树的最终结构。结果如下图所示。
总结
- 我们递归插入一个节点到 AVL 中,如果 高度 不变,那么插入完成
- 若高度 改变,则 区分 四种 情形,并做相应的适当的旋转分析
二、代码块解析
2.1 前期声明
-
为获取 树的高度差,我们有多种方法,这里我采用 一个计算并返回树的函数 :getHight ( )
-
一个插入的函数 :insert ( )
-
以及相应的旋转 函数 :
- “ 外边”
1.左-左 型:singleRotateWithLeft ( )
2.右-右 型:singleRotateWithRight ( )
- “内边”
3.左-右 型:doubleRotateWithLeft ( )
4.右-左 型:doubleRotateWithRight ( )
2.2 声明
1. 获取 树高度 函数
int getHight(struct AVLTreeNode *root)
{
/*如果当前层无节点,说明无高度,故返回 0 */
if(root==NULL)
return 0;
/*获取左右子树的高度*/
int left=getHight(root->left);
int right=getHight(root->right);
return fmax(left,right)+1;/*或用三目运算符代替,因当前层不为空,说明高度应加 1 */
}
2. 插入函数
struct AVLTreeNode *insert(struct AVLTreeNode *root,ElmType x)
{
if(root==NULL)/*如果树为空,则创建一棵节点为一的 AVL,并返回*/
{
root=(struct AVLTreeNode *)malloc(sizeof(struct AVLTreeNode));
if(root==NULL)
printf("out of space\n");
else
{
root->val=x;
root->left=root->right=NULL;
}
}
else if(root->val>x)/* x 插入为 root 的左子树 */
{
root->left=insert(root->left,x);
if(getHight(root->left)-getHight(root->right)==2)
if(root->left->val>x)/* case 1 : 左-左 型 (外边) */
root=singleRotateWithLeft(root);
else/* case 3 : 左-右 型 (内边) */
root=doubleRotateWithLeft(root);
}
else if(root->val<x)/* x 插入为 root 的右子树 */
{
root->right(root->right,x);
if(getHight(root->right)-getHight(root->left)==2)
if(root->right->val<x)/*case 2 : 右-右 型 (外边) */
root=singleRotateWithRight(root);
else /* case 4 : 右-左 型 (内边) */
root=doubleRotateWithRight(root);
}
/* else : 插入的 元素 x 已经在 AVL 树里,我们什么都不用做 */
return root;/*返回 AVL*/
}
3. 相关旋转函数
-
单旋转
- 左-左 型
static struct AVLTreeNode *singleRotateWithLeft(struct AVLTreeNode *k2) { struct AVLTreeNode *k1; k1=k2->left;/* k1一开始为 k2 的左子树 */ /*开始旋转*/ k2->left=k1->right;/* k1 的右孩子作为 k2 的左子树*/ k1->right=k2;/* k2 作为 k1 的左右子树,让 k1 成为新的根 */ return k1;/* 返回 以 k1 作为根的新AVL*/ }
- 右-右 型
static struct AVLTreeNode *singleRotateWithRight(struct AVLTreeNode *k2) { struct AVLTreeNode *k1; k1=k2->right;/* k1一开始为 k2 的右子树 */ /*开始旋转*/ k2->right=k1->left;/* k1 的左孩子作为 k2 的右子树*/ k1->left=k2;/* k2 作为 k1 的左子树,让 k1 成为新的根 */ return k1;/* 返回 以 k1 作为根的新AVL*/ }
-
双旋转
- 左-右 型
static struct AVLTreeNode *doubleRotateWithLeft(struct AVLTreeNode *k2) { k2->left=singleRotateWithRight(k2->left);/* 在 k1 和 k3 间进行旋转 ,将 k3 作为 左子树 的根*/ return singleRotateWithLeft(k2);/* 在 k3 和 k2 间进行旋转,将k3作为 整棵树 的根 */ }
- 右-左 型
static struct AVLTreeNode *doubleRotateWithRight(struct AVLTreeNode *k2) { k2->right=singleRotateWithLeft(k2->right);/* 在 k1 和 k3 间进行旋转 ,将 k3 作为 右子树 的根*/ return singleRotateWithRight(k2);/* 在 k3 和 k2 间进行旋转,将k3作为 整棵树 的根 */ }