STL源码笔记(18)—平衡二叉树AVL(C++封装+模板)
在几年前刚学数据结构时,avl-tree只是一个仅仅需要掌握其概念的东西,今非昔比,借看stl剖析的契机希望从代码层面将其拿下。
1.简介
二叉查找树给我们带来了很多方便,但是由于其在有序序列插入时就会退化成单链表(时间复杂度退化成 o(n)),avl-tree就克服了上述困难。avl-tree是一个“加上了平衡条件的”二叉搜索树,平衡条件确保整棵树的深度为o(log n)。
avl树是最先发明的自平衡二叉查找树。在avl树中任何节点的两个子树的高度最大差别为一,所以它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下都是 o(log n)。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。
节点的平衡因子是它的左子树的高度减去它的右子树的高度(有时相反)。带有平衡因子1、0或-1的节点被认为是平衡的。带有平衡因子-2或2的节点被认为是不平衡的,并需要重新平衡这个树。平衡因子可以直接存储在每个节点中,或从可能存储在节点中的子树高度计算出来。
avl树的所有操作都与二叉查找树相同,不同的是,这里avl树需要做“avl旋转”。
2.avl旋转
avl树最重要的核心部分就是avl旋转了,这部分我的感触是,单做旋转还是挺好理解的,只不过写起代码来有点复杂,书中以插入节点为例,删除节点的部分折腾了好久。
在理解avl旋转之前,首先得知道以下几个概念:
1. avl 树节点的插入总是在叶子节点。
2. avl 树在插入节点之前总是满足平衡条件的。
3. 插入新节点后有可能满足平衡条件也有可能不满足。
4. 当不满足平衡条件后,我们就需要对新的树进行旋转。
旋转之前,我们首先要找到一个x节点,这个x节点做如下定义:
假如我们在某一个叶子节点处插入一个新的节点后,此时这棵树的某些节点的平衡性会发生变化,那么我们从叶子节点向上到根节点的路径上第一个平衡性发生变化的节点。
基于这个x节点,考虑一件事情:
这个x节点分为左右子树,左右子树又有左右子树,1分2,2分4,所以以这个x节点为根节点的话,新插入的节点可能出现的位置有:
x的左孩子节点的左子树上(left-left)
x的右孩子节点的右子树上(right-right)
x的左孩子节点的右子树上(left-right)
x的右孩子节点的左子树上(right-left)
根据上述情况就延生出了4种旋转:
1.left-left rotation
2.right-right rotation
3.left-right rotation
4.right-left rotation
前两种属于单旋转,后两种属于双旋转,双旋转的操作可以由两次单旋转组成。
ps:avl树的旋转还是得画图来理解,这里直接贴出书中的图了。
插入新节点
喎? f/ware/vc/"="" target="_blank" class="keylink">vcd4ncjxomybpzd0="平衡破坏条件">平衡破坏条件
单旋转(以left-left rotation为例)
双旋转(以left-right rotation为例)
双旋转:两次单旋转
3.avl-tree实现
avl-tree是一个二叉排序树,其基本操作也跟它类似,唯一需要注意的就是在插入,删除节点后,需要对树进行调整,让树的每个节点保持平衡。
节点的平衡因子是通过计算其左子树和右子树的差得来的,这里有两种考虑方式:
1. 每次都计算一次(递归求深度)。
2. 将平衡因子作为一个成员变量保存在节点中,平衡性发生变化的时候更新。
我采取了第1种方式,网上也有用第2种方式的,我说不上利弊,但是感觉起码掌握一种方式就可以“一招鲜”了吧。
另外,这里我用了c++类封装,为了学习还顺便使用了模板,所以类的声明和实现都放在了一个文件中,感觉内容太多,还是分开来比较好。
(1)avl-tree节点结构定义
//avlnode.h #ifndef __avlnode_h__ #define __avlnode_h__ #include #include #include template class avlnode{ public: keytype key; avlnode * left; avlnode * right; avlnode() :key(0),left(null), right(null){} avlnode(keytype k) :key(k), left(null), right(null){} }; #endif
(2)avl-tree 类声明
//avltree.h #ifndef __avltree_h__ #define __avltree_h__ #include "avlnode.h" //avl树的模板实现 template class avltree { typedef avlnode avlnode;//类型定义 private: avlnode * avlroot;//私有数据结构 int __height(const avlnode *root);//求树的高度 int __diff(const avlnode*root);//高度差(平衡因子) //avl4种旋转:左左,左右,右右,右左 //x定义为插入位置节点到根节点的路径上平衡条件被改变的节点中最深的那个节点 //x通过递归返回的方式找到 //左左:插入点位于x的左孩子节点的左子树 //左右:插入点位于x的左孩子节点的右子树 //右右:插入点位于x的右孩子节点的右子树 //右左:插入点位于x的右孩子节点的左子树 //单旋转 avlnode * __ll_rotation(avlnode *root);//left-left rotation avlnode * __rr_rotation(avlnode *root);//right-right rotation //双旋转 avlnode * __lr_rotation(avlnode *root);//left-right rotation avlnode * __rl_rotation(avlnode *root);//right-left rotation avlnode * __balance(avlnode *root);//平衡操作 avlnode * __insert(avlnode *root, const keytype &k);//插入的内部实现 //中序遍历的两种重载 void __inordertraversal(const avlnode* root);//输出 void __inordertraversal(const avlnode*root, std::vector&vec);//结果保存 bool __isleaf(avlnode* const &);//判断是否是叶子节点 bool __isnodewithtwochild(avlnode * const &);//判断是否有两个孩子 avlnode* __search(avlnode *const root, const keytype &k);//查找的内部实现 void __deletetree(avlnode * root);//删除树的所有节点 avlnode* __delete(avlnode * root, const keytype& k);//删除节点 avlnode*__treemin(avlnode *root);//求当前根节点最小(一路向左) avlnode*__treemax(avlnode*root);//求当前根节点的最大(一路向右) public: avltree(){ avlroot = null; }//默认构造函数 ~avltree();//析构函数删除树中所有节点 avltree(const std::vector&);//构造函数,容器构造 avltree(const keytype * arr, size_t len);//构造函数,数组构造 void inordertraversal();//中序遍历外部接口 void inordertraversal(std::vector&);//中序遍历外部接口重载2 bool delete(const keytype &k);//删除节点的外部接口 bool insert(const keytype & k);//插入节点的外部接口 bool isempty(){ return avlroot == null; }//树空? bool search(const keytype &k);//查询外部接口 };//class avltree //... #endif
(3)插入节点
套路与bst一样,小的往左走,大的往右走,新节点插到叶子节点处,这里只需要根据新节点的位置进行四种不同的旋转即可。
//avltree.h //插入节点的私有成员实现 template avlnode * avltree::__insert(avlnode * root, const keytype&k) { if (null == root) { root = new avlnode(k); return root; }//递归返回条件 else if (k < root->key) { root->left = __insert(root->left, k);//递归左子树 //balance operation root = __balance(root);//平衡操作包含了四种旋转 } else if (k>root->key) { root->right = __insert(root->right, k);//递归右子树 //balance operation root = __balance(root);//平衡操作包含了四种旋转 } return root; }
这里还写了一个平衡操作的函数,就把四种旋转包含进去了。
//avltree.cpp //树高 template int avltree::__height(const avlnode *root)//求树高 { if (root == null) return 0; return std::max(__height(root->left) , __height(root->right)) + 1; } //平衡因子 template int avltree::__diff(const avlnode *root)//求平衡因子,即当前节点左右子树的差 { return __height(root->left) - __height(root->right); } //平衡操作 template avlnode * avltree::__balance(avlnode *root) { int balancefactor = __diff(root);//__diff用来计算平衡因子(左右子树高度差) if (balancefactor > 1)//左子树高于右子树 { if (__diff(root->left) > 0)//左左外侧 root=__ll_rotation(root); else//左右内侧 root=__lr_rotation(root); } else if (balancefactor < -1)//右子树高于左子树 { if (__diff(root->right) > 0)//右左内侧 root=__rl_rotation(root); else//右右外侧 root=__rr_rotation(root); } return root; }
四种旋转
avl tree的核心:在刚学数据结构的时候,这部分的要求是理解即可,现在要写代码还真有点懵逼,好在双旋转可以由单旋转合成,这样也就降低了我们的工作量,只需要完成两个单旋转即可(指针操作,两个单旋转操作对称),旋转的命名以插入节点与x节点相对位置来决定。
//avltree.h //四种avl旋转 template avlnode * avltree::__rr_rotation(avlnode *root)//right-right rotation { avlnode* tmp; tmp = root->right; root->right = tmp->left; tmp->left = root; return tmp; } template avlnode * avltree::__ll_rotation(avlnode *root)//left-left rotation { avlnode * tmp; tmp = root->left; root->left = tmp->right; tmp->right = root; return tmp; } template avlnode * avltree::__lr_rotation(avlnode *root)//left-right rotation { avlnode * tmp; tmp = root->left; root->left = __rr_rotation(tmp); return __ll_rotation(root); } template avlnode * avltree::__rl_rotation(avlnode *root)//right-left rotation { avlnode * tmp; tmp = root->right; root->right = __ll_rotation(tmp); return __rr_rotation(root); }
(4)删除节点
删除节点就麻烦了,也是要分情况讨论,删除节点后,还要根据不同的情况做相应的旋转。
//avltree.h //删除节点的私有成员实现 template avlnode* avltree::__delete(avlnode *root, const keytype& k) { if (null == root) return root; if (!search(k))//查找删除元素是否存在 { std::cerr << "delete error , key not find" << std::endl; return root; } if (k == root->key)//根节点 { if (__isnodewithtwochild(root))//左右子树都非空 { if (__diff(root) > 0)//左子树更高,在左边删除 { root->key = __treemax(root->left)->key;//以左子树的最大值替换当前值 root->left = __delete(root->left, root->key);//删除左子树中已经替换上去的节点 } else//右子树更高,在右边删除 { root->key = __treemin(root->right)->key; root->right = __delete(root->right, root->key); } } else//有一个孩子、叶子节点的情况合并 { //if (!__isleaf(root)) avlnode * tmp = root; root = (root->left) ? (root->left) :( root->right); delete tmp; tmp = null; } }//end-if else if (k < root->key)//往左边删除 { root->left = __delete(root->left, k);//左子树中递归删除 //判断平衡的条件与在插入时情况类似 if (__diff(root) < -1)//不满足平衡条件,删除左边的后,右子树变高 { if (__diff(root->right) > 0) { root = __rl_rotation(root); } else { root = __rr_rotation(root); } } }//end else if else { root->right = __delete(root->right, k); if (__diff(root) > 1)//不满足平衡条件 { if (__diff(root->left) < 0) { root = __lr_rotation(root); } else { root = __ll_rotation(root); } } } return root; } //删除节点的外部接口 template bool avltree::delete(const keytype &k) { return __delete(avlroot, k)==null?false:true; }
(5)构造与析构
构造
以插入元素为基础,构造函数就简单了,我写了三个构造函数,应该足够。
1.默认构造函数
2.用容器构造
3.数组构造
//avltree.h template class avltree { typedef avlnode avlnode;//类型定义 //... public: avltree(){ avlroot = null; }//默认构造函数 avltree(const std::vector&);//构造函数,容器构造 avltree(const keytype * arr, size_t len);//构造函数,数组构造 //... }; //构造函数1-容器构造 template < typename keytype > avltree::avltree(const std::vector&vec) { avlroot = null; for (int i = 0; i < (int)vec.size(); i++) { insert(vec[i]); } } //构造函数2-数组构造 template < typename keytype > avltree::avltree(const keytype * arr,size_t len) { avlroot = null; for (int i = 0; i < (int)len; i++) { insert(*(arr + i)); } }
析构
由于节点数据结构是动态申请的,这里在程序结束后,需要手动释放,avl-tree的析构很简单,因为它本质上就是一棵二叉树么,直接按照二叉树的方式去做就ok~
template void avltree::__deletetree(avlnode *root)//删除所有节点 { if (null == root) return; __deletetree(root->left); __deletetree(root->right); delete root; root = null; return; } //析构函数 template avltree::~avltree() { __deletetree(avlroot); }
(6)avl-tree的查找与遍历
avl-tree也是二叉搜索树,其中序遍历满足升序,且不允许有相同元素。
同理,avl-tree的查找与bst一样,也是小的往左走,大的往右走,不同是,由于加了平衡条件,avltree查找的复杂度能控制在对数范围o(log n),这也是avl相比较bst在有序情况下会退化为线性表的一个最大优势了。
//avltree.h //查找内部实现 template avlnode* avltree::__search(avlnode *const root, const keytype &k) { if (null == root) return null; if (k == root->key) return root; else if (k > root->key) return __search(root->right, k); else return __search(root->left, k); } //查找外部接口 template bool avltree::search(const keytype &k) { return __search(avlroot, k) == null ? false : true; } //中序遍历内部调用(1直接打印) template void avltree::__inordertraversal(const avlnode*root) { if (null == root) return; __inordertraversal(root->left); std::cout << root->key << " "; __inordertraversal(root->right); } //中序遍历内部调用(2存入容器) template void avltree::__inordertraversal(const avlnode*root,std::vector&vec) { if (null == root) return; __inordertraversal(root->left); vec.push_back(root->val); __inordertraversal(root->right); } //中序遍历外部接口(重载版本1) template void avltree::inordertraversal() { __inordertraversal(avlroot); } //中序遍历外部接口(重载版本2) template void avltree::inordertraversal(std::vector&vec) { __inordertraversal(avlroot,vec); }
(7)测试代码
测试环境
visual studio 2013
windows 7 32bits
主要测试插入删除等函数:
//main.cpp #include "avltree.h" int main() { #if 1 std::vectorvec = { 7, 6, 5, 4, 3, 2, 1 }; avltree avl(vec); avl.insert(8); int keytofind = 9; if (avl.search(keytofind)) { std::cout << keytofind << " is found" << std::endl; } else { std::cerr << keytofind << " is not found" << std::endl; } keytofind = 4; if (avl.search(keytofind)) { std::cout << keytofind << " is found" << std::endl; } else { std::cerr << keytofind << " is not found" << std::endl; } avl.delete(4); //avl.inordertraversal(); #endif std::cout << std::endl; system("pause"); return 0; }喎?>
上一篇: 明星直播带货为何惨败:私域流量的前景如何
下一篇: Win10 不小心误删环境变量解决办法