《编程珠玑》笔记13 不重复集set的实现
1.问题:设计一个数据结构,能够有序存储一组整数,便于我们查找,类似与STL中的set集合
集合建立时,传入两个参数,一个是maxNumbers,表示集合元素的最大个数,另一个是maxValue,表示集合元素的范围,所有元素都小于maxValue,maxValue会被用作哨兵。
在后面的程序中,虽然设置了默认值INT_MAX,但最好不要使用,因为它可能会使程序效率很低,特别是位图法中
2.实现
2.1采用STL中的set模板实现,在实际编程中完全可以直接使用set集,这里为了与后面的实现和测试保持一致,对set进行了封装。封装类IntSetSTL如下:
1 class IntSetSTL 2 { 3 public: 4 IntSetSTL(int n = INT_MAX, int v = INT_MAX):maxNumbers(n), maxValue(v) {} 5 int insert(int t) { 6 if(t >= maxValue || s.size() >= maxNumbers) 7 return -1; 8 s.insert(t); 9 } 10 bool find(int t){ 11 if(s.find(t) == s.end()) 12 return false; 13 else 14 return true; 15 } 16 int size() { return s.size(); } 17 void report(int *arr) 18 { 19 int i = 0; 20 set<int>::iterator iter; 21 for(iter = s.begin(); iter != s.end(); iter++) 22 arr[i++] = *iter; 23 } 24 private: 25 set<int> s; 26 int maxValue; 27 int maxNumbers; 28 };
2.2采用连续数组来实现。
这种方法在在已知集合的大小时比较方便,并且实现也简单。而其最大缺点也在于:
一方面,数组大小是固定的,如果超过了这个大小,需要进行动态扩展,这种扩展的过程完全可以放在类内部来实现,外界对什么时候内部数组被扩展了是不可知的。STL就是采用的这种动态扩展方法。在《C++沉思录》中有这种动态数组的详细讲解和代码。
另一方面,由于数组是连续地址结构,对其插入和删除可能需要较长时间,在后面的习题6中是最坏时间。
1 class IntSetArray 2 { 3 public: 4 IntSetArray(); 5 IntSetArray(int n, int v): maxValue(v), maxNumbers(n), curn(0) 6 { 7 array = new int[maxNumbers + 1]; //the last position is for guard 8 array[curn] = maxValue; 9 } 10 ~IntSetArray() { delete []array; } 11 int insert(int t) 12 { 13 if(t >= maxValue || curn >= maxNumbers) 14 return -1; 15 int i, j; 16 for(i = 0; t > array[i]; i++) 17 ; 18 19 if(array[i] == t) //如果已存在,就直接返回 20 return 0; 21 for(j = curn; j >= i; j--) 22 array[j+1] = array[j]; 23 array[i] = t; 24 curn++; 25 return 0; 26 } 27 int size() { return curn; } 28 bool find(int t) 29 { 30 if(bsearch(t, 0, curn-1) == -1) 31 return false; 32 else 33 return true; 34 } 35 void report(int *arr) 36 { 37 for(int i = 0; i < curn; i++) 38 arr[i] = array[i]; 39 } 40 private: 41 int *array; 42 int maxValue; 43 int maxNumbers; 44 int curn; 45 46 int bsearch(int obj, int l, int u) 47 { 48 if(l > u) 49 return -1; 50 int mid = (l+u)/2; 51 if(array[mid] == obj) 52 return mid; 53 if(array[mid] < obj) 54 return bsearch(obj, mid+1, u); 55 else 56 return bsearch(obj, l, mid-1); 57 } 58 };
2.3 采用链表来实现
链表每个元素占用了较大的空间,但是节省插入操作的时间,(但不是O(1)时间因为要先找到待插入的位置,只是免去了移动后面元素的操作)。
1 class IntSetLink{ 2 public: 3 IntSetLink(int n = INT_MAX, int v = INT_MAX): maxNumbers(n), maxValue(v), curn(0) 4 { 5 guard = new node(maxValue, NULL); 6 head = new node(0, guard); 7 } 8 ~IntSetLink() 9 { 10 // deleteNode(head); 递归删除方法 11 // 迭代删除方法 12 node *temp = head; 13 while(temp) 14 { 15 node *n = temp; 16 temp = temp->next; 17 delete n; 18 } 19 } 20 int insert(int t) 21 { 22 if(t >= maxValue || curn >= maxNumbers) 23 return -1; 24 node * cur = head; 25 while(cur->next->val < t) //测试cur的下一个位置的值,这样我们得到的一定是仅次于t小的那个位置 26 cur = cur->next; 27 if(cur->next->val == t) 28 return 0; 29 cur->next = new node(t, cur->next); 30 curn++; 31 return 0; 32 } 33 bool find(int t) 34 { 35 node *work = head->next; 36 while(work != guard) 37 { 38 if(work->val == t) 39 return true; 40 work = work->next; 41 } 42 return false; 43 } 44 void report(int *arr) 45 { 46 int i = 0; 47 node *work = head->next; 48 while(work!= guard) 49 { 50 arr[i++] = work->val; 51 work = work->next; 52 } 53 } 54 int size() { return curn; } 55 56 private: 57 int maxNumbers; 58 int maxValue; 59 int curn; 60 61 struct node{ 62 node(int v, node *p):val(v), next(p) {} 63 int val; 64 node * next; 65 }; 66 node *head, *guard; 67 68 void deleteNode(node *t) 69 { 70 if(t->next != NULL) 71 deleteNode(t->next); 72 delete t; 73 } 74 };
2.4使用二分搜索树来实现
注意这里的二分搜索树并不是平衡树(因为对于随机产生的数据,我们认为数据也是基本均匀分布)。该结构支持快速搜索和插入。
1 class IntSetBST 2 { 3 public: 4 IntSetBST(int n = INT_MAX, int v = INT_MAX): maxNumbers(n), maxValue(v), curn(0) 5 { 6 root = NULL; 7 } 8 ~IntSetBST(){ 9 deleteTNode(root); 10 } 11 int insert(int t) //insert一定是插入到叶节点 12 { 13 if(t >= maxValue || curn >= maxNumbers) 14 return -1; 15 root = myinsert(root, t); //这里采用了递归插入的方法 16 } 17 bool find(int t) 18 { 19 return myfind(root, t); 20 } 21 int size(){ return curn; } 22 void report(int *arr) 23 { 24 int carr = 0; 25 traverse(root, arr, carr); //使用中序遍历方法将结果输出到arr中 26 } 27 private: 28 int maxNumbers; 29 int maxValue; 30 int curn; 31 32 struct tnode{ 33 tnode(int v, tnode *l, tnode *r): val(v), left(l), right(r) {} 34 int val; 35 tnode *left; 36 tnode *right; 37 }; 38 tnode *root; 39 40 void deleteTNode(tnode *t) 41 { 42 if(t->left != NULL) 43 deleteTNode(t->left); 44 if(t->right != NULL) 45 deleteTNode(t->right); 46 delete t; 47 } 48 tnode* myinsert(tnode * p, int t) //这里返回值是必须的,因为要将新建的节点添加到叶节点上是通过返回后赋值来实现的 49 { 50 if(p == NULL) 51 { 52 p = new tnode(t, NULL, NULL); 53 curn++; 54 } 55 /*如果采用不加返回值形式,实现如下(包括前面几行): 56 * if(t < p->val) 57 * { 58 * if(p->left == NULL) 59 * { 60 * p->left = new tnode(t, NULL, NULL); 61 * curn++; 62 * } 63 * else 64 * myinsert(p->left, t); 65 * }else if( t > p->val) 66 * { 67 * if(p->right == NULL) 68 * { 69 * p->right = new tnode(t, NULL, NULL); 70 * curn++; 71 * } 72 * else 73 * myinsert(p->right, t); 74 * } 75 * 显然,有很多冗余。。。 76 */ 77 if(t < p->val) 78 p->left = myinsert(p->left, t); //最终会返回第一个参数,这个返回只有当到达了叶节点后才有意义,其他情况返回值与参数值是相同的 79 else if(t> p->val) 80 p->right = myinsert(p->right, t); 81 // 如果p->val == t,那就什么都不做 82 return p; 83 } 84 85 bool myfind(tnode *p, int t) 86 { 87 if(p == NULL) 88 return false; 89 if(p->val == t) 90 return true; 91 else if(t < p->val) 92 return myfind(p->left, t); 93 else 94 return myfind(p->right, t); 95 } 96 97 void traverse(tnode *p, int * arr, int & carr) //carr要是用引用传递,才能确保递归结束arr有一致的当前下标,也可以使用全局变量 98 { 99 if(p == NULL) 100 return; 101 traverse(p->left, arr, carr); 102 arr[carr++] = p->val; 103 traverse(p->right, arr, carr); 104 } 105 };
2.5使用位向量法
对于整数而言可以使用位向量来实现这种集合结构,根据在第一章提供的函数,可以在O(1)时间内完成,插入,删除和查找。
1 class IntSetVec 2 { 3 public: 4 IntSetVec(int n = INT_MAX, int v = INT_MAX):maxNumbers(n), maxValue(v) 5 { 6 x = new int[1+maxValue/BITPERWORD]; 7 curn = 0; 8 for(int i = 0; i < maxValue; i++) 9 clr(i); 10 } 11 ~IntSetVec() { delete []x; } 12 int insert(int t) 13 { 14 if(t >= maxValue || curn >= maxNumbers) 15 return -1; 16 if(test(t)) 17 return 0; 18 set(t); 19 curn++; 20 return 0; 21 } 22 bool find(int t) 23 { 24 if(test(t)) 25 return true; 26 else 27 return false; 28 } 29 int size() { return curn; } 30 void report(int *arr) 31 { 32 int j = 0; 33 for(int i = 0; i < maxValue; i++) 34 if(test(i)) 35 arr[j++] = i; 36 } 37 private: 38 enum {BITPERWORD = 32, SHIFT = 5, MASK = 0x1f}; 39 int *x; 40 int curn; 41 int maxValue; 42 int maxNumbers; 43 void set(int i){ x[i>>SHIFT] |= (1<<(i & MASK)); } 44 void clr(int i){ x[i>>SHIFT] &= ~(1<<(i & MASK)); } 45 int test(int i){ return x[i>>SHIFT] & (1<<(i & MASK));} 46 };
2.6 使用箱来实现,具体就是使用一个元素为链表指针的数组表示一个箱,每个箱存储一定范围内元素,箱内的元素通过链表连接。
在第9章(笔记9 代码调优)是一个这样的程序,用来测量程序的性能。
用于测试这些类的主函数如下:
1 #include<iostream> 2 #include<cstdio> 3 #include<cstdlib> 4 #include<set> 5 #include<climits> 6 using namespace std; 7 8 void print(const char * info, int *data, int numbers) 9 { 10 cout << info << endl; 11 for(int i = 0; i < numbers; i++) 12 cout << data[i]<<" "; 13 cout << endl; 14 } 15 16 int generate(int numbers, int maxValue) 17 { 18 int * data = new int[numbers]; 19 /////////////////////////// 20 IntSetSTL s1(numbers, maxValue); 21 while(s1.size() < numbers) 22 s1.insert(rand()%maxValue); 23 s1.report(data); 24 print("IntSetSTL:", data, numbers); 25 cout << "s1.find(data[1]): " << s1.find(data[1]) << endl; 26 cout << "s1.find(100): " << s1.find(100) << endl; 27 28 //////////////////////////////// 29 IntSetArray s2(numbers, maxValue); 30 while(s2.size() < numbers) 31 s2.insert(rand()%maxValue); 32 s2.report(data); 33 print("IntSetArray:", data, numbers); 34 cout << "s2.find(data[1]): " << s2.find(data[1]) << endl; 35 cout << "s2.find(100): " << s2.find(100) << endl; 36 37 ////////////////////////////// 38 IntSetLink s3(numbers, maxValue); 39 while(s3.size() < numbers) 40 s3.insert(rand()%maxValue); 41 s3.report(data); 42 print("IntSetLink:", data, numbers); 43 cout << "s3.find(data[1]): " << s3.find(data[1]) << endl; 44 cout << "s3.find(100): " << s3.find(100) << endl; 45 46 //////////////////////////// 47 IntSetBST s4(numbers, maxValue); 48 while(s4.size() < numbers) 49 s4.insert(rand()%maxValue); 50 s4.report(data); 51 print("IntSetBST:", data, numbers); 52 cout << "s4.find(data[1]): " << s4.find(data[1]) << endl; 53 cout << "s4.find(100): " << s4.find(100) << endl; 54 55 //////////////////////////. 56 IntSetVec s5(numbers, maxValue); 57 while(s5.size() < numbers) 58 s5.insert(rand()%maxValue); 59 s5.report(data); 60 print("IntSetVec:", data, numbers); 61 cout << "s5.find(data[1]): " << s5.find(data[1]) << endl; 62 cout << "s5.find(100): " << s5.find(100) << endl; 63 64 } 65 66 67 int main(int argc, char **argv) 68 { 69 int numbers = atoi(argv[1]); 70 int maxValue = atoi(argv[2]); 71 generate(numbers, maxValue); 72 73 return 0; 74 }
3.原理
考虑C++标准模板库STL:
空间的重要性:在作者测试中发现,链表花费时间比数组长,这与理论不符(因为链表不需要移动),这实际上是因为链表占用内存较大。是的很多时间用于cathe与内存间数据的调入调出。
代码调优方法:1)引入哨兵,获得简单,清晰的代码;2)将递归改为迭代;3)在一开始分配一个较大的内存块。
4.习题
4.2 在插入时加上条件检测,并返回适当的标志表示此次插入是否成功。
4.3 在用数组实现的IntSet中,使用二分搜索更为高效。
4.4 对链表实现迭代版本,最好是额外定义一个头节点head,这样可以保证对于头节点的处理与后面的节点相同。若不定义,实现如下:
void insert1(node *head, int t) { if(t == head->val) return; if(t < head->val) { head = new node(t, head); curn++; return; } /////////////////对头节点进行特殊处理 node *cur = head; while(cur->next->val < t) cur = cur->next; if(cur->next->val == t) return; cur->next = new node(t, cur->next); curn++; }
书上说了一种使用指针的指针的方法来避免上面的重复:
void insert2(node *head, int t) { node **p; for(p = &head, (*p)->val < t; p = &((*p)->next)) ; if((*p)->val == t) return; *p = new node(t, (*p)); curn++; }
4.5 避免多次调用malloc,可以通过一开始就分配一块较大的内存,在后面每次插入需要申请新空间时,先查看是否有已分配好的可用空间,如果还有,就直接返回这个空间地址,如果没有就重新分配一块大内存。
如下pmalloc的实现:(调用过程与malloc完全相同,NODESIZE设为在后面最长insert的node的大小)
#define NODESIZE 8 #define NODEGROUP 1000 int nodeleft = 0; void *freenode; void *pmalloc(int size) { if(size != NODESIZE) return malloc(size); if(nodeleft == 0) { freenode = malloc(NODESIZE * NODEGROUP); nodeleft = NODEGROUP } nodeleft--; p = freenode; freenode += NODESIZE; return p; }
4.6 IntSetImp是按升序初始化了这个集合。对数组和链表会在第一个位置就插入这个元素,只要O(1)时间就可以完成。而对于二叉树或箱,这种情况会出现高度不平衡,出现最坏情况。二叉树变成单侧的,箱会使其中一个占满。