C++ 实现AVL树
一、介绍二叉搜索树
二叉搜索树,就是一种特殊的二叉树,二叉树中每个节点都分为左、右两个子节点。这两个子节点不仅拥有自己的子节点而且还指向自己父节点。下面是代码表示和图示
template<typename T>
struct TreeNode
{
TreeNode(T d,TreeNode* p=nullptr,TreeNode* l=nullptr,TreeNode* r= nullptr):data(d),parent(p),lc(l),rc(r){};
TreeNode* parent,lc,rc; //树型结构
T data; //数据元素
}
而二叉搜索树在二叉树的基础规则之上定义了新的规则:
每个节点的左子树均小于本节点,其右子树均大于本节点。
这样整棵树的中序遍历一定是有序的。那么一课这样的二叉搜索树,其查询效率可以近似的看做是o(n)。在完全二叉树的情况下,每次向下搜索都排除掉一半的选项。但是可以预想到现实中不存在完全巧合的刚好是一个完全二叉树,或者说完全二叉树的条件过于苛刻。
读者可以考虑一下,如果我们插入一个有序序列会发生什么?
可以看到这个都已经退化为链表了。那么搜索效率肯定是降低至o(n)了,因为需要遍历一遍(链式结构,不能二分)。
二、自平衡二叉树
通过上面的例子看出,普通的二叉搜索树是有缺陷的,甚至在极端情况下退化为链表。那么我们怎么解决的呢?科学家研究发明了自平衡二叉树(AVL),这棵树可以通过特殊的平衡方法去平衡节点。自平衡的方法就是旋转
AVL树的旋转
LL:对n1进行右旋
RR:对n1进项左旋
LR:先左旋最下面的节点,然后转换为LL的情况
RL:先右旋最下面的节点,然后转换为RR的情况
代码实现取了个巧,因为设计3个节点,4课子树,所以有一部分代码可以复用,就是调整树结构,我的设计就是先判断如何旋转,然后传入结构调整函数
//旋转接口
template<typename T>
TreeNodePtr<T> AVLTree<T>::rotateAt(TreeNodePtr<T> v)
{
TreeNodePtr<T> p = v->parent; TreeNodePtr<T> g = p->parent; //找出connect34中的3个节点
//三个节点关系v是孙子, p是父亲, g是爷爷
//根据node2是左子节点还是右子节点可以判断时左旋还是右旋
if (IsLChild(*p)) //p是左子树,zig
if (IsLChild(*v)) //v是左子树 ,zig
{
p->parent = g->parent;
return connect34(v, p, g, v->lc, v->rc, p->rc, g->rc);
}
else //zag
{
v->parent = g->parent;
return connect34(p, v, g, p->lc, v->lc, v->rc, g->rc);
}
else //zag
if (IsRChild(*v)) //v是左子树 zag
{
p->parent = g->parent;
return connect34(g, p, v, g->lc,p->lc, v->lc, v->rc);
}
else //zig
{
v->parent = g->parent;
return connect34(g, v, p, g->lc, v->lc, v->rc, p->rc);
}
}
//旋转
/* 三个节点,四颗子树 */
template<typename T>
TreeNodePtr<T> AVLTree<T>::connect34(TreeNodePtr<T> node1, TreeNodePtr<T>node2, TreeNodePtr<T>node3,
TreeNodePtr<T>tree1, TreeNodePtr<T>tree2, TreeNodePtr<T>tree3, TreeNodePtr<T>tree4) //平衡操作,根据输入顺序区分左右旋
{
node1->lc = tree1; if (tree1) tree1->parent = node1;
node1->rc = tree2; if (tree2) tree2->parent = node1;
updataHeight(node1); //更新该节点高度
node3->lc = tree3; if (tree3) tree3->parent = node3;
node3->rc = tree4; if (tree4) tree4->parent = node3;
updataHeight(node3);
node2->lc = node1; node1->parent = node2;
node2->rc = node3; node3->parent = node2;
updataHeight(node2);
return node2;
}
三、解析
插入和删除操作就是改变后,对失衡节点进行重平衡。AVL树如何判断是否失衡呢?通过平衡因子,其定义是左子树高度减去右子树高度,如果树高相差超过2(即平衡因子绝对值)。
判断平衡因子
//当前平衡因子
template<typename T>
inline int BalFac(const TreeNode<T>& x) { return (((x.lc)?x.lc->height:-1)-((x.rc)?x.rc->height:-1)); }
//是否平衡
template<typename T>
inline bool AvlBalanced(const TreeNode<T>& x) { return (-2 < BalFac(x) && 2 > BalFac(x)); }
搜索:二分搜索
template<typename T>
TreeNodePtr<T>& AVLTree<T>::search(const T& e)
{
//特殊情况:插入第一个节点
if (!_root || e == _root->data) {
_parent = NULL;
return _root;
} //在树根v处命中
//printf("树根不是节点\n");
for (_parent = _root; ; ) { //否则,自顶而下
TreeNodePtr<T>& c = (e < _parent->data) ? _parent->lc : _parent->rc; //确定方向
if (!c || e == c->data) {
return c;
}
_parent = c;
}
}
插入:先插入值,在判断是否需要重平衡。注意insert后可能在某个祖先节点失衡,极端情况下是在根节点处平衡。因为平衡导致的失衡传播(因为平衡操作导致树高变化,引起了祖先节点的失衡),所以需要全树遍历。
template<typename T>
TreeNodePtr<T> AVLTree<T>::insert(const T& e)
{
//先判断是否存在本节点
TreeNodePtr<T>& p = search(e); if (p!=nullptr) return p;
//确定此节点不存在,创建新节点,新节点一定是叶节点
TreeNodePtr<T> x= p = new TreeNode<T>(e, _parent);
_size++;
//向上循环重平衡
for (TreeNodePtr<T> i = _parent; i; i = i->parent) //从插入节点的父节点开始,向上逐层平衡
{
if (!AvlBalanced(*i)) //失衡,则调用接口复衡
{
FromParentTo(*i) = rotateAt(tallerChild(tallerChild(i))); //涉及的三个节点中进行判断,即判断几次旋转
break;
}
else
updataHeight(i); //平衡就更新高度,以防高度错误
}
return x;
}
删除:删除时需要找到直接后继(该节点在中序遍历中的下一个节点,被称为该节点的直接后继),删除操作除了找直接后继比较麻烦,平衡和insert一样。所以本来考虑抽出来写个函数,但是如果以后有时间再整理吧。
/*删除就是先删除节点,然后根据删除节点前后,向上平衡,平衡策略和insert一样,主要就是remove一个节点*/
template<typename T>
bool AVLTree<T>::remove(const T& e)
{
//先判断是否存在本节点
TreeNodePtr<T>& x = search(e); if (!x) return false; //节点不存在,返回错误
//确定此节点不存在,创建新节点,新节点一定是叶节点
{//想抽出来写个函数,这样比较整洁
TreeNodePtr<T> w = x; //实际被摘除的节点,初值同x
TreeNodePtr<T> succ = NULL; //实际被删除节点的接替者
if (!x->lc) //若*x的左子树为空,则可
succ = x = x->rc; //直接将*x替换为其右子树
else if (!x->rc) //若右子树为空
succ = x = x->lc;
else { //若左右子树均存在,则选择x的直接后继作为实际被摘除节点,为此需要
w = w->succ(); //(在右子树中)找到*x的直接后继*w
std::swap(x->data, w->data); //交换*x和*w的数据元素
TreeNodePtr<T> u = w->parent;
((u == x) ? u->rc : u->lc) = succ = w->rc;
}
_parent = w->parent; //记录实际被删除节点的父亲
if (succ) succ->parent = _parent; //并将被删除节点的接替者与parent相联
}
_size--; //这里是-1
//向上循环重平衡
for (TreeNodePtr<T> i = _parent; i; i = i->parent) //从插入节点的父节点开始,向上逐层平衡
{
if (!AvlBalanced(*i)) //失衡,则调用接口复衡
FromParentTo(*i) = rotateAt(tallerChild(tallerChild(i))); //涉及的三个节点中进行判断,即判断几次旋转
updataHeight(i); //删除树节点,高度更容易改变,需要依次更新节点
}
return true;
}
romParentTo(*i) = rotateAt(tallerChild(tallerChild(i))); //涉及的三个节点中进行判断,即判断几次旋转
updataHeight(i); //删除树节点,高度更容易改变,需要依次更新节点
}
return true;
}
四、源码地址
https://github.com/yqm-307/yqm_data_structure