习题 5. 那个程序员说他有1MB的可用存储空间,但是我们概要描述的代码需要1.25MB的空间。他可以不费力气的索取到额外的空间。如果1MB空间是严格的边界,你会推荐如何处理呢?你的算法的运行时间又是多少?
用位向量表示10 000 000个整数需要10 000 000个位,10000000 bits = 1.192MB.所以程序员可利用的1MB空间是显然不够的,可以采用两趟算法,第一趟利用5000000个位来排序0~4999999之间的整数,然后在第二趟中排序排序5000000~9999999之间的整数。代码如下:
#include <stdlib.h>
#include <time.h>
#include <iostream>
#include <boost/timer.hpp>
#include <boost/progress.hpp>
using namespace std;
using namespace boost;
#define BITSPERDWORD 32 //表示一个整形变量具有32个位
#define SHIFT 5 //单次位移量,右移一位相当于除以2,左移一位相当于乘以2
#define MASK 0x1f //掩码,其十进制即为31
#define N 10000000 //表示10000000个7为非负整数
//将位数组从0开始的第i个位置为0,即将整数i在位向量中对应位置的位置为false
void resetBits(int * arry, int i)
{
arry[i >> SHIFT] &= ~(1 << (i & MASK)); //m%n,n = 2^x时,m%n = m & (n-1)
}
//将位数组从0开始的第i个位置为1,即将整数i在位向量中对应位置的位置为true
void setBits(int * arry, int i)
{
arry[i >> SHIFT] |= (1 << (i & MASK));
}
//检测i是否在集合中
int testBits(int * arry, int i)
{
return arry[i >> SHIFT] & (1 << (i & MASK));
}
//返回[l, u]范围内的一个随机整数
int randint(int l, int u)
{
return (l + rand() % (u - l + 1));
}
int * geneRand2(int m ,int n)
{
int i, j;
int *pArry = new int[n];
for (i = 0; i < n; i++) //生成数组
{
pArry[i] = i;
}
//筛选随机数
srand((unsigned)time(NULL));
for (i = 0; i < m; i++)
{
j = randint(i, n - 1);
int t = pArry[i];
pArry[i] = pArry[j];
pArry[j] = t ;
}
return pArry;
}
void mySort(int m ,int n)
{
int i, j = 0, k;
int *pBitsArry = new int[1 +n / (2 * BITSPERDWORD)]; //使用空间减半
int * p = geneRand2(m ,n);
int *pOutArry = new int[m]; //模拟输出文件
progress_timer t;
for (k = 0; k < 2; k++)
{
for (i = 0; i < (n / 2); i++)
{
resetBits(pBitsArry, i);
}
if (0 == k)
{
for (i = 0; i < m; i++)
{
if (p[i] < (n / 2))
{
setBits(pBitsArry, p[i]);
}
}
for (i = 0; i < (n / 2); i++)
{
if (testBits(pBitsArry, i))
{
pOutArry[j++] = i;
}
}
}
else
{
for (i = 0; i < m; i++)
{
if (p[i] >= (n / 2))
{
setBits(pBitsArry, p[i]-(n / 2));
}
}
for (i = n / 2; i < n; i++)
{
if (testBits(pBitsArry, (i - n / 2)))
{
pOutArry[j++] = i;
}
}
}
}
cout << t.elapsed()<<endl; // 生成位于0至n-1之间m个有序且不重复的随机整数所用的时间
/* cout<<"..........................."<<endl;
for (i = 0; i < m; i++)
{
cout << pOutArry[i]<<endl;
}
cout<<"..........................."<<endl;*/
delete [] p;
delete [] pBitsArry;
delete [] pOutArry;
}
int _tmain(int argc, _TCHAR* argv[])
{
mySort(10000000,10000000);
return 0;
}
排序计算时间为(m = n = 1000 0000):0.859s
k趟算法可以在kn的时间开销和n/k的空间开销内完成对最多n个小于n的无重复正整数的排序。
习题 6. 如果那个程序员说的不是每个整数做多出现一次,而是每个整数最多出现10次,你又如何建议他呢?你的解决方案如何随着可用存储空间总量的变化而变化呢?
因为10等于二进制的1010, 2^3<10<2^4,所以可以用4位的半字节来统计它的出现次数,即需要4*1000 0000个位来表示1000 0000个正整数。利用习题五,可以使用4000 0000/k个位在k趟内完成对整个文件的排序。
习题 7. 本书1.4节中描述的程序存在一些缺陷。首先是假设在输入中没有出现两次的整数。如果是假定在输入中没有出现两次的整数。如果某个数出现超过一次的话,会发生什么?在这种情况下,如何修改程序来调用错误处理函数?当输入整数小于零或者大于等于n时,又会发生什么?如果某个输入不是数值又如何?在这些情况下,程序该如何处理?程序还应该包含哪些明智的检查?描述一些用以测试程序的小型数据集合,并说明如何正确处理上述以及其他不良情况。
1) 如果某个数出现超过一次的话,那么该数在排序后的文件中只会出现一次,因为该数对应的位被重复置位为1.在调用setBits()函数时,首先检查该数在位向量中对应的的位是否为0,如果是0表示该数第一次出现,那么将其置为1;否则则表示该数是重复出现的,可抛出异常进行错误处理。
2) 因为负数对于是个有符号数,其二进制的最高位为1,但计算机存储负数并不是按照其二进制形式直接存储的,而是对其取反加1再存储的,所以当输入小于零时会导致移位运算符和位运算符产生错误的值,从而导致访问数组越界等问题。如:
#include <iostream>
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
int a = 10, b = -10;
cout << (a >> 5) << endl;
cout << (a & 31)<<endl;
cout << (b >> 5) << endl;
cout << (b & 31)<<endl;
return 0;
}
输出结果为:0
10
-1
22
对于任何一个整数A,若A>0,则其补码等于其本身;若A<0,则其补码为其符号位不变,其他位按位取反再加1。
b = 10000000000000000 00000000 00001010 //b的原码
b' = 11111111 11111111 11111111 11110110 //b的补码
所以,b>>5 = 1111111111111111 11111111 11111111 (算术右移,即对有符号数右移时,左边移入该数的符号位,原来为1就移入1,原来为0就移入0),也就等于-1. b & 31 = 00000000 0000000000000000 00010110 = 22
若输入整数大于或小于n时,会发生位向量数据访问越界。对于输入不是数值时,应首先检查输入数据是否为整数。