先说说二叉搜索树: 是有序的二叉树,根值>左节点值,右节点值>根值。
如果要查找某个值,二叉搜索树和二分查找一样,每进行一次值比较,就会减少一半的遍历区间。
但是,如果树插入的值一直递增/递减,就会出现这种情况:
这样,二叉树性能就完全失去了,直接退化成了顺序表,查找效率低下。
由此,引入了能保持性能最佳的二叉搜索树。
AVL树: 具有高度平衡的二叉搜索树。
性质: 1.它的左右子树都是AVL树
2.左右子树高度差(简称平衡因子)的绝对值不超过1
搜索的时间复杂度: O(log2(n))
AVL树节点
struct AVLTree{ pair<K,V> kv; AVLTreeNode* left; AVLTreeNode* right; AVLTreeNode* parent; int bf; //balance factor };
与二叉搜索树不同的是引入了父节点(方便后面讲的旋转)和平衡因子(保持高度平衡的核心)。
AVL树的插入:
在插入节点后,需要调整平衡:
1.bf更新规则:新增在左 父亲bf-1 ; 新增在右 父亲bf+1
2.持续往祖先更新 如果 祖先bf==0,祖先肯定是从1或者-1变来的,那就说明祖先所在子树的高度不变,停止更新;
祖先|bf|==1,祖先肯定是从0变来的,说明祖先所在子树高度变了,继续往上更新;
祖先|bf|==2,祖先肯定是从1或者-1变来的,说明高度已经不平衡了,需要旋转调整;
旋转:
1.左单旋: 新结点插在较高右子树右侧 2 1
2.右单旋: 新结点插在较高左子树左侧 -2 -1
3.左右双旋(先左单旋parent,再右单旋g): 新结点插在较高左子树右侧 -2 1 -> -2 -2
4.右左双旋: 新结点插在较高右子树左侧 2 -1 -> 2 2
总结:
//右单旋 void RotateR(Node* parent) { Node* subL = parnet->left; Node* subLR = subL->right; //右旋下去,原根左孩子成为新根 parent->left = subLR; if(subLR) //LR不为空,才连接其父亲 subLR->parent = parent; //更新新根与原根的关系 subL->right = parent; //记录原根的父 Node* ppNode = parent->parent; //更新新根与原根的关系 parent->parent = subL; //下面都是因为双向链表带来的问题 //更新原根父与新根的关系 //新根就是根节点 if(ppNode==nullptr) { root = subL; root->parent = nullptr; } //新根更新与原根父的关系 else { if(ppNode->left==parent) ppNode->left = subL; else ppNode->right = subL; subL->parent = ppNode; } //根据上图,更新部分节点平衡因子 parent->bf = subL->bf = 0; } //左单旋同理 void RotateL(Node* parent);
//右左双旋 void RotateRL(Node* parent) { Node* subR = parent->right; Node* subRL = subR->left; int bf = subRL->bf; //右左双旋 RotateR(parent->right); RotateL(parent); //更新双旋后的bf //由于单旋会将bf置0,而双旋有三种情况,需要记录旋转前的新根的bf //1.新结点插入后,subRL的bf是0 --- 插入的节点就是subRL if(bf == 0) { parent->bf = subRL->bf = subR->bf = 0; } //2.新结点插入后,subRL的bf是1 --- 插入的节点在subRL右边 else if(bf == 1) { subR->bf = 0; parent->bf = -1; subRL->bf = 0; } //3.新结点插入后,subRL的bf是-1 --- 插入的节点在它subRL左边 else if(bf == -1) { parent->bf = 0; subR->bf = 1; subRL->bf = 0; } } //左右双旋同理 void RotateLR(Node* parnet);
AVL树插入代码
bool Insert(const pair<K,V>& kv) { //插入结点 if(root == nullptr) { root=new Node(kv); root->bf = 0; return true; } Node* parent = nullptr; Node* cur = root; while(cur) { if(cur->kv.first < kv.first) else if(cur->kv.first > kv.first) else return false; } cur = new Node(kv); //父节点连接插入的结点 if(parent->kv.first < kv.first) { parent->right = cur; cur->parent = parent; } else { ... } //调平衡 //1.新增在左 父亲bf-1 新增在右 父亲bf+1 //2.持续往祖先更新 if 祖先bf == 0,则祖先所在子树高度不变,停止往上更新 // if 祖先|bf| == 1,则祖先所在子树高度变了,继续往上更新 // if 祖先|bf| == 2,则祖先所在树不平衡,则旋转调整 //1.更新平衡因子 while(parent) { if(cur == parent->right) parent->bf++; else parent->bf--; //高度不变,更新完成 if(parent->bf == 0) break; //高度变了,继续更新 else if(abs(parent->bf) == 1) { cur = parent; parent = parent->parent; } //不平衡,旋转调整 else if(abs(parent->bf) == 2) { //判断旋转方式 if(parent->bf==2) { if(cur->bf==1) RotateL(parent); else if(cur->bf==-1) RotateRL(parent); } else if(parent->bf==-2) { if(cur->bf==-1) RotateR(parent); else if(cur->bf==1) RotateLR(parent); } break; } else assert(false); } }
因为引入了bf和双向链表,所以有了很多坑,应该避免以下两点:
1.单旋完成后,注意更新其新根与原根父节点的连接关系
2.双旋完成后,注意根据双旋规律进行bf更新