整数集合的若干种实现

        本文用几种不同的数据结构,来实现整数集合的一些操作,比如插入、查找、遍历等。比较了它们的性能差异。实现中,应该注意两点,第一是集合的元素是不重复的,第二遍历输出时要求是按序输出。本文的代码主要参考了《编程珠玑》一书中的第13章 查找。完整代码在该书的附录部分。

         公共接口如下,接下来实现的类都是继承自这个类。

//公共接口
class IntSetImp
{
public:
	IntSetImp(int n) { maxNum=n; curNum=0; }
	virtual ~IntSetImp() {}
	virtual void Insert(int x)=0;           //插入
	virtual bool Find(int x)=0;             //查询
	virtual int Size()=0;                   //元素个数
	virtual void Report(int *visit)=0;      //遍历元素
protected:
	int maxNum;       //最大元素个数
	int curNum;       //当前个数
};

          (1)最简单的就是用C++自带的集合来实现,利用了设计模式中的适配器模式。插入和查找的时间复杂度都为O(logn)。代码如下:

//利用标准库的set实现
class IntSetSTL:public IntSetImp
{
private:
	set<int> S;
public:
	IntSetSTL(int n):IntSetImp(n) {}
	void Insert(int x)
	{
		if(S.size()<maxNum)
			S.insert(x);
	}
	int Size() { return S.size(); }
	bool Find(int x) { return (S.find(x)==S.end())?false:true;}
	void Report(int *visit)
	{
		int j=0;
		set<int>::iterator i;
		for(i=S.begin();i!=S.end();i++)
			visit[j++]=*i;
	}
};

        (2) 用数组实现,插入的时间复杂度为O(n),查找的时间复杂度为O(logn),因为是用二分查找实现查找功能的。

//整数数组实现
class IntSetArray:public IntSetImp
{
private:
	int *S;
public:
	IntSetArray(int n):IntSetImp(n) { S=new int[n];}
	~IntSetArray() { delete [ ] S; }
	int Size() {return curNum;};
	bool Find(int x) //二分查找的运用
	{
		int l,r,m;
		l=0;r=curNum;
		while(l<r){
			m=l+((r-l)>>1);
			if(S[m]<x)
				l=m+1;
			else if(S[m]==x)
				return true;
			else
				r=m;
		}
		return false;
	}
	void Insert(int x) 
	{
		if(curNum==maxNum||Find(x))  //已满或已存在
			return;
		int i;
		for(i=curNum-1;i>=0&&S[i]>x;i--) //找到插入位置
			S[i+1]=S[i];
		S[i+1]=x;
		curNum++;
	}
	void Report(int *visit)
	{
		for(int i=0;i<curNum;i++)
			visit[i]=S[i];
	}
};

         (3)用链表实现。插入和查找的时间复杂度都为O(n)

//链表实现
class IntSetList:public IntSetImp
{
	struct node
	{
		int data;
		node *next;
		node(int d=0,node *p=NULL) {data=d;next=p;}
	};
private:
	node *head; //头指针
public:
	IntSetList(int n):IntSetImp(n) { head=NULL; }
	~IntSetList() 
	{ 
		node *p=head;
		while(p!=NULL) 
		{
			node *q=p;
			p=p->next;
			delete q;
		}
	}
	int Size() {return curNum;};
	bool Find(int x) 
	{
		node *p=head;
		while(p!=NULL)
		{
			if(p->data==x)
				return true;
			p=p->next;
		}
		return false;
	}
	void Insert(int x) 
	{
		if(curNum==maxNum) 
			return;

		node *p=head,*q=NULL;
		while(p!=NULL&&p->data<x) //寻找插入位置
		{
			q=p;p=p->next;
		}
		if(p!=NULL&&p->data==x)  //已存在
			return;


		if(q==NULL)  //插在头部
			head=new node(x,p);
		else
			q->next=new node(x,p);
		curNum++;
	}
	void Report(int *visit)
	{
		int i=0;
		node *p=head;
		while(p!=NULL)
		{
			visit[i++]=p->data;
			p=p->next;
		}
	}
};

        (4)二分查找树实现。插入和查询的时间复杂度为O(logn)。

class IntSetBST:public IntSetImp
{
	struct node
	{
		int data;
		node *left;
		node *right;
		node(int d,node *r,node *l) { data=d; right=r;left=l;}
	};
private:
	node *root; //根节点
	int *tmpS;  //遍历时用到
	int tmpI;   //遍历时用到

	node* rInsert(node *p,int x)
	{
		if(p==NULL)
		{
			p=new node(x,NULL,NULL);
			curNum++;
		}
		else
		{
			if(p->data>x)
				p->left=rInsert(p->left,x);
			if(p->data<x)
				p->right=rInsert(p->right,x);
			//等于不处理
		}
		return p;
	}
	//中序遍历
	void rTraverse(node *p)
	{
		if(p==NULL)
			return;


		rTraverse(p->left);
		tmpS[tmpI++]=p->data;
		rTraverse(p->right);
	}
	void rDelete(node *p)
	{
		if(p==NULL)
			return;
		node *left=p->left;
		node *right=p->right;
		delete p;
		rDelete(left);
		rDelete(right);
	}
public:
	IntSetBST(int n):IntSetImp(n) { root=NULL;}
	~IntSetBST() { rDelete(root); }
	int Size(){return curNum;}
	bool Find(int x) 
	{
		node *p=root;
		while(p!=NULL)
		{
			if(x<p->data)
				p=p->left;
			else if(x>p->data)
				p=p->right;
			else
				return true;
		}
		return false;
	}
	void Insert(int t)
	{
		if(curNum<maxNum) 
			root=rInsert(root,t);
	}
	void Report(int *visit)
	{
		tmpS=visit;
		tmpI=0;
		rTraverse(root);
	}
};

       (5)利用位向量实现。查找与插入都可以在O(1)内实现,只是初始化的时间比较长。如果最大值比较小的话,那么非常好,空间和时间都可以接受。但是如果n很大的话,位向量需要的空间太大了。如果最大值可以是2^32 ,那么需要0.5G内存。

//位图实现
class IntSetBitVec:public IntSetImp
{
private:
	enum { BITWIDTH=32,MASK=0x1f,SHIFT=5};
	void set(int i) { S[i>>SHIFT] |= (1<< (i&MASK));}          //设置
	void clr(int i) { S[i>>SHIFT] &= ~(1<< (i&MASK));}       //复位
	int test(int i) { return S[i>>SHIFT] & (1<< (i&MASK));} //测试
	int *S;
	int maxVal; //允许的最大值
public:
	IntSetBitVec(int n,int max):IntSetImp(n) 
	{
		maxVal=max;
		S=new int [1+max/BITWIDTH];
		for(int i=0;i<max;i++)
			clr(i);
	}
	~IntSetBitVec() { delete []S; }
	int Size(){ return curNum; }
	void Insert(int t)
	{
		if(curNum==maxNum||test(t)) 
			return;

		set(t);
		curNum++;
	}
	void Report(int *visit)
	{
		int j=0;
		for(int i=0;i<maxVal;i++)
		{
			if(test(i))
				visit[j++]=i;
		}
	}
	bool Find(int x)
	{
		return test(x)?true:false;
	}
};

         (6)将位图技术与链表两种结构结合起来,其实就是散列,只不过散列函数比较特殊而已。

//结合链表和位图 散列的思想
class IntSetBins:public IntSetImp
{
	struct node
	{
		int data;
		node *next;
		node(int d,node *p) {data=d;next=p;}
	};
private:
	node **S;
	int maxVal;

public:
	IntSetBins(int n,int max):IntSetImp(n) 
	{
		maxVal=max;
		S=new node* [n];
		for(int i=0;i<n;i++)
			S[i]=NULL;
	}
	~IntSetBins() 
	{ 
		int i;
		for(i=0;i<maxNum;i++)
		{
			node *p=S[i];
			while(p!=NULL) 
			{
				node *q=p;
				p=p->next;
				delete q;
			}
		}
	}
	int Size(){ return curNum; }
	void Insert(int x)
	{
		int i=x/(1+maxVal/maxNum);

		//下面是链表的插入过程
		if(curNum==maxNum) 
			return;

		node *p=S[i],*q=NULL;
		while(p!=NULL&&p->data<x) //寻找插入位置
		{
			q=p;p=p->next;
		}
		if(p!=NULL&&p->data==x)  //已存在
			return;

		if(q==NULL)  //插在头部
			S[i]=new node(x,p);
		else
			q->next=new node(x,p);
		curNum++;
	}
	void Report(int *visit)
	{
		int i,j=0;
		for(i=0;i<maxNum;i++) //检查每个桶
		{
			for(node *p=S[i];p!=NULL;p=p->next)
				visit[j++]=p->data;
		}
	}
	bool Find(int x)
	{
		int i=x/(1+maxVal/maxNum); //散列到某个桶
		node *p=S[i];
		while(p!=NULL)
		{
			if(p->data==x)
				return true;
			p=p->next;
		}
		return false;
	}
};

       下面给出测试的驱动程序。通过调整N、MAX的值可以控制插入个数和允许的最大值

#include <iostream>
#include <algorithm>
#include <ctime>
#include <set>
using namespace std;
const int N=10000;               //整数个数
const int MAX=100000000; //最大值
int bigrand()  //产生大整数
{
	return RAND_MAX*rand()+rand();
}
void GenSets(int n,int maxval)
{
	long start,end,total=0;
	int tnum=10;
	for(int i=0;i<tnum;i++)
	{
		IntSetSTL S(n);
		//IntSetArray S(n);
		//IntSetList S(n);
		//IntSetBST S(n);
		//IntSetBitVec S(n,maxval);	
		//IntSetBins S(n,maxval);
		start=clock();
		while(S.Size()<n)
			S.Insert(bigrand()%maxval);
		end=clock();
		total+=(end-start);
	}
	cout<<"total time is : "<<total/(tnum*1000.0)<<'s'<<endl;

	//如果需要查看插入的元素,可以执行这段代码
	/*int *v=new int[n];
	S.Report(v);
	for(int j=0;j<n;j++)
	{
		cout<<v[j]<<' ';
		if(j%10==0&&j!=0)
			cout<<endl;
	}
	cout<<endl;*/
}

int main()
{
	srand(time(0));
	GenSets(N,MAX);
	return 0;
}

         当N取1000、20000、40000,MAX取100000000时,运行时间比较。这里最后两种结构未包含初始化的时间。

 N                                       10000                 20000                 40000

标准库                              0.034s                0.064s                0.145s          

数组                                  0.167s                0.667s                2.667s

链表                                  0.283s                1.156s                5.562s    

二叉查找树                      0.011s                0.023s                0.043s

位图                                  0.000s                0.003s                0.007s

位图+链表                        0.000s                0.006s                0.018s


时间复杂度对比

                                         插入                          查找

标准库                          O(logn)                  O(logn)  

数组                              O(n)                        O(logn)

链表                              O(n)                        O(n)    

二叉查找树                  O(logn)                  O(logn)

位图                              O(1)                        O(1)    

位图+链表                   O(n/m)                    O(n/m)     m为桶的个数

       在应用中,如果整数集合的最大值比较小,用位图比较好。如果最大值比较大,可以考虑用二叉树来实现。

    本人享有博客文章的版权,转载请标明出处 http://blog.csdn.net/wuzhekai1985

你可能感兴趣的:(struct,report,null,iterator,Class,insert)