《编程珠玑》笔记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)时间就可以完成。而对于二叉树或箱,这种情况会出现高度不平衡,出现最坏情况。二叉树变成单侧的,箱会使其中一个占满。

  

 

 

 

 

posted @ 2012-09-09 19:05  dandingyy  阅读(547)  评论(0编辑  收藏  举报