【c++】AVL树模拟实现
简介
AVL树是最先被发明出来的自平衡二叉查找树,在1962由前苏联科学家G. M. Adelson-Velsky和E. M. Landis在论文中发表。AVL树中引入了平衡因子,每一个节点都有一个平衡因子(一般是右子树高度 - 左子树高度);AVL树要求左右子树高度相差不能超过1,即平衡因子只能是0,1或-1。AVL树的高度始终是log(n),n为节点数量。
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;//平衡因子balance factorAVLTreeNode(const pair<K, V>& kv):_kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _bf(0){}
};template<class K,class V>
class AVLTree
{typdef AVLTreeNode<K, V> Node;
public:private:Node* _root = nullptr;};
_kv是AVL树储存的键值对,既能存储键值key,每一个key也能对应储存一个value值。
为什么要有_parent?
AVL树通过平衡因子来控制树的平衡,插入或删除数据后,需要回溯到父节点计算平衡因子,所以我们需要_parent来储存每一个节点父节点的值。
AVL树的插入
插入的基本流程
寻找插入的位置
按照二叉树遍历的顺序找到要插入的位置。
更新平衡因子
插入一个新的节点之后,如果父节点的平衡因子发生改变,则需更新从根节点到父节点这条路径的所有节点的平衡因子。
判断是否产生不平衡
如果更新平衡因子之后,没有出现平衡因子不为0,-1,1的,则说明树是平衡的,插入结束。
如果出现了不平衡,对不平衡⼦树旋转,旋转后本质调平衡的同时,本质降低了⼦树的⾼度,不会再影响上⼀层,所以插⼊结束。
平衡因子的更新
更新的原则
更新停止的条件
插入的整体框架:
bool insert(pair<K, V>& kv){if (_root == nullptr){_root = new Node(kv);return true;}Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_kv.first < kv.first){parent = cur;cur = cur->_right;}else if (cur->_kv.first > kv.first){parent = cur;cur = cur->_left;}else{return false;//不应许插入相同的值}}cur = new Node(kv);if (cur->_kv.first < parent->_kv.first){parent->_left = cur;}else if (cur->_kv.first > parent->_kv.first){parent->_right = cur;}cur->_parent = parent;//更新平衡因子while (parent){// 更新平衡因⼦if (cur == parent->_left)parent->_bf--;elseparent->_bf++;if (parent->_bf == 0){// 更新结束break;}else if (parent->_bf == 1 || parent->_bf == -1){// 继续往上更新cur = parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2){// 不平衡了,旋转处理break;}else{assert(false);}}return true;}
旋转
旋转的原则
1.保持搜索树的规则
2.让树变平衡,降低旋转树的高度
根据插入的位置不同,旋转一共分为四种,左单旋/右单旋/左右双旋/右左双旋
右单旋
右单旋适合处理下图这种情况,插入前,左子树高度刚好比右子树高1,且刚好插入值为在左子树最左边(比原来树中的所有值都小)。
普遍情况:
为了方便理解我们这里取特例:
插入节点-3之后,树不再平衡。这时候就需要通过旋转来使树再次平衡。怎样让树再次平衡呢?
我们只需要操作最深的那一条路径就好了,让它高度减一,在移到右子树上,这样平衡因子就为0了。
虽然不符合AVL树的规则了,但是这颗树还是符合二叉搜索树的规则的。还是可以中序遍历。
这棵树中序遍历的结果是:-3,1,5,8,10,15。把10拿下来让左子树高度减一,再向办法把10放到右子树中,5的右子树绝对比它的父节点(10)小,所以可以把10变成5的右节点,同时让8接到10的左节点。这样即保证中序遍历的结果又使树恢复了平衡。
代码实现:
//右单旋void RotateR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;// 需要注意除了要修改孩⼦指针指向,还是修改⽗亲parent->_left = subLR;if (subLR)subLR->_parent = parent;Node* parentParent = parent->_parent;subL->_right = parent;parent->_parent = subL;// parent有可能是整棵树的根,也可能是局部的⼦树// 如果是整棵树的根,要修改_root// 如果是局部的指针要跟上⼀层链接if (parentParent == nullptr){_root = subL;subL->_parent = nullptr;}else{if (parent == parentParent->_left){parentParent->_left = subL;}else{parentParent->_right = subL;}subL->_parent = parentParent;}parent->_bf = subL->_bf = 0;}
要处理的三个节点
注意:parent不一定是根节点,所以要创建一个临时变量parentParent储存parent的父节点,处理好子树之后,再让subL的父节点指向parentParent(parentParent为空则,subL变为根节点)。
左单旋
左单旋适合一下情况
树中的左旋和右旋互为对称操作,左旋和右旋是可以相互抵消的,即经历一次左旋和一次右旋之后会恢复成原树。
代码实现:
void RoateL(Node* parent){Node* subR = parent->_right;parent->_right = subR;Node* subRL = subR->_left;if (subRL){subRL->_parent = subR;}Node* parentParent = parent->_parent;subR->_left = parent;parent->_parent = subR;parent->_right = subRL;if (subRL){subRL->_parent = parent;}if (parentParent){subR->_parent = parentParent;if (parentParent->_left == parent){subR->_parent = parentParent;parentParent->_left = subR;}else if(parentParent->_right == parent){subR->_parent = parentParent;parentParent->_right = subR;}}else if (parentParent == nullptr){_root = subR;}parent->_bf = subL->_bf = 0;}
左右双旋
观察下面这种情况,它并不是纯粹的左边高,对于节点10是左边高,对于节点5来说是右边高。
这时候单纯的右单旋或者左单旋都不能解决问题, 这时候两次单旋就可以解决问题。以节点5为旋转点,进行一次左单旋,这时候就是一个完全左边高的树结构,完全符合右单旋的应用场景,以节点10为旋转点进行一次右单旋,树恢复平衡。
要处理的节点:
代码实现:
void RotateLR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;RotateL(parent->_left);RotateR(parent);if (bf == 0){subL->_bf = 0;subLR->_bf = 0;parent->_bf = 0;}else if (bf == -1){subL->_bf = 0;subLR->_bf = 0;parent->_bf = 1;}else if (bf == 1){subL->_bf = -1;subLR->_bf = 0;parent->_bf = 0;}else{assert(false);}}
右左双旋
右左双旋于左右双旋类似,接下来我们以普适情况抽象出a,b,c三棵子树来讨论。另外我们需要把b⼦树的 细节进⼀步展开为12和左⼦树⾼度为h-1的e和f⼦树,因为我们要对b的⽗亲15为旋转点进⾏右单旋,右单旋需要动b树中的右⼦树。
场景二:h >= 1时,新增结点插⼊在f⼦树,f⼦树⾼度从h-1变为h并不断更新12->15->10平衡因⼦, 引发旋转,其中12的平衡因⼦为1,旋转后15和12平衡因⼦为0,10平衡因⼦为-1。
场景三:h == 0时,a/b/c都是空树,b⾃⼰就是⼀个新增结点,不断更新15->10平衡因⼦,引发旋 转,其中12的平衡因⼦为0,旋转后10和12和15平衡因⼦均为0。
代码实现:
void RotateRL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;RotateR(parent->_right);RotateL(parent);if (bf == 0){subR->_bf = 0;subRL->_bf = 0;parent->_bf = 0;}else if (bf == 1){subR->_bf = 0;subRL->_bf = 0;parent->_bf = -1;}else if (bf == -1){subR->_bf = 1;subRL->_bf = 0;parent->_bf = 0;}else{assert(false);}}
查找
按照遍历二叉树的逻辑查找即可。
Node* Find(const K& key)//按照键值查找{Node* cur = _root;while (cur){if (cur->_kv.first < key){cur = cur->_right;}else if (cur->_kv.first > key){cur = cur->_left;}else{return cur;}}return nullptr;}
AVL树平衡检测
对于实现的AVL树是否合格,我们通过判断左右子树高度差来判断,即判断平衡因子。
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 _IsBalanceTree(Node* root){// 空树也是AVL树if (nullptr == root)return true;// 计算pRoot结点的平衡因⼦:即pRoot左右⼦树的⾼度差int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);int diff = rightHeight - leftHeight;// 如果计算出的平衡因⼦与pRoot的平衡因⼦不相等,或者// pRoot平衡因⼦的绝对值超过1,则⼀定不是AVL树if (abs(diff) >= 2){cout << root->_kv.first << "⾼度差异常" << endl;return false;}if (root->_bf != diff){cout << root->_kv.first << "平衡因⼦异常" << endl;return false;}// pRoot的左和右如果都是AVL树,则该树⼀定是AVL树return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);}