【C++】AVL树(AVLTree)
目录
一、AVL树概念:
二、定义:
三、AVL树的插入:
四、AVL树的旋转:
1、左单旋:
2、右单旋:
3、右左双旋:
4、左右双旋:
五、AVL树的检验:
一、AVL树概念:
搜索二叉树的缺点:
二叉搜索树虽然正常来说其搜索效率不错,但是其结构不够严格导致其下限很低(趋近于链式结构的那种)这样的话其搜索就会相当于在顺序表中查找,效率变得很低,下限不能够保证。
于是下面两位前苏联学家就搞了AVL树:
解决方法:
当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
于是在每一个节点中引入了平衡因子来记录这个节点的左右子树的高度差,
又引入了一个指针指向这个节点的父节点,方便旋转。
如下就是一个AVL树,蓝色的数字是这个节点的平衡因子(右子树的高度减左子树的高度)
二、定义:
AVL的每一个节点相比于搜索二叉树多了一个指针指向父节点 还有一个变量记录平衡因子。
这叫做三叉链式结构
template<class K, class V>
struct AVLTreeNode
{pair<K, V> _kv;AVLTreeNode<K, V>* _left;AVLTreeNode<K, V>* _right;AVLTreeNode<K, V>* _parent;int _bf;AVLTreeNode(pair<K, V> kv):_kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _bf(0){}
};
三、AVL树的插入:
思路:
1、毕竟是根据二叉搜索树修改的,在找待插入位置的时候可以按照二叉搜索树的插入方法。
2、找到待插入位置后,将待插入结点插入到树中。
3、更新平衡因子,如果出现不平衡(即_bf不是1/0/-1),则需要进行旋转。
二叉搜索树的插入规则:1、待插入结点的key值比当前结点小就往该结点的左子树找。
2、待插入结点的key值比当前结点大就往该结点的右子树找。
3、待插入结点的key值与当前结点的key值相等就插入失败。直到找到与待插入结点的key值相同的结点或者最终走到空节点位置进行插入。
在插入完成后,
该节点的左右子树的高度发生了变化,就需要更新平衡因子,因此插入一个节点后,该节点和该节点的祖先节点的平衡因子也可能需要更新。
平衡因子的修改规则:
1、如果新插入的节点,在它的父节点的左边,它的父节点的平衡因子就--;
2、如果新插入的节点,在它的父节点的右边,它的父节点的平衡因子就++;
这个新节点的平衡因子就是0;
接着要判断它们的祖先是否要进行平衡因子的修改
这个时候多出来的指针parent就发挥很大的作用了
每修改完一个节点的平衡因子都要往上走找这个节点的parent
祖先的平衡因子的修改规则:
1、如果修改后其parent的平衡因子是1或-1就证明这个树影响了祖先,要继续往上找。
2、如果修改后其parent的平衡因子是0,就证明下面新增的节点补全了这个0平衡因子的节点的父节点的少的那一边树,就需要往上继续修改了
3、如果修改后其parent的平衡因子是2或-2,那么此时以parent为根的树就被破坏平衡了,那么就需要旋转了
注意!!!!!!!!!!!
旋转之后平衡因子已经改好了,这个时候就需要退出循环了
bool Insert(pair<K, V> kv)
{//先进来判断这个树是不是空树if (_root == nullptr){_root = new Node(kv);return true;}Node* cur = _root;Node* parent = nullptr;//像二叉搜索树那样找待插入节点while (cur){if (cur->_kv.first > kv.first){parent = cur;cur = cur->_left;}else if (cur->_kv.first < kv.first){parent = cur;cur = cur->_right;}else{return false;}}//走到这就说明找到了cur = new Node(kv);//下面操作是连接新的节点到树中if (parent->_kv.first < cur->_kv.first){parent->_right = cur;}else{parent->_left = cur;}cur->_parent = parent;while (parent){//控制新节点的父节点的平衡因子if (cur == parent->_left){parent->_bf--;}else //if (cur == parent->_right){parent->_bf++;}//向上修改祖先的平衡因子if (parent->_bf == 0){break;}else if (abs(parent->_bf) == 1){//继续往上走cur = parent;parent = parent->_parent;}else if (abs(parent->_bf) == 2){//旋转}//防止出现其他情况else{assert(false);}break;}return true;
}
四、AVL树的旋转:
我们将插入节点的父节点记为cur,cur的父节点称为parent
当找到一个parent节点为2或-2的时候,就需要进行旋转了,此时cur必定为1或者-1,不可能为0,(毕竟是0就不会再往上走了),
这个时候旋转情况分为四种:
当parent的平衡因子为2,cur的平衡因子为1时,进行左单旋
当parent的平衡因子为-2,cur的平衡因子为-1时,进行右单旋
当parent的平衡因子为-2,cur的平衡因子为1时,进行左右双旋
当parent的平衡因子为2,cur的平衡因子为-1时,进行右左双旋注意:
1、旋转的时候要保持它是搜索树始终
2、要变平衡并且降低这个子树的高度
1、左单旋:
新节点插入较高右子树的右侧
接下来上述h == 1的情况扩展为h == 2情况:
思路:
1、让cur的左子树成为parent的右子树
2、让parent成为cur的左子树
3、修改各自的父指针
4、让cur成为根或者让cur成为parent的原来父节点的子树(要提前保存parent的父节点)
5、更新平衡因子
6、h == 3及以上思路都是一样的
void RotateL(Node* parent)
{Node* cur = parent->_right;Node* curleft = cur->_left;parent->_right = curleft;if (curleft){curleft->_parent = parent;}cur->_left = parent;//将parent的父节点保存起来Node* pparent = parent->_parent;parent->_parent = cur;if (parent == _root){_root = cur;cur->_parent = nullptr;}else{if (pparent->_kv.first < cur->_kv.first){pparent->_right = cur;}else //if (pparent->_kv.first > cur->_kv.first){pparent->_left = cur;}cur->_parent = pparent;}parent->_bf = cur->_bf = 0;
}
2、右单旋:
新节点插入较高左子树的左侧:
下面就直接上h == 2情况:
思路:
1、让cur的右子树成为parent的左子树
2、让parent成为cur的右子树
3、修改各自的父指针
4、让cur成为根或者让cur成为parent的原来父节点的子树(要提前保存parent的父节点)
5、更新平衡因子
void RotateR(Node* parent)
{Node* cur = parent->_left;Node* curright = cur->_right;parent->_left = curright;if (curright){curright->_parent = parent;}cur->_right = parent;//将parent的父节点保存起来Node* pparent = parent->_parent;parent->_parent = cur;if (parent == _root){_root = cur;cur->_parent = nullptr;}else{if (pparent->_kv.first < cur->_kv.first){pparent->_right = cur;}else //if (pparent->_kv.first > cur->_kv.first){pparent->_left = cur;}cur->_parent = pparent;}parent->_bf = cur->_bf = 0;
}
3、右左双旋:
新节点插入较高右子树的左侧:
如下图所示:
实际上是通过一次右单旋转化为能够进行一次左单旋就成的AVL树,这种旋转叫做右左双旋
思路:
1、在插入后向上找到对应的parent和cur,之后进行右单旋把这个折线变成直线。
2、根据平衡因子找到对应左单旋的parent和cur,进行左单旋
3、更新平衡因子
当旋转完成后,因为高度存在改变,所以就存在平衡因子的改变,如上最后一个图所示,不同的平衡因子的改变根据上述的80节点的初始平衡因子有关(毕竟新增节点是存在于80的左边或者是右边)分为三种情况(如下):
1、初始平衡因子是0,这个就证明这个节点是新增的,左右双旋后平衡因子都更新为0
2、初始平衡因子是1,左右双旋后平衡因子parent,cur,curleft更新为-1,0,0
3、初始平衡因子是-1,左右双旋后平衡因子parent,cur,curleft更新为0,1,0
void RotateRL(Node* parent)
{Node* cur = parent->_right;Node* curleft = cur->_left;int bf = curleft->_bf;RotateR(cur);RotateL(parent);//控制平衡因子if (bf == 1){parent->_bf = -1;cur->_bf = 0;curleft->_bf = 0;}else if (bf == -1){parent->_bf = 0;cur->_bf = 1;curleft->_bf = 0;}else if (bf == 0){parent->_bf = 0;cur->_bf = 0;curleft->_bf = 0;}else{assert(false);}
}
4、左右双旋:
新节点插入较高左子树的右侧:
如下图所示:
实际上是通过一次左单旋转化为能够进行一次右单旋就成的AVL树,这种旋转叫做左右双旋
思路:
1、在插入后向上找到对应的parent和cur,之后进行左单旋把这个折线变成直线。
2、根据平衡因子找到对应右单旋的parent和cur,进行右单旋
3、更新平衡因子
或者是插在b上,这个和c是一样的。
当旋转完成后,因为高度存在改变,所以就存在平衡因子的改变,如上最后一个图所示,不同的平衡因子的改变根据上述的45节点的初始平衡因子有关(毕竟新增节点是存在于45的左边或者是右边)同右左双旋,分为三种情况(如下):
1、初始平衡因子是0,这个就证明这个节点是新增的,左右双旋后平衡因子都更新为0
2、初始平衡因子是1,左右双旋后平衡因子parent,cur,curleft更新为0,-1,0
3、初始平衡因子是-1,左右双旋后平衡因子parent,cur,curleft更新为1,0,0
void RotateLR(Node* parent){Node* cur = parent->_left;Node* curright = cur->_right;int bf = curright->_bf;RotateL(cur);RotateR(parent);//控制平衡因子if (bf == 1){parent->_bf = 0;cur->_bf = -1;curright->_bf = 0;}else if (bf == -1){parent->_bf = 1;cur->_bf = 0;curright->_bf = 0;}else if (bf == 0){parent->_bf = 0;cur->_bf = 0;curright->_bf = 0;}else{assert(false);}}
五、AVL树的检验:
思路:
根据平衡因子的大小来检测AVL树的合法性,
平衡因子又是通过左右子树的高度差(右子树的高度 - 左子树的高度)来进行计算的
所以就需要写获得高度的函数,和检验是否是平衡树的函数,
因为在外面不能访问根节点,所以在内部就进行嵌套进而完成。
int Height()
{return _Height(_root);
}int _Height(Node* root)
{if (root == nullptr)return 0;int leftHeight = Height(root->_left);int rightHeight = Height(root->_right);return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}bool IsBalance()
{return _IsBalance(_root);
}bool _IsBalance(Node* root)
{if (root == nullptr)return true;int leftHight = Height(root->_left);int rightHight = Height(root->_right);if (rightHight - leftHight != root->_bf){cout << "平衡因子异常:" << root->_kv.first << "->" << root->_bf << endl;return false;}return abs(rightHight - leftHight) < 2&& IsBalance(root->_left) && IsBalance(root->_right);
}
10000个随机数进行插入:
运行效率挺快的,毕竟是绝对平衡的二叉搜索树,log_2(N),
查询的时候没啥问题,很快,但是如果是插入或者删除的时候,会进行旋转,性能就可能比较低了,所以,如果是查询高效且有序的数据结构并且数据不会改变就可以考虑使用AVL树
int main()
{int N = 10000;vector<int> v;v.reserve(N);srand(time(0));for (size_t i = 0; i < N; i++){v.push_back(rand());}AVLTree<int, int> t;for (auto e : v){t.Insert(make_pair(e, e));cout << "Insert:" << e << "->" << t.IsBalance() << endl;}return 0;
}