这个实现有个问题就是不能用于多线程,因为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树容器,并行测试反而大大降低了性能。