AVL树是一种紧凑的二叉查找树。它的每个结点,都有左右子树高度相等,或者只相差1这样的特性。文章https://blog.csdn.net/aaasssdddd96/article/details/106291144给出了一个例子。
为了便于讨论,这里对AVL树的结点平衡情况定义2个名称,如果两边左右子树高度相等,称之为真平衡,如果两边高度相差1,称之为准平衡。真平衡和准平衡的这个意义只限于在本文的讨论。准平衡有2种情况,所以实际是3种情况。
当AVL树中插入一个新结点时,它的父结点如果是真平衡,那么状态转化为准平衡,树的高度增加了1,因为高度增加,这个父结点需要接着向更高一层的父结点报告这个变化。
如果父结点是准平衡,那么也有2种情况。如果增加的高度是在较矮的一侧,那么父结点状态转为真平衡,操作完成;如果增加的高度是在较高的一侧,那么结点的平衡因子超限,这种情况在上面第一种情况的最后一个变化发生,需要进行平衡调整。调整之后,恢复为真平衡,局部高度不增,操作完成。
这些问题不难。真正让初学者感到烦恼的是平衡调整。调整分4种情况,分别称为“LL-旋转”,“LR-旋转”,“RL-旋转”,“RR-旋转”。后2种和前两种镜像对应。编写这部分代码的时候,最好写完前2种,休息20分钟,再去复制前面的代码,逐条改成它的镜像。休息片刻是为了避免头晕,转来转去转的头一晕,修改镜像时随便漏掉一条,就会带来极为费时难调的bug。这样编程并没有什么不对,这是需要掌握的基本功,而且实际开发过程也会常常碰到需要蛮力硬搞来攻克的难题。
但在这里是否有比硬搞好一点的办法?
如果在构造AVL树时,预分配3个储备结点会怎么样。左、中、右,预先结成一个三角形。但不在AVL树中,而是备用:
struct avl_help {struct node *root;struct triangle {struct node *left;struct node *mid;struct node *right;} help;
};struct avl_help avl;
初始化为:
avl.root=NULL;
avl.help.left = (struct node*)malloc(sizeof(struct node));
avl.help.mid = (struct node*)malloc(sizeof(struct node));
avl.help.right = (struct node*)malloc(sizeof(struct node));avl.help.mid->left=avl.help.left;
avl.help.mid->right=avl.help.right;
avl.help.left->parent=avl.help.mid;
avl.help.right->parent=avl.help.mid;
现在大家都想到了:在AVL树需要发生平衡调整的时候,把已经调好的3个储备结点拿出来,整体替换掉树中需要调整的3个结点,再把替换下来的原来的3个结点做成初始化中的左、中、右三角结构,存回到help中储备,留给下次用。这样调整就完成了。而替换下来的3个结点只要把头结点连到中间结点的左指针,或右指针,具体看那个指针空闲,就重新构成了三角结构。
以“LR型-旋转”为例,调整操作大致是这样子。假设ap0,ap1,ap2三个指针已经分别指向LR型的待调整的上中下3个结点:
help= &avl.help;
help->left->left=ap1->left;
help->left->right=ap2->left;
help->left->tag=(ap2->tag==1?0:-1);help->right->left=ap2->right;
help->right->right=ap0->right;
help->right->tag=(ap2->tag==-1?1:0);help->mid->tag=0;
if(help->left->left)
help->left->left->parent=help->left;
if(help->left->right)
help->left->right->parent=help->left;
if(help->right->left)
help->right->left->parent=help->right;
if(help->right->right)
help->right->right->parent=help->right;
help->mid->parent=ap0->parent;if(help->mid->parent==NULL)
avl->root= help->mid;
else if(help->mid->parent->left==ap0)
help->mid->parent->left=help->mid;
else help->mid->parent->right=help->mid;
而存回替换下来的ap0,ap1,ap2三个结点的代码就是:
ap1->left=ap0;
ap0->parent=ap1;
help->left= ap0;
help->mid=ap1;
help->right=ap2;
其它几种情况可以类推,大同小异,这里就不重复了。