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