编程珠玑 学习笔记1.1

刚开始看《编程珠玑》,将网上的诸多资料整理于此,便于自己学习总结,也便于广大朋友相互交流。

位图的应用

编程珠玑 Chapter1

位图位向量图作为一个集合,表示的这样的一个数据结构:

          用字符串 0 1 1 1 0 1 0 0 1 0 0 0 0 1 0 0 0 0 0 0 表示集合 {1,2,3,5,8,13}.

    位图的应用需要数据有如下的特性:

         1.输入数据限制在相对较小的范围内;

         2.数据没有重复;

         3.除了单一整数外,没有任何其他关联数据.

    但很可惜的是,大多数待排序数据没有这些特性(就是说这些特性在大多数情况下是很难满足的).

习题1 如果不缺内存,如何使用一个具有库的语言来实现一种排序算法以表示和排序集合

解题报告:

拥有库的语言,C/C++/JAVA都是很好的选择,由于目前只会C++,因此对于我来说别无选择. 而对于C++,实现排序的库有太多的选择,典型的有STL中的stdlib.h中的qsort和algorithm中的sort.对于这两者的区别,在这里不想多谈(其实我并不清楚,大概了解的是stdlib是C的产物,而algorithm的后代,不过,sort在使用上比较简单).

  1. #include <algorithm>  
  2.   
  3. #include <iostream>  
  4.   
  5. #include <vector>  
  6.   
  7. using namespace std;  
  8.   
  9. int main(){  
  10.   
  11. vector<int> a;  
  12.   
  13. for(int i=0;i<=5;i++) a.push_back(i-5);  
  14.   
  15. sort(a.begin(),a.end());  
  16.   
  17. for(int i=0;i<5;i++) cout<<a[i]<<'' '';  
  18.   
  19. return 0;  
  20.   
  21. }  

习题二 如何使用位逻辑运算(例如与、或、移位)来实现位向量

解题报告:

一开始当然没有想到要用位逻辑运算来实现位向量,而是用十进制来实现(这是理所当然的事)。

用一个一维数组a[10000000]来存储至多1E7个号码,考虑整数m,一旦发现这个号码,根据我们的算法,应当置a[m]=1.

好的,一切看起来都如此完美,简单的算法,出色的时间效率,差强人意的空间效率.但上机起来就不是这回事了:

  1. #include <fstream>  
  2.   
  3. #include <iostream>  
  4.   
  5. using namespace std;  
  6.   
  7. int main(){  
  8.   
  9. ifstream in;  
  10.   
  11. ofstream outt;  
  12.   
  13. in.open("c:/project/out.txt");  
  14.   
  15. outt.open("c:/project/outt.txt");  
  16.   
  17. bool a[10000000];  
  18.   
  19. for(int i=0;i<10000000;i++) a[i]=false;  
  20.   
  21. int m;  
  22.   
  23. for(int i=0;i<1000000;i++)   
  24.   
  25. {in>>m; a[m]=true;}  
  26.   
  27. for(int i=0;i<10000000;i++)   
  28.   
  29. if(a[m]==true) outt<<m<<endl;  
  30.   
  31. in.close();  
  32.   
  33. outt.close();  
  34.   
  35. return 0;  
  36.   
  37. }  

但是美好的想法在现实面前是如此的脆弱,这段代码在运行的时候出错了,原因是数组越界.好吧,现在我可以承认,数组开到1E7是不现实的,这该如何是好?

现在是时候回到位逻辑运算了,这是一种模仿计算机底层二进制运算的运算方法,十分高效,但是第一次看上去会显得晦涩难懂,等到将它与十进制运算联系起来后,会发现它相当有用.

整个的思想是,a[10000000]显得太过巨大的原因是每一个元素a[i]只保留了一个bool值或者是一个整型值0或1,如果我们把每一个元素包含的内容扩充,使之保留尽可能多的号码是否存在的信息,那么数组范围会得到明显的下降.

事实上,我们是用每一个元素表示一个32位的二进制字符串,这样这个元素可以保留相邻32个号码是否存在的信息,数组范围就下降到10000000/32了.例如对于号码89256,由于89256 mod 32=2789…8,这样我们应该置a[2789]中32位字符串的第8位(从低位数起)为1.

现在问题的关键是,如何用位逻辑运算来表示这种操作. 关于位逻辑运算的知识,你应当去参考手头的C++教材,因为在这里我无法讲的比教材更好:

  1. #define WORD 32  
  2.   
  3. #define SHIFT 5 //移动5个位,左移则相当于乘以32,右移相当于除以32取整  
  4.   
  5. #define MASK 0x1F //六进制下的31  
  6.   
  7. #define N 10000000  
  8.   
  9. //置位函数——用"|"操作符,i&MASK相当于mod操作  
  10.   
  11. //m mod n 运算,当n = 2的X次幂的时候,m mod n = m&(n-1)  
  12.   
  13. void set(int i)  
  14.   
  15. {a[i>>SHIFT]|=(1<<(i&MASK));}  
  16.   
  17. //清除位操作,用&~操作符  
  18.   
  19. void clear(int i)  
  20.   
  21. {a[i>>SHIFT]&=~(1<<(i&MASK));}  
  22.   
  23. //测试位操作用&操作符  
  24.   
  25. int test(int i)  
  26.   
  27. {return a[i>>SHIFT]&(1<<(i&MASK));}  

重要的是要从十进制运算的思维转化为二进制运算,位逻辑运算不过是工具而已.

下面是一个位运算的类:

  1. namespace mybv     
  2. {     
  3.     class BitVector     
  4.     {     
  5.     private :     
  6.         const int shift;     
  7.         const int mask;     
  8.         const int bitPerWord;     
  9.         int* a;     
  10.     public:     
  11.         const int n;     
  12.     
  13.     
  14.     public:     
  15.         BitVector():shift(5),mask(0x1F),bitPerWord(32),n(10000000)     
  16.         {     
  17.             a = new int[1+n/bitPerWord];     
  18.     
  19.             for (int i =0; i<n;i++)     
  20.             {     
  21.                 clr(i);     
  22.             }     
  23.         }     
  24.     
  25.         ~BitVector()     
  26.         {     
  27.         }     
  28.     
  29.         void set(int i)     
  30.         {     
  31.             a[i>>shift] |=(1<<(i&mask));     
  32.         }     
  33.     
  34.         void clr(int i)     
  35.         {     
  36.             a[i>>shift] &=~(1<<(i&mask));     
  37.         }     
  38.     
  39.         int test(int i)     
  40.         {     
  41.             return a[i>>shift]&(1<<(i&mask));     
  42.         }     
  43.     
  44.     
  45.     };     
  46. }   

习题三 在你自己的系统上实现位图排序并度量其运行时间

解题报告:

  1. #define WORD 32  
  2.   
  3. #define SHIFT 5 //移动5个位,左移则相当于乘以32,右移相当于除以32取整  
  4.   
  5. #define MASK 0x1F //六进制下的31  
  6.   
  7. #define N 10000000  
  8.   
  9. #include <fstream>  
  10.   
  11. #include <iostream>  
  12.   
  13. using namespace std;  
  14.   
  15. //置位函数——用"|"操作符,i&MASK相当于mod操作  
  16.   
  17. //m mod n 运算,当n = 2的X次幂的时候,m mod n = m&(n-1)  
  18.   
  19. void set(int i)  
  20.   
  21. {a[i>>SHIFT]|=(1<<(i&MASK));}  
  22.   
  23. //清除位操作,用&~操作符  
  24.   
  25. void clear(int i)  
  26.   
  27. {a[i>>SHIFT]&=~(1<<(i&MASK));}  
  28.   
  29. //测试位操作用&操作符  
  30.   
  31. int test(int i)  
  32.   
  33. {return a[i>>SHIFT]&(1<<(i&MASK));}  
  34.   
  35. int main(){  
  36.   
  37. ifstream in;  
  38.   
  39. ofstream outt;  
  40.   
  41. in.open("c:/project/out.txt");  
  42.   
  43. outt.open("c:/project/outt.txt");  
  44.   
  45. int m;  
  46.   
  47. for(int i=0;i<N;i++) clear(i);  
  48.   
  49. for(int i=0;i<N/10;i++) {in>>m; set(m);}  
  50.   
  51. for(int i=0;i<N;i++) {if(test(i)==1) outt<<i<<endl;}  
  52.   
  53. in.close();  
  54.   
  55. outt.close();  
  56.   
  57. return 0;  
  58.   
  59. }  

为什么说这个算法时空效率达到极致呢?我们对100万个不重复的正整数(1000,0000以内)的文件进行测试:

 

系统排序

C++/STL.set

C++/sort

C++/位图

总时间(s)

89

38

12.6

10.7

计算时间(s)

79

28

2.4

0.5

内存使用(MB)

0.8

70

4

1.25

(本测试数据是在较旧的电脑上测试的,但还是体现性能的差距) 
第一行是总时间,第二行的计算时间是总时间减去数据读取耗时10.2秒。虽然通用C++程序使用内存和CPU时间是专用C++程序(C++位图)的50倍,但是它的使用仅需要一半的代码,并能很容易扩展到其他问题上,这也是专用C++程序最大的缺点吧。

posted @ 2011-07-03 13:25  shandatian  阅读(517)  评论(1编辑  收藏  举报