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