刚开始看《编程珠玑》,将网上的诸多资料整理于此,便于自己学习总结,也便于广大朋友相互交流。
位图的应用
编程珠玑 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在使用上比较简单).
- #include <algorithm>
- #include <iostream>
- #include <vector>
- using namespace std;
- int main(){
- vector<int> a;
- for(int i=0;i<=5;i++) a.push_back(i-5);
- sort(a.begin(),a.end());
- for(int i=0;i<5;i++) cout<<a[i]<<'' '';
- return 0;
- }
习题二 如何使用位逻辑运算(例如与、或、移位)来实现位向量
解题报告:
一开始当然没有想到要用位逻辑运算来实现位向量,而是用十进制来实现(这是理所当然的事)。
用一个一维数组a[10000000]来存储至多1E7个号码,考虑整数m,一旦发现这个号码,根据我们的算法,应当置a[m]=1.
好的,一切看起来都如此完美,简单的算法,出色的时间效率,差强人意的空间效率.但上机起来就不是这回事了:
- #include <fstream>
- #include <iostream>
- using namespace std;
- int main(){
- ifstream in;
- ofstream outt;
- in.open("c:/project/out.txt");
- outt.open("c:/project/outt.txt");
- bool a[10000000];
- for(int i=0;i<10000000;i++) a[i]=false;
- int m;
- for(int i=0;i<1000000;i++)
- {in>>m; a[m]=true;}
- for(int i=0;i<10000000;i++)
- if(a[m]==true) outt<<m<<endl;
- in.close();
- outt.close();
- return 0;
- }
但是美好的想法在现实面前是如此的脆弱,这段代码在运行的时候出错了,原因是数组越界.好吧,现在我可以承认,数组开到1E7是不现实的,这该如何是好?
现在是时候回到位逻辑运算了,这是一种模仿计算机底层二进制运算的运算方法,十分高效,但是第一次看上去会显得晦涩难懂,等到将它与十进制运算联系起来后,会发现它相当有用.
整个的思想是,a[10000000]显得太过巨大的原因是每一个元素a[i]只保留了一个bool值或者是一个整型值0或1,如果我们把每一个元素包含的内容扩充,使之保留尽可能多的号码是否存在的信息,那么数组范围会得到明显的下降.
事实上,我们是用每一个元素表示一个32位的二进制字符串,这样这个元素可以保留相邻32个号码是否存在的信息,数组范围就下降到10000000/32了.例如对于号码89256,由于89256 mod 32=2789…8,这样我们应该置a[2789]中32位字符串的第8位(从低位数起)为1.
现在问题的关键是,如何用位逻辑运算来表示这种操作. 关于位逻辑运算的知识,你应当去参考手头的C++教材,因为在这里我无法讲的比教材更好:
- #define WORD 32
- #define SHIFT 5 //移动5个位,左移则相当于乘以32,右移相当于除以32取整
- #define MASK 0x1F //六进制下的31
- #define N 10000000
- //置位函数——用"|"操作符,i&MASK相当于mod操作
- //m mod n 运算,当n = 2的X次幂的时候,m mod n = m&(n-1)
- void set(int i)
- {a[i>>SHIFT]|=(1<<(i&MASK));}
- //清除位操作,用&~操作符
- void clear(int i)
- {a[i>>SHIFT]&=~(1<<(i&MASK));}
- //测试位操作用&操作符
- int test(int i)
- {return a[i>>SHIFT]&(1<<(i&MASK));}
重要的是要从十进制运算的思维转化为二进制运算,位逻辑运算不过是工具而已.
下面是一个位运算的类:
- namespace mybv
- {
- class BitVector
- {
- private :
- const int shift;
- const int mask;
- const int bitPerWord;
- int* a;
- public:
- const int n;
- public:
- BitVector():shift(5),mask(0x1F),bitPerWord(32),n(10000000)
- {
- a = new int[1+n/bitPerWord];
- for (int i =0; i<n;i++)
- {
- clr(i);
- }
- }
- ~BitVector()
- {
- }
- void set(int i)
- {
- a[i>>shift] |=(1<<(i&mask));
- }
- void clr(int i)
- {
- a[i>>shift] &=~(1<<(i&mask));
- }
- int test(int i)
- {
- return a[i>>shift]&(1<<(i&mask));
- }
- };
- }
习题三 在你自己的系统上实现位图排序并度量其运行时间
解题报告:
- #define WORD 32
- #define SHIFT 5 //移动5个位,左移则相当于乘以32,右移相当于除以32取整
- #define MASK 0x1F //六进制下的31
- #define N 10000000
- #include <fstream>
- #include <iostream>
- using namespace std;
- //置位函数——用"|"操作符,i&MASK相当于mod操作
- //m mod n 运算,当n = 2的X次幂的时候,m mod n = m&(n-1)
- void set(int i)
- {a[i>>SHIFT]|=(1<<(i&MASK));}
- //清除位操作,用&~操作符
- void clear(int i)
- {a[i>>SHIFT]&=~(1<<(i&MASK));}
- //测试位操作用&操作符
- int test(int i)
- {return a[i>>SHIFT]&(1<<(i&MASK));}
- int main(){
- ifstream in;
- ofstream outt;
- in.open("c:/project/out.txt");
- outt.open("c:/project/outt.txt");
- int m;
- for(int i=0;i<N;i++) clear(i);
- for(int i=0;i<N/10;i++) {in>>m; set(m);}
- for(int i=0;i<N;i++) {if(test(i)==1) outt<<i<<endl;}
- in.close();
- outt.close();
- return 0;
- }
为什么说这个算法时空效率达到极致呢?我们对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++程序最大的缺点吧。