问题:
输入:给出至多10,000,000个正整数的序列
特征:每个数都小于10,000,000、数据不重复 且 数据之间不存在关联关系(相互独立)
输出:增序输出序列
约束:内存容量1MB,磁盘空间充足,运行时间至多几分钟,10是最适宜的时间
分析:我们需要10,000,000个数表示10,000,000个位。1MB的包含8*1024*1024个位
则,所需要的内存容量为:10,000,000/(8*1024*1024) = 1.20MB
这里和要求的差不多,我们可以使用位图排序,之后再进行优化,使之在1MB内存上运行
思路:
/*第一步,初始化这个比特位集合中的所有位为0*/ for i = [0, 10,000,000) bit[i] = 0 /*第二步,读入待排序的正整数*/ for each i in the input file bit[i] = 1 /*第三步,输出排序结果*/ for i = [0, 10,000,000) if bit[i] == 1 write i on the output file
程序
使用字节存储位向量
#include <iostream> using namespace std; const int BITRERBYTE = 8;//位向量使用字节存储存储 const int SHIFT = 3; //右移3位,相当于除去8 const int MASK = 0x7; //16进制下的7 const int MAXNUM = 10000000;//最大的数为1千万 const int NUMCOUNT = 5;//实际待排序的数个数 unsigned char bit[MAXNUM/BITRERBYTE + 1];//位向量 /*置0*/ void InitBit(int i) { bit[i>>SHIFT] &= ~(1 << (i&MASK)); } /*置1*/ void SetBit(int i) { bit[i>>SHIFT] |= (1 << (i&MASK)); } /*检查是否1*/ int TestBit(int i) { return bit[i>>SHIFT] & (1 << (i&MASK)); } int main() { int arr[NUMCOUNT] = {3,2,5,4,9}; //关闭所有位 memset(bit,0,sizeof(bit)); //读入待排序的整数 for (int i = 0;i < NUMCOUNT;i++) { SetBit(arr[i]); } //输出数据 for (int i = 0;i < MAXNUM;i++) { if (TestBit(i)) { cout<<i<<" "; } } cout<<endl; system("pause"); return 1; }
使用整形存储位向量
#include <iostream> using namespace std; const int BITPERWORD = 32;//位容器使用整形数据存储 const int SHIFT = 5; //右移5位,相当于除去32 const int MASK = 0x1F; //16进制下的31 const int MAXNUM = 10000000;//最大的数为1千万 const int NUMCOUNT = 5;//实际待排序的数个数 int bit[MAXNUM/BITPERWORD+ 1];//位向量 /*置0*/ void InitBit(int i) { bit[i>>SHIFT] &= ~(1 << (i & MASK)); } /*置1*/ void SetBit(int i) { bit[i>>SHIFT] |= (1 << (i & MASK)); } /*检查是否1*/ int TestBit(int i) { return bit[i>>SHIFT] & (1 << (i & MASK)); } int main() { int arr[NUMCOUNT] = {3,2,5,4,9}; //关闭所有位 memset(bit,0,sizeof(bit)); //读入待排序的整数 for (int i = 0;i < NUMCOUNT;i++) { SetBit(arr[i]); } //输出数据 for (int i = 0;i < MAXNUM;i++) { if (TestBit(i)) { cout<<i<<" "; } } cout<<endl; system("pause"); return 1; }
注意:
0、位图排序使用范围:
(1)非负整数 (2)每个整数最多出现一次 (3)最大整数小于 N (4)整数之间相互独立
1、使用int和char存储位图原理上是没有区别的,上述代码的区别仅仅是几个常量BITPERWORD、SHIFT、MASK不同,其他代码全部相同
2、给出一个数,怎么设置其对应位图的位置
unsigned char bit[2];(这里使用字节存储位图,如果使用int存储,原理一样)
此时,可用字节有两个0、1字节,一个字节中有八位
也就是说,我们只能表示16个数(0~15)
问题:给出一个数,怎么判断其对应位图的位置
步骤:找到该数对应的字节 + 再找到该数对应的位
举例:14对应的字节:14/8 = 1
14对应的位:第14%8位
即,14是存储在第1个字节上的第6号位
8是存储在第1个字节上的第0号位
确定num所在字节的方法
num/8 或 num>>3
确定num所在上述字节的几号位
num % 8 num & 0b111(二进制下的7,0b表示二进制) //注意:num MOD n = num & (n-1) (n = 2^m,n-1表示低几位为全1)
找到待处理数所在的字节和位后,我们怎么设置它为1或0呢
思想:针对其所在字节的所在位,进行 & 和 | 操作,设置为0或1
/*置1*/ bitmap[num>>3] |= (1<<(num & 0b111));
处理时,我们针对其所在字节整体进行操作,即找到其所在字节 bitmap[num>>3]
之后,我们要把其对应位设为1(则使用或操作),同时又不能影响该字节中的其他位,即只改动一位
方法:我们可以利用一个新的字节且把该字节上,num对应的位设为1,其他全为0(则找到num对应的位置num & 0b111,之后对1进行右移操作)。之后和num所映射的字节进行或操作
/*置0*/ bitmap[num>>3] &= ~(1<<(num & MASK));
同理,置0操作可以一样设置
由于,我们要把一个位设置为0,我们可以使用与操作
/*判断是1或0*/ if( bitmap[num>>3] & (1<<(num & MASK)) )
判读是0还是1,可以让其和1进行与操作。如果是0,结果为0。是1,结果为1
优化:假如严格限制为1MB,可以采用的策略:
1、两次遍历待排序列
2、观察待排序列的特征,判断是否含有不可能出现的整数,缩减范围
两次遍历待排序列是指我们可以把数据分成两部分,
第一次对1-4,999,999之间的数排序,需要存储空间为:5,000,000/(8*1024*1024) = 0.596MB,之后再对5,000,000 -10,000,000 之间的数排序。总体上消耗的空间为0.596MB。
#include <iostream> using namespace std; const int BITPERWORD = 32;//位容器使用整形数据存储 const int SHIFT = 5; //右移5位,相当于除去32 const int MASK = 0x1F; //16进制下的31 const int MAXNUM = 10000000;//最大的数为1千万 const int NUMCOUNT = 5;//实际待排序的数个数 int bit[MAXNUM/(2 * BITPERWORD) + 1];//位向量 /*置0*/ void InitBit(int i) { bit[i>>SHIFT] &= ~(1 << (i & MASK)); } /*置1*/ void SetBit(int i) { bit[i>>SHIFT] |= (1 << (i & MASK)); } /*检查是否1*/ int TestBit(int i) { return bit[i>>SHIFT] & (1 << (i & MASK)); } int main() { int j = 0; int arr[NUMCOUNT] = {3,2,5,4,9}; int arrTmp[NUMCOUNT]; //只是测试结果,实际过程可以直接输出 //关闭所有位 memset(bit,0,sizeof(bit)); //对1-4,999,999之间的数排序 for (int i = 0;i < NUMCOUNT;i++) { if (arr[i] < MAXNUM/2) { SetBit(arr[i]); } } //收集排好序的结果 for (int i = 0;i < MAXNUM/2;i++) { if (TestBit(i)) { arrTmp[j++] = i; } } //关闭所有位 memset(bit,0,sizeof(bit)); //对5,000,000 -10,000,000 之间的数排序 for (int i = 0;i < NUMCOUNT;i++) { if (arr[i] >= MAXNUM/2) { SetBit(arr[i]); } } //收集排好序的结果 for (int i = MAXNUM/2;i < MAXNUM;i++) { if (TestBit(i)) { arrTmp[j++] = i; } } //测试--输出所有结果 for (int i = 0;i < j;i++) { cout<<arrTmp[i]<<" "; } cout<<endl; system("pause"); return 1; }
也就是说,当内存充足时,我们遍历一遍。
当内存不足以把整个bitset全部搬入内存时,我们可以放入一部分,遍历多遍数据
注意,每一遍处理的数据范围是不同的
需要的遍历的趟数k = 排序用的总的内存/实际使用的内存(往上取整)
时间开销 = k*一趟遍历数据的时间
空间开销 = 总的内存消耗/趟数k
习题六、改变题目的限制条件,现在数据可以重复且最多重复十次,该怎么使用位图排序
方法:我们需要使用四位表示一个数出现的次数。此时消耗的空间=1.25*4 = 5M
这里我们仍然使用上面的方法,进行k趟遍历数据
即可以把数据分成5部分,一部分使用1MB进行排序
习题八:免费电话的区号为800,878,888等,七位数字表示电话号码
要求:
问题1、现在给出1MB空间,对所有免费号码进行排序
方法1:把整个区间放入内存
若一千万个电话号码都可能成为免费号码,那么需要的空间1.25MB * (免费号码前缀个数)。
方法2:省空间,多次扫描文件:
1、首先扫描整个文件,看有哪个免费号码前缀 以及 每个免费号码前缀下的号码个数。
2、设置区间映射表:比如800前缀有125个免费号码,找到最大的数、最小的数,差值做为bit长度。
问题2、如何存储免费号码,使得给一个免费电话号码,快速确定该免费电话是可用还是被占用
使用散列存储电话号码,实现快速查询
方法:使用7位的电话作为关键字,后面区号(800,878)作为散列值。
给出一个区号 + 电话号码,我们使用电话号码迅速获取其有关散列值,在检查区号是否出现在这些值。
在STL中,bitset实现了相同的功能
#include <iostream> #include <bitset> using namespace std; const int MAXNUM = 10;//最大能出现的数,用于设定bitset的大小 const int NUMCOUNT = 5;//实际待排序的数个数 int main() { int arr[NUMCOUNT] = {3,2,5,4,9}; bitset<MAXNUM + 1> bitmap;//10位,初始化为0 //把待排序元素放入bitset中 for (int i = 0;i < NUMCOUNT;i++) { bitmap.set(arr[i]); } //输出结果 for (int i = 0;i < MAXNUM;i++) { if (bitmap.test(i)) { cout<<i<<" "; } } cout<<endl; system("pause"); return 1; }
相关操作
习题四、随机生成k个[0, n)之间不重复的随机数
思路:先顺序生成所有的可能数,之后打乱顺序
要求:整数范围:[a,b)、k个整数唯一、次序随机、不重复
for i = [0,n) x[i] = i; for i = [0,k) swap(i,randint(i,n-1));
扩展:整数范围:[a,b)、k个整数唯一、次序随机、不重复
int arr[b]; //初始化数据 for (int i = a ;i < b;i++) { arr[i] = i; } //随机数代表数据的位置, srand((unsigned)time(NULL)); for (int i = 0;i < k;i++) { int loc = rand()%(b - i) + i;//随机数的区间[i,b) swap(arr[i],arr[loc]) }
相关应用
1、给40亿个不重复的unsigned int的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中
2、使用位图法判断整形数组是否存在重复
3、位图法存数据
参考文章
http://blog.csdn.net/tianshuai11/article/details/7555563
http://blog.csdn.net/silenough/article/details/6956758