一.平衡树的介绍
平衡树是以二叉树结构为基础,同时引入了平衡因子进行了限制,以保证树的结点之间的高度差小于等于1,在插入删除结点时通过旋转的方法保持高度相对平衡,从而提高搜索等效率。
二.代码实现
1.平衡树结点
平衡树结点是以二叉树为基础构建的,因此需要左右结点指针left与right。传入pair一部分为值value另一部分为关键字key,以及平衡因子bf(左子树高度减右子树高度)。
下面是结点代码:
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(const pair<K, V>& kv):_kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _bf(0){} };
2.平衡树的插入
插入结点时首先要找到合适的位置空间来放置新结点,所以我们需要遍历树。从根结点开始(cur),若值比当前结点小让cur往左边走,若值比当前结点大让cur往右边走,直到cur跑到空时停止。
接着进行判断,根据结点高度差的不同,结点位置不一样进行不同的旋转方式,调整相应的平衡因子,根据parent结点的高度决定是否要继续向上更新。
下面是寻找结点的相应代码:
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 (parent->_kv.first < kv.first){parent->_right = cur;}else{parent->_left = cur;}cur->_parent = parent;
若树的结点为空那么直接插入根结点,若不为空按照上述的方法遍历到空结点后,需要注意要确定parent的位置。
下面我们来讨论一下旋转
首先计算平衡因子,若cur在parent的左边则bf--,若cur在parent的右边则bf++,若parent的bf为0时直接跳出结果。
若parent的左右都存在结点,那么就是平衡的,反之则不平衡,若bf等于1或者-1,时继续向上更新bf平衡值,让cur等于parent,parent等于parent的parent。
向上遍历若得到bf等于2或者-2时,说明此时树已经不平衡了,需要进行旋转调整树的结构。
下面我们依次分析左旋,右旋,左右双旋,右左双旋的情况
1.右单旋
当parent的平衡因子为-2,cur的平衡因子为-1时我们需要右单旋
如图p代表parent,c代表cur此时我们需要进行右旋操作d代表插入的结点。以parent为轴向右旋转让cur变成parent。
我们需要得到parent结点,cur结点(SUL),以及cur结点的右结点(SULR)。
让SULR变成parent的左结点SUL的右结点变成parent,其他的不进行改变。
需要注意的是,首先需要判断SULR结点是否存在,若存在就让其变为parent的左结点,若不存在就不进行操作。以及我们需要找到parent结点的parent结点,若parentparent结点为空则则让root结点直接变为SUL即可,若不为空需要判断parent结点是parentparent结点的左还是右结点,然后将其左或右给给SUL。
下面放上代码:
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;}
2.左单旋
左单旋与右单旋类似,只是parent结点的左右子树互换了位置。
当parent的bf为2时,cur的bf为1时,需要进行左旋操作。
如图p代表parent ,c代表cur结点,插入的结点为a。以parent为轴向左旋转。
我们需要得到cur结点的左结点(SURL),parent结点以及cur结点(SUR)。让parent的右为SURL,SUR的左为parent。其他的不进行改变。
与上文相同需要找到parent的parent,若parentparent不存在就将root结点设置为SUR,若存在,则根据parent是parentparent的左或者右结点来分配给SUR结点。
下面是代码:
void RotateL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if (subRL)subRL->_parent = parent;Node* parentParent = parent->_parent;subR->_left = parent;parent->_parent = subR;if (parentParent == nullptr){_root = subR;subR->_parent = nullptr;}else{if (parent == parentParent->_left){parentParent->_left = subR;}else{parentParent->_right = subR;}subR->_parent = parentParent;}parent->_bf = subR->_bf = 0;}
3.左右双旋
当parent的bf等于-2,cur的bf等于1时,进行左右双旋。先对cur进行左旋让其bf等于-1,此时就回到了第一种情况,再对parent进行一次右旋就完成了操作。
当新增结点cur结点与parent结点形成一个角度时,需要进行双旋操作,如果是开口向右则进行左右双旋,若开口向左则进行右左双旋。
此处的难点在于对插入结点bf的分类讨论。
第一种情况 当SULR(R)的bf为0时
此时我们需要先对SUL进行左旋操作,使其满足三个点连成一条直线,之后再对parent进行右旋操作,得到下图。
我们可以根据最终结果直接写出代码,先对SUL进行左旋再对parent进行右旋,最后设置三个结点的bf都为0即可。
第二种情况 当SULR(R) 的bf为-1时
当我们进行左右双旋操作后
变成如图,此时需要将parent的bf设置为1,SUL和SULR设置为0。
第三种情况 当SURL(R)的bf为1时
进行左右双旋操作之后
将SUL的bf设置为-1,parent与SULR的bf设置为0即可。
下面附上代码:
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 = -1;parent->_bf = 0;subLR->_bf = 0;}else if (bf == -1){subL->_bf = 0;subLR->_bf = 0;parent->_bf = 1;}else{assert(false);}}
4.右左双旋
右左双旋本质与左右双旋一样,只是左右子树位置互换
所以我就直接附上代码:
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){subRL->_bf = 0;parent->_bf = -1;subR->_bf = 0;}else if (bf == -1){subLR->_bf = 0;subR->_bf = 1;parent->_bf = 0;}else{assert(false);}}
5.插入代码总结
bool insert(const 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 (parent->_kv.first < kv.first){parent->_right = cur;}else{parent->_left = 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){//旋转处理不平衡if (parent->_bf == 2 && cur->_bf == 1)//左单旋{RotateL(parent);}else if (parent->_bf == 2 && cur->_bf == -1)//右左双旋{RotateRL(parent);}else if (parent->_bf == -2 && cur->_bf == 1)//左右双旋{RotateLR(parent);}else if(parent->_bf == -2 && cur->_bf == -1)//右单旋{RotateR(parent);}break;}else{//说明传入的有问题报错日志assert(false);}}return true;}//右单旋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;}//左单旋void RotateL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if (subRL)subRL->_parent = parent;Node* parentParent = parent->_parent;subR->_left = parent;parent->_parent = subR;if (parentParent == nullptr){_root = subR;subR->_parent = nullptr;}else{if (parent == parentParent->_left){parentParent->_left = subR;}else{parentParent->_right = subR;}subR->_parent = parentParent;}parent->_bf = subR->_bf = 0;}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 = -1;parent->_bf = 0;subLR->_bf = 0;}else if (bf == -1){subL->_bf = 0;subLR->_bf = 0;parent->_bf = 1;}else{assert(false);}}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){subRL->_bf = 0;parent->_bf = -1;subR->_bf = 0;}else if (bf == -1){subLR->_bf = 0;subR->_bf = 1;parent->_bf = 0;}else{assert(false);}}
3.键值查找 树的高度计算 与平衡树判断
查找值时,只需要遍历整个树即可,以根节点(cur)为起始点,当所找的值比cur小时向左树寻找,比cur大时向右树寻找,直到cur值等于寻找值时返回当前结点,若cur为空了仍未找到结点就返回空。
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;}
计算树的高度时我们采用递归左右子树的方法实现,判断左右子树的大小取大值,逐层递归得到最终值。
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;}
判断是否平衡时,我们可以调用左右子树的高度,然后相减,得到diff。若diff大于2说明高度异常,若根结点的bf不等于diff说明平衡因子计算存在错误。
bool _IsBalanceTree(Node* root){if (nullptr == root)return true;int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);int diff = leftHeight - rightHeight;if (abs(diff) >= 2){cout << root->_kv.first << "高度异常" << endl;}if (root->_bf != diff){cout << root->_kv.first << "平衡因子异常" << endl;return false;}return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);}
三.总结
平衡树要求任意节点的左右子树高度差绝对值不超过 1,且左右子树本身也是平衡树。通过限制树的高度(保持在 O(logn) 级别),确保查找、插入、删除等操作的时间复杂度稳定在 O(logn),避免退化为链表的 O(n) 复杂度。每次插入或删除后通过旋转(左旋、右旋、左右双旋、右左双旋)立即恢复平衡。
它的优点在于时间复杂度低,适合大量数据的操作,缺点在于需要动态的更新数据进行旋转增大了时间开销。