《编程珠玑》--第一章 开山

探讨在一兆字节(MB)的有限内存空间中,如何高效地对一千万个整数进行排序的方法。介绍了位图表示法的应用,以及通过多次遍历优化内存使用的策略。

位图表示法:输入范围小,不包含重复数据,没有数据与单个整数以外的每一记录相关联。


题目:

       如何在1MB的空间里面对一千万个整数进行排序?并且每个数都小于1千万。实际上这个需要1.25MB的内存空间(这里所说的空间是考虑用位图表示法时,每一位代表一个数,则1千万/(1024*1024*8) 约为1.25MB  )。

       1MB总共有8388608个可用位。所以估计也可以在1MB左右的空间里面进行排序了。

分析:

        1)基于磁盘的归并排序(耗时间)

        2)每个号码采用32位整数存储的话,1MB大约可以存储250 000 个号码,需要读取文件40趟才能把全部整数排序。(耗时间)

        3)位图法,采用一个1千万位的字符串表示每个数,比如{0,2,3}表示为   1  0 1 1 0 0 0 0 。(说明:左边第一位表示 0 第二位表示1 第三位表示 2 。如果有则表示为1,否则为0)遍历每一个整数,有则标记为1,否则标记为0。然后按顺序输出每个整数。这种方法实际需要1.25MB内存,如果可以方便弄到内存的话可以采用此种方法。

        4)假如严格限制为1MB,可以采用的策略:两次遍历或者除去第一位为0或1的整数。

         解释:1.考虑到没有以数字0或1开头的电话号码,可以省略对这些数的操作。

                  2.两次遍历:对  1 ---4999 999之间的数排序,需要存储空间为:5000 000/8 =625 000 字节(8为一个字节中的位数)

                                   对 5000 000 -10000 000 之间的数排序。

                  3.如果需要采用k趟算法,方法类似。

源码:

[html] view plaincopy
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3.   
  4. #define SHIFT 5  
  5. #define MASK 0x1F  
  6. #define N 10000000  
  7. int a[1 + N/32];  
  8. void set(int i) {        a[i>>SHIFT] |=  (1<<(i & MASK)); }  
  9. void clr(int i) {        a[i>>SHIFT] &= ~(1<<(i & MASK)); }  
  10. int  test(int i){ return a[i>>SHIFT] &   (1<<(i & MASK)); }  
  11. int main()  
  12. {       int i = 0;  
  13.         //int  top = 1 + N/BITSPERWORD;  
  14.         //memset(a, 0, sizeof(a)*sizeof(int));  
  15.         while (scanf("%d", &i))   
  16.             set(i);  
  17.         for (i = 0; i < N; i++)  
  18.                 if (test(i))   
  19.            printf("%d\n", i);  
  20.         return 0;  
  21. }  

课后的题目:

1、使用库函数来进行排序

[html] view plaincopy
  1. #include <stdio.h>    
  2. #include <stdlib.h>    
  3. #define ms 1025    
  4. int a[ms];    
  5. int cmp(const void *a, const void *b)    
  6. {    
  7.     return (*(int*)a) - (*(int*)b);    
  8. }    
  9. int main(void)    
  10. {    
  11.     int n, i;    
  12.     while (scanf("%d", &n) != EOF)    
  13.     {    
  14.         for (i = 0; i < n; ++i) scanf("%d", &a[i]);                          
  15.         qsort(a, n, sizeof(int), cmp);    
  16.         for (i = 0; i < n - 1; ++i) printf("%d ", a[i]);    
  17.         printf("%d\n", a[i]);    
  18.     }    
  19.     return 0;    
  20. }    

使用set容器

[html] view plaincopy
  1. #include <iostream>  
  2. #include <set>  
  3. using namespace std;  
  4. int main()  
  5. {  
  6.     set<int> S;  //STL容器内部采用红黑树作为排序数据结构  
  7.     int i;  
  8.     set<int>::iterator j;  
  9.     while(cin>>i)  
  10.         S.insert(i);  
  11.     for(j=S.begin();j!=S.end();++j)  
  12.         cout<<*j<<endl;  
  13.     return 0;  
  14. }  

2、使用位运算

[html] view plaincopy
  1. void set(int i) {  a[i>>SHIFT] |=  (1<<(i & MASK)); }    
  2. void clr(int i)  {  a[i>>SHIFT] &= ~(1<<(i & MASK)); }    
  3. int  test(int i) {   return a[i>>SHIFT] &   (1<<(i & MASK)); }    
3、比较位图排序与系统排序

       位图排序是最快的,针对这个问题而言,qsort比stl sort速度快。

4、随机生成[0, n)之间不重复的随机数(关键思想为:先把所有可能数顺序放到数组,然后打乱顺序,则保证不重复)

[html] view plaincopy
  1. for (i = 0; i < n; ++i)     
  2.     a[i] = i;    
  3. for (i = 0; i < n; ++i)    
  4. {    
  5.     pos = rand()%(n - i) + i;    
  6.     t = a[i]; a[i] = a[pos]; a[pos] = t;    
  7. }    
5、如果1MB是严格控制的空间,如果数据有1.25MB的bit数目。那么应该是需要读取2次。

        k = 需要跑几趟直接用     需要排序的数据量/内存空间bit数,往上取整则可。

        时间开销 = kn

        空间开销 n/k

        注意的是,每次在扫描的时候,取数据的范围是不一样的。

6、如果每个数据出现最多10次,那么需要4个bit位来刻录一个数。这时存储空间减小至原来的1/4。

       那么如果一定要按照bitmap的方式来进行处理,则需要利用5题中的结论。

7、问题:[R. Weil]本书1.4 节中描述的程序存在一些缺陷。首先是假定在输入中没有出现两次的整数。如果某个数出现超过一次的话,会发生什么?在这种情况下,如何         修改程序来调用错误处理函数?当输入整数小于零或大于等于n时,又会发生什么?如果某个输入不是数值又如何?在这些情况下,程序该如何处理?程序还应该包含 哪些明智的检查?描述一些用以测试程序的小型数据集合,并说明如何正确处理上述以及其他的不良情况。

      如果某个数出现超过一次的话,会发生什么?

      会被忽略掉, 因为原来的程序本身就是用来处理只出现一次的情况的。

      在这种情况下,如何修改程序来调用错误处理函数?

[html] view plaincopy
  1. while (scanf("%d", &i) != EOF)    
  2.     if(test(i)) call_error_fun();    
  3.     else set(i);    

当输入整数小于零或大于等于n时,又会发生什么?

      会出现访问越界的情况。-1访问时,会访问a[-1]的31个bit位。

如果某个输入不是数值又如何?在这些情况下,程序该如何处理?

    输入可能是浮点数,或是字符什么的~~

   可以先读入字符串,再用atoi转换成为整形数,如果失败,则进行出错处理。

    程序还应该包含哪些明智的检查?

8、免费电话号码至少有800,878,888等,那么如何查看一个号码是否是免费号码。?

第一种方案:如果是一千万个电话号码都有可能成为免费号码,那么至需要1.25MB * (免费号码前缀个数)。

第二种方案:省空间,多次扫描文件:

                  1、首先扫描整个文件,看有哪个免费号码前缀。以及每个免费号码前缀下的号码个数。

                  2、设置区间映射表:比如800前缀有125个免费号码,找到最大的数,与最小的数,差值做为bit长度。

第三种方案:建立索引的方式来进行处理。以最后7位为索引,后面800,878什么的,为值。如果不是免费号码,应该是不用加入到这个hash表中。

9、避免初始化问题

做法是:使用两个等长的辅助数组,比如要把a[n]初始化,那么在第一次访问时:

     b[i] = top;

    c[b[i]] = i;

    ++top;

给出示例代码

[html] view plaincopy
  1. #include<stdio.h>    
  2. #include<stdlib.h>    
  3. #include<string.h>    
  4.     
  5. #define ms 100    
  6. int a[ms];    
  7. int b[ms];    
  8. int c[ms];    
  9. int top;    
  10.     
  11. //判断是否被初始化过。    
  12. bool is_init(int i)    
  13. {    
  14.     return (b[i] < top && c[b[i]] == i);    
  15. }    
  16. int main(void)    
  17. {    
  18.     top = 0;    
  19. //这里生成一些随机数值。    
  20.         for (int i = 0; i < ms; ++i)    
  21.         a[i] = b[i] = c[i] = i;    
  22.     for (int i = 0; i < ms; ++i)    
  23.     {    
  24.         if (is_init(i))    
  25.         {    
  26.             printf("error");    
  27.         }    
  28.         int v = i + rand()%(ms - i + 1);    
  29.         int t = a[i]; a[i] = a[v]; a[v] = t;    
  30.     
  31.         v = i + rand()%(ms - i + 1);    
  32.         t = b[i]; b[i] = b[v]; b[v] = t;    
  33.     
  34.         v = i + rand()%(ms - i + 1);    
  35.         t = c[i]; c[i] = c[v] ; c[v] = t;    
  36.     }    
  37.     
  38.     for (int i = 0; i < ms; ++i)    
  39.     {    
  40.         if (is_init(i) == false)    
  41.         {    
  42.             a[i] = i;    
  43.             b[i] = top;    
  44.             c[top++] = i;    
  45.         }    
  46.     }    
  47.     
  48.     for (int i = 0; i < ms; ++i)    
  49.     {    
  50.         if (!is_init(i))    
  51.         {    
  52.             printf("error: %d\n", i);    
  53.         }    
  54.     }    
  55.     return 0;    
  56. }    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值