二叉树查找算法查找时间依赖于树的拓扑结构。查找效率取决于树的高度,因此保持树的高度最小,即可保证树的查找效率。最佳情况是 O(log2n),而最坏情况是 O(n)。如果建立的二叉搜索树不平衡了。极端不平衡的情况就是类似一个单链表,这个时候,查找就需要O(n)时间复杂度。
假设我们能够对二叉树进行调正,使得二叉查找树总保持平衡或者不会出现太不平衡的情况。那不就能解决二叉查找树极端情况的出现了从而保证了二叉查找算法的效率了吗。 平衡二叉树的定义 简称平衡二叉树。由前苏联的数学家 Adelse-Velskil 和 Landis 在 1962 年提出的高度平衡的二叉树,根据科学家的英文名也称为 AVL 树。它具有如下几个性质:
可以是空树。假如不是空树,任何一个结点的左子树与右子树都是平衡二叉树,并且高度之差的绝对值不超过 1。平衡因子 定义:某结点的左子树与右子树的高度(深度)差即为该结点的平衡因子(BF,Balance Factor),平衡二叉树中不存在平衡因子大于 1 的节点。在一棵平衡二叉树中,节点的平衡因子只能取 0 、1 或者 -1 ,分别对应着左右子树等高,左子树比较高,右子树比较高。
ALV结构定义
typedef struct AVLNode *Tree; typedef int ElementType; struct AVLNode{ int depth; //深度,这里计算每个结点的深度,通过深度的比较可得出是否平衡 Tree parent; //该结点的父节点 ElementType val; //结点值 Tree lchild; Tree rchild; AVLNode(int val=0) { parent = NULL; depth = 0; lchild = rchild = NULL; this->val=val; } };最小失衡子树:在新插入的结点向上查找,以第一个平衡因子的绝对值超过 1 的结点为根的子树称为最小不平衡子树。也就是说,一棵失衡的树,是有可能有多棵子树同时失衡的。而这个时候,我们只要调整最小的不平衡子树,就能够将不平衡的树调整为平衡的树。
平衡二叉树的失衡调整主要是通过旋转最小失衡子树来实现的。根据旋转的方向有两种处理方式,左旋 与 右旋 。 旋转的目的就是减少高度,通过降低整棵树的高度来平衡。哪边的树高,就把那边的树向上旋转。
ALV树的失衡主要是在插入和删除结点的时候发生的。如: 插入结点: 删除结点: 失衡的情况主要有: A原本是一棵平衡二叉树,现在执行插入操作,可能的情况如表格所示:
插入方式描述旋转方式LL在 A 的左子树根节点的左子树上插入节点而破坏平衡右旋转RR在 A 的右子树根节点的右子树上插入节点而破坏平衡左旋转LR在A的左子树根节点的右子树上插入节点而破坏平衡先左旋后右旋RL在 A 的右子树根节点的左子树上插入节点而破坏平衡先右旋后左旋只需要执行一次右旋:(1)将失衡结点A变为其左孩子B的右孩子(2)将其原本的左孩子B的右孩子E变为A的左孩子
只需要执行一次左旋:(1)将失衡结点A变为其右孩子C的左孩子(2)将其原本的右孩子C的左孩子D变为A的右孩子
若 A 的左孩子节点 B 的右子树 E 插入节点 F ,导致结点 A 失衡,如图: 那我们该怎么做呢?先尝试以下右旋: 经过右旋调整发现,调整后树仍然失衡,说明这种情况单纯的进行右旋操作不能使树重新平衡。那么这种插入方式需要执行两步操作
(1)先对失衡节点 A 的左孩子 B 进行左旋操作,即上述 RR 情形操作。(2)对失衡节点 A 做右旋操作,即上述 LL 情形操作。 也就是说,经过这两步操作,使得 原来根结点的左孩子的右孩子 E 节点成为了新的根节点。右孩子插入左节点的过程与左孩子插入右节点过程类似,也是需要执行两步操作,使得旋转之后为 原来根结点的右孩子的左孩子作为新的根节点 。(A->D)
(1)先对失衡节点 A 的右孩子 C 进行右旋操作,即上述 LL 情形操作。(2)对失衡节点 A 做左旋操作,即上述 RR情形操作。运行结果
在所有的不平衡情况中,都是按照先 寻找最小不平衡树,然后 寻找所属的不平衡类别,再 根据 4 种类别进行固定化程序的操作。 LL , LR ,RR ,RL其实已经为我们提供了最后哪个结点作为新的根指明了方向。如 LR 型最后的根结点为原来的根的左孩子的右孩子,RL 型最后的根结点为原来的根的右孩子的左孩子。只要记住这四种情况,可以很快地推导出所有的情况。 维护平衡二叉树,最麻烦的地方在于平衡因子的维护。
AVL 树和二叉查找树的删除操作情况一致,都分为四种情况:
+(1)删除叶子节点 +(2)删除的节点只有左子树 +(3)删除的节点只有右子树 +(4)删除的节点既有左子树又有右子树
只不过 AVL 树在删除节点后需要重新检查平衡性并修正,同时,删除操作与插入操作后的平衡修正区别在于,插入操作后只需要对插入栈中的弹出的第一个非平衡节点进行修正,而删除操作需要修正栈中的所有非平衡节点。
删除操作的大致步骤如下:
1、 以前三种情况为基础尝试删除节点,并将访问节点入栈。 如果尝试删除成功,则依次检查栈顶节点的平衡状态,遇到非平衡节点,即进行旋转平衡,直到栈空。 如果尝试删除失败,证明是第四种情况。这时先找到被删除节点的右子树最小节点并删除它,将访问节点继续入栈。再依次检查栈顶节点的平衡状态和修正直到栈空。对于删除操作造成的非平衡状态的修正,可以这样理解:对左或者右子树的删除操作相当于对右或者左子树的插入操作,然后再对应上插入的四种情况选择相应的旋转就好了。
代码实现:
//删除结点 Tree* Delete(Tree *root, const ElenmentType k) { if (NULL == root) return root; if (!binaryTreeSearch(root,k))//查找删除元素是否存在 { std::cerr << "Delete error , key not find" << std::endl; return root; } if (k == root->value)//根节点 { if (root->left!=NULL&&root->right!=NULL)//左右子树都非空 { if (diff(root) > 0)//左子树更高,在左边删除 { root->value = tree(root->left,0);//以左子树的最大值替换当前值 root->left = Delete(root->left, root->value);//删除左子树中已经替换上去的节点 } else//右子树更高,在右边删除 { root->value = tree(root->right,1); root->right = Delete(root->right, root->value); } } else//有一个孩子、叶子节点的情况合并 { Tree * tmp = root; root = (root->left) ? (root->left) :( root->right); delete tmp; tmp = NULL; } } else if (k < root->value)//往左边删除 { root->left = Delete(root->left, k);//左子树中递归删除 //判断平衡的条件与在插入时情况类似 if (diff(root) < -1)//不满足平衡条件,删除左边的后,右子树变高 { if (diff(root->right) > 0) { root = RL_rotate(root); } else { root = RR_rotate(root); } } }//end else if else { root->right = Delete(root->right, k); if (diff(root) > 1)//不满足平衡条件 { if (diff(root->left) < 0) { root = LR_rotate(root); } else { root = LL_rotate(root); } } } return root; }运行结果:
平衡二叉树在查找的时候就有二叉查找树的优势,同时也保持了平衡。
但是平衡二叉树的劣势在于:
删除:对于平衡二叉树来说,在最坏情况下,需要维护从被删节点到根节点这条路径上所有节点的平衡性,旋转的量级是OlogN。但是红黑树就不一样了,最多只需3次旋转就会重新平衡,旋转的量级是O(1)。保持平衡:平衡二叉树高度平衡,这也就意味着在大量插入和删除节点的场景下,平衡二叉树为了保持平衡需要调整的频率会更高。 所以在大量查找的情况下,平衡二叉树的效率更高,也是首要选择。在大量增删的情况下,红黑树是首选。参考博客: https://blog.csdn.net/zhangxiao93/article/details/51459743 https://zhuanlan.zhihu.com/p/56066942