这个实现有个问题就是不能用于多线程,因为avl旋转的特性一次需要锁住整棵树。
话不多说直接上代码:
/* Created on 2018/5/19 author:HZL * 该AVL树容器不带迭代器(偷懒),并且由于MinGW不支持SGI STL, * 故无法使用其内置的高效内存管理器alloc,而是仅仅封装了一下 * new和delete。同时由于关键的插入和删除函数使用递归方式实现 * 因而不适用于处理大量数据。 * */ #ifndef _AVLTREE_H #define _AVLTREE_H #include <cmath> //#include <allocator.h> #include <memory> #include <bits/stl_pair.h> #include <functional> #include <queue> using namespace std; template<typename Key,typename Value> struct AVLTreeNode { pair<Key,Value> keyValue; AVLTreeNode * leftNode; AVLTreeNode * rightNode; int height; AVLTreeNode():keyValue(pair<Key,Value>(Key(),Value())), leftNode(nullptr),rightNode(nullptr),height(0){} AVLTreeNode(const pair<Key,Value>&val,AVLTreeNode* l,AVLTreeNode*r,int h=0): keyValue(val),leftNode(l),rightNode(r),height(h){} }; template<typename Key,typename Value,typename Comparator=less<Value> > // typename Allot=alloc> class AVLTree{ public: typedef pair<Key,Value> value_type; typedef pair<Key,Value>* pointer; typedef pair<Key,Value>& reference; typedef size_t size_type; typedef ptrdiff_t difference_type; typedef AVLTreeNode<Key,Value>* nodePtr; typedef AVLTreeNode<Key,Value> treeNode; protected: //typedef simple_alloc<treeNode> nodeAllocator; protected: //nodePtr getNode(){ return nodeAllocator::allocate();} //将分配空间和构造对象分离 //void putNode(nodePtr delNode){ nodeAllocator::deallocate(delNode);} nodePtr createNode(const value_type& val ){ //nodePtr tempNode = getNode(); nodePtr tempNode = new AVLTreeNode<Key,Value>(val,nullptr, nullptr); ++treeSize; return tempNode; } nodePtr cloneNode(nodePtr val){ nodePtr tempNode = createNode(val->keyValue); return tempNode; } void destroyNode(nodePtr delNode){ //destroy(&(delNode->keyValue)); // putNode(delNode); delete delNode; --treeSize; } protected: size_type treeSize; nodePtr root; Comparator comp; public: AVLTree():treeSize(0),root(nullptr),comp(Comparator()){} ~AVLTree(){clear();} private: //禁用拷贝构造函数以及赋值函数 AVLTree(AVLTree<Key,Value,Comparator>& clone){} AVLTree<Key,Value,Comparator>& operator=(const AVLTree<Key,Value,Comparator>& clone){} public: size_type size(){ return treeSize;} nodePtr begin(){return leftMost(root);} nodePtr end(){return nullptr;} //匹配STL规范 bool empty(){ return treeSize==0;} inline nodePtr leftMost(nodePtr p){ //该节点下最小节点 if(p==nullptr) return p; while(p->rightNode!=nullptr) p = p->rightNode; return p; } inline nodePtr rightMost(nodePtr p){ //该节点下最大节点 if(p==nullptr) return p; while(p->leftNode!=nullptr) p = p->leftNode; return p; } inline int height(nodePtr p)const{ return p==nullptr?-1:p->height;} inline void insert(const value_type& val){_insert(root,val);} inline void erase(const Key& k){_erase(root,k);} void clear(){ if(root==nullptr) return; queue<nodePtr> nodes; nodes.push(root); nodePtr p; while(!nodes.empty()){ p = nodes.front(); if(p->leftNode!=nullptr) nodes.push(p->leftNode); if(p->rightNode!=nullptr) nodes.push(p->rightNode); nodes.pop(); destroyNode(p); } root = nullptr; } Value& operator[](const Key& k){ nodePtr p; if((p=find(k))!=nullptr) return (p->keyValue).second; else{ _insert(root,pair<Key,Value>(k,Value())); return (find(k)->keyValue).second; } } nodePtr find(const Key& k); private: void _insert(nodePtr& p,const value_type& val); nodePtr _erase(nodePtr& p,const Key& k); void rotateLeft(nodePtr& p); void rotateRight(nodePtr& p); void doubleRotateLeft(nodePtr& p){ rotateRight(p->leftNode); rotateLeft(p); } void doubleRotateRight(nodePtr& p){ rotateLeft(p->rightNode); rotateRight(p); } }; template<typename Key,typename Value,typename Comparator> void AVLTree<Key,Value,Comparator>::_insert( typename AVLTree<Key,Value,Comparator>::nodePtr& p, const pair<Key,Value>& val){ if( p==nullptr ){ p = createNode(val); }else if( comp(val.first , (p->keyValue).first) ){ _insert(p->leftNode,val); if( height(p->leftNode) - height(p->rightNode) == 2 ) if( comp(val.first , (p->leftNode->keyValue).first) ) rotateLeft(p); else doubleRotateLeft(p); }else if( comp( (p->keyValue).first,val.first) ){ _insert(p->rightNode,val); if( height(p->rightNode) - height(p->leftNode) == 2 ) if( comp( (p->rightNode->keyValue).first ,val.first)) rotateRight(p); else doubleRotateRight(p); }else ; p->height = max( height(p->leftNode),height(p->rightNode)) +1; } template <typename Key,typename Value,typename Comparator> typename AVLTree<Key,Value,Comparator>::nodePtr AVLTree<Key,Value,Comparator>::_erase(nodePtr& p,const Key& k){ if( p==nullptr) return p; if( comp(k,(p->keyValue).first)){ p->leftNode = _erase(p->leftNode,k); if( height(p->rightNode)-height(p->leftNode)==2 ) if( height(p->rightNode->leftNode)<=height(p->rightNode->rightNode) ) rotateRight(p); else doubleRotateRight(p); }else if( comp((p->keyValue).first,k) ){ p->rightNode = _erase(p->rightNode,k); if( height(p->leftNode)-height(p->rightNode)==2 ) if( height(p->leftNode->leftNode)>=height(p->rightNode->rightNode) ) rotateLeft(p); else doubleRotateLeft(p); }else{ if((p->leftNode!=nullptr)&&(p->rightNode!=nullptr)){ if( height(p->leftNode)>height(p->rightNode) ){ nodePtr maxNode = rightMost(p->leftNode); p->keyValue = maxNode->keyValue; p->leftNode = _erase(p->leftNode,(maxNode->keyValue).first); }else{ nodePtr minNode = leftMost(p->rightNode); p->keyValue = minNode->keyValue; p->rightNode = _erase(p->rightNode,(minNode->keyValue).first); } }else{ nodePtr p1 = p; p = p->leftNode!=nullptr?p->leftNode:p->rightNode; destroyNode(p1); } } if( p!=nullptr) p->height = max(height(p->leftNode),height(p->rightNode))+1; return p; } template <typename Key,typename Value,typename Comparator> typename AVLTree<Key,Value,Comparator>::nodePtr AVLTree<Key,Value,Comparator>::find(const Key& k){ nodePtr p1 = root; while(p1!=nullptr){ if( comp(k,(p1->keyValue).first) ){ p1 = p1->leftNode; }else if(comp((p1->keyValue).first,k)){ p1 = p1->rightNode; }else{ break; } } return p1; } template<typename Key,typename Value,typename Comparator> void AVLTree<Key,Value,Comparator>::rotateLeft( typename AVLTree<Key,Value,Comparator>::nodePtr& p){ nodePtr p1 = p->leftNode; p->leftNode = p1->rightNode; p1->rightNode = p; p->height = max( height(p->leftNode),height(p->rightNode) )+1; p1->height = max( height(p1->leftNode),height(p1->rightNode) )+1; p = p1; } template<typename Key,typename Value,typename Comparator> void AVLTree<Key,Value,Comparator>::rotateRight( typename AVLTree<Key,Value,Comparator>::nodePtr& p){ nodePtr p1 = p->rightNode; p->rightNode = p1->leftNode; p1->leftNode = p; p->height = max( height(p->leftNode),height(p->rightNode) )+1; p1->height = max( height(p1->leftNode),height(p1->rightNode) )+1; p = p1; } #endif
下面在单线程环境下进行了串行测试:
/* Created on 2018/5/19 author:HZL * 对AVLTree进行了1000000级别的增删改查测试, * 可以看出虽然AVL树的插入和删除操作复杂了很多, * 但是随着数据量的增大,耗时依然与查询维持着10:1 * 的比例,因而仍是O(logN)的时间复杂度。 * */ #include <iostream> #include <ctime> #include <cstdlib> #include <limits.h> #include "AVLTree.h" static const int COUNT = 1000000; using namespace std; int main(){ AVLTree<int,int> testTree; clock_t startTime,endTime; startTime = clock(); for(int i=0;i<COUNT;i++){ int temp = rand()%INT_MAX; testTree.insert(pair<int,int>(temp,temp)); } endTime = clock(); cout<<"Insert "<<COUNT<<" numbers cost "<<endTime-startTime<<" ms"<<endl; startTime = clock(); for(int i=0;i<COUNT;++i) testTree.find(i); endTime = clock(); cout<<"Select "<<COUNT<<" numbers cost "<<endTime-startTime<<" ms"<<endl; startTime = clock(); for(int i=0;i<COUNT;++i) testTree[i] = 1007; endTime = clock(); cout<<"Update "<<COUNT<<" numbers cost "<<endTime-startTime<<" ms"<<endl; startTime = clock(); testTree.clear(); endTime = clock(); cout<<"Delete "<<COUNT<<" numbers cost "<<endTime-startTime<<" ms"<<endl; cin.get(); return 0; }
由于原始的查找二叉树可能会因为顺序插入等问题导致链表化,从而导致性能大大降低,而红黑树数据结构过于复杂,因而设计了AVL树作为测试对象。
测试环境:操作系统:Windows 10
CPU: I7 – 6700HQ
内存:8G
硬盘:128GSSD+1T机械硬盘
测试对AVL进行了插入1000000个随机数据,以及1000000次的查找、修改和删除操作。测试结果如下:
Insert |
Select |
Update |
Delete |
800 |
112 |
1057 |
126 |
763 |
123 |
1080 |
129 |
763 |
115 |
1083 |
129 |
708 |
112 |
1092 |
130 |
698 |
112 |
1084 |
129 |
691 |
115 |
1072 |
125 |
701 |
116 |
1085 |
132 |
测试结果单位为ms。四种操作平均值分别为:732ms,115ms,1079ms,128ms。之所以修改操作耗时最长是因为修改操作重载的operator[]集合了查询和插入两个操作,即先查询是否存在该数据,若不存在则先创建再返回数据。
减少或者增加测试集数量后增改和查删的时间比依然维持在9:1左右,因而证明虽然增改操作较为复杂,但是依然是O(logN)的时间复杂度。
对其尝试了一下并行测试:
这次测试把环境换到了debian8上。
测试环境:操作系统:debian8
CPU: I7 – 6700HQ
内存:8G
硬盘:128GSSD+1T机械硬盘
首先对串行增删改查1000W数据测试进行了观测:top -d 1
增删改查用时分别为18.8s,1.8s,4.5s,1.8s,总用时27s。内存使用仅有2%,并且用于处理该进程的cpu用于io等待和system进程的时间百分比为0%,因而内存和硬盘io都不是瓶颈。而cpu占用为100%,是主要瓶颈。但是串行测试仅使用了一个cpu,下面试一下并行测试,提高cpu利用率。
并行两个线程插入和查询1000w数据:
插入用时51.1s,查询用时55.3s,总用时55.3s。为什么并行测试使用了多个cpu反而比串行要慢?
可以看到上面top监控的数据,插入两个线程和查询两个线程所用的cpu实际应用占用时间仅有20%左右,大部分时间用在了睡眠(60%)和系统调用(20%),而这里多线程的使用的时NPTL库,是内核级线程,不会存在对多核cpu利用不足的问题。这是因为为了保证avl树是线程安全的,不得不给avl树的增删改查加上了读写锁,由于avl树的旋转特性,加锁不能仅对一个节点加锁,因此一次插入就要锁住整个树,这样两个线程并行插入,其实是一个线程(cpu0)插入时另一个线程(cpu1)在等待,完全浪费了cpu的性能,查询的两个线程同此理,虽然查询用的读锁,但是仅有写入和查询时,读锁和写锁是互斥的,因而也很糟糕。但是这个应该也不会造成性能如此糟糕,最多稍微比单线程时时间长一点,但不会这么多。我用vmstat监控了一下运行时过程发现如下所示:
在蓝线之上的是没有运行时的情况,蓝线之下的是开始运行时的情况。可以发现第一列等待线程从0变成了2左右,这是正常状况符合我的猜想,读的时候两个写线程在等待。但是看cs和in时发现上下文切换突然暴涨,一般是调用系统函数造成上下文切换,而我在增删改查操作前都加上了读写锁函数,因此每次调用这些操作时都会调用加锁函数,如果失败就会等待然后循环尝试,因此应该是等待和加锁轮询导致的大量上下文切换,从而大大降低了速度。这个问题可以通过条件变量解决,当一个锁被占用时,其他线程进入睡眠,当锁释放时通过条件变量进行唤醒,从而减少上下文切换。
但不管怎么说对于这个avl树容器,并行测试反而大大降低了性能。