avl树容器c++实现及其性能测试

这个实现有个问题就是不能用于多线程,因为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

《avl树容器c++实现及其性能测试》

       增删改查用时分别为18.8s,1.8s,4.5s,1.8s,总用时27s。内存使用仅有2%,并且用于处理该进程的cpu用于io等待和system进程的时间百分比为0%,因而内存和硬盘io都不是瓶颈。而cpu占用为100%,是主要瓶颈。但是串行测试仅使用了一个cpu,下面试一下并行测试,提高cpu利用率。

       并行两个线程插入和查询1000w数据:

           《avl树容器c++实现及其性能测试》

       插入用时51.1s,查询用时55.3s,总用时55.3s。为什么并行测试使用了多个cpu反而比串行要慢?

可以看到上面top监控的数据,插入两个线程和查询两个线程所用的cpu实际应用占用时间仅有20%左右,大部分时间用在了睡眠(60%)和系统调用(20%),而这里多线程的使用的时NPTL库,是内核级线程,不会存在对多核cpu利用不足的问题。这是因为为了保证avl树是线程安全的,不得不给avl树的增删改查加上了读写锁,由于avl树的旋转特性,加锁不能仅对一个节点加锁,因此一次插入就要锁住整个树,这样两个线程并行插入,其实是一个线程(cpu0)插入时另一个线程(cpu1)在等待,完全浪费了cpu的性能,查询的两个线程同此理,虽然查询用的读锁,但是仅有写入和查询时,读锁和写锁是互斥的,因而也很糟糕。但是这个应该也不会造成性能如此糟糕,最多稍微比单线程时时间长一点,但不会这么多。我用vmstat监控了一下运行时过程发现如下所示:

      《avl树容器c++实现及其性能测试》

 在蓝线之上的是没有运行时的情况,蓝线之下的是开始运行时的情况。可以发现第一列等待线程从0变成了2左右,这是正常状况符合我的猜想,读的时候两个写线程在等待。但是看cs和in时发现上下文切换突然暴涨,一般是调用系统函数造成上下文切换,而我在增删改查操作前都加上了读写锁函数,因此每次调用这些操作时都会调用加锁函数,如果失败就会等待然后循环尝试,因此应该是等待和加锁轮询导致的大量上下文切换,从而大大降低了速度。这个问题可以通过条件变量解决,当一个锁被占用时,其他线程进入睡眠,当锁释放时通过条件变量进行唤醒,从而减少上下文切换。

       但不管怎么说对于这个avl树容器,并行测试反而大大降低了性能。

    原文作者:AVL树
    原文地址: https://blog.csdn.net/qq_38125183/article/details/80647525
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞