编程珠玑第一章习题答案
习题
1.1
如果不缺内存,如何使用一个具有库的语言来实现一种排序算法?
因为C++有sort,JAVA也有,这里以C++为例给出,记住如果用set集合来排序时,是不可以有元素重复的
代码:
#include <iostream> #include <cstring> #include <cmath> #include <queue> #include <stack> #include <list> #include <map> #include <set> #include <sstream> #include <string> #include <vector> #include <cstdio> #include <algorithm> using namespace std; void print(int a[],int n) { for(int i=0;i<n;i++) { cout<<" "<<a[i]; } cout<<endl; } bool cmp(int a,int b) { return a>b; } int main() { int a[9]={9,6,5,5,4,11,23,8,99}; int b[9]={9,6,5,5,4,11,23,8,99}; sort(a,a+9);//从小到大 print(a,9); sort(b,b+9,cmp);//从大到小 print(b,9); return 0; }
1.2
如何使用位逻辑运算来实现位向量?
可能会出现以下问题:(这部分和第3题有重复,读者可直接看第4题)
1) 找到数据i所对应的字节位置
2)找到数据i对应的字节中位位置
3) 判断某位是否为1, 置某位为1
解决它:
1) 找到 对应字节位置: 32系统,相当于 i/32, 使用位操作 数据i >> 5
2) 找到对应字节的位位置: 32系统相当于 i%32, 使用位操作 数据 i&0x1F(31)
附:(m mod n 运算,当n = 2的X次幂的时候,m mod n = m&(n-1) ,这里32为2^5,所以是i&0X1F)
n=2^x 写成二进制就是1后面x个0,m%n就是m的低x位。
n-1=2^x-1 写成二进制是x个1,m&(n-1)也是取m的低x位。
3)数字 a 的 第i位 是1: 方法 a & (1 << i) ,将数据a的第 i位 置1: a | (1 << i)
#define SHIFT 5 #define MASK 0x1F #define N 10000000 int a[1 + N/BITSPERWORD]; 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)); }
1.3
题目大意就是实现位图排序,有不重复的100万个整数,最大为1000万,我有详细解释,见本人博客位图排序。(不过建议你先看第4题)
1.4
如何生成小于n且没有重复的k个整数的问题。最简单的方法是、就是使用前k个正整数。这个极端的数据集合将不明显的改变位图方法的运行时间,但是可能会歪曲系统排序的运行时间。如何生成位于0至n-1之间的k个不同的随机顺序的随机整数?
利用rand()函数可以产生0到RAND_MAX范围内的随机整数。
RAND_MAX是在前面头文件中定义的宏,具体大小与实现有关,至少为32767.
一般32位机,int型为4字节,故RAND_MAX大小为2147483647(2^31-1).
看代码:
int Bigrand()// 产生很大随机整数 { return RAND_MAX*rand()+rand(); } int Randl_r(int l,int r)//产生很l~r之间的随机数 { return l+Bigrand()%(r-l+1); }
然后利用这两个函数产生K个不重复无序随机数。(基本原理就是在长为n的有序区间里,把前K个元素随机打乱)
代码:
void make_data(int num) { int *temp = new int[MAXN_NUM]; for(int i = 0; i <MAXN_NUM;i++)//MAXN_NUM指随机数最大可能是多少 { temp[i] = i + 1; } for(int i = 0; i < DATA_NUM; i++)//DATA_NUM指随机数有多少个 { int t = Randl_r(i,MAXN_NUM) % MAXN_NUM; swap(temp[i], temp[t]); } }
1.5
那个程序员说他又1MB的可用存储空间,但是我们概要描述的代码需要1.25MB的空间。他可用不费力气的索取到额外的空间。如果1MB空间是严格的边界,你会推荐如何处理呢?
解答:我们可以采用多趟算法,如2趟,首先采用500万/8字节的空间(100字节约为1M),来排序0到4999999的数,第二趟再排序5000000到9999999的数!K趟算法空间需求n/k,时间开销kn.典型的时间换空间。代码:
#include <iostream> #include <algorithm> #include <time.h> #include <bitset> #include <iostream> #include <cstring> #include <cmath> #include <queue> #include <stack> #include <list> #include <map> #include <set> #include <sstream> #include <string> #include <bitset> #include <vector> #include <cstdio> #include <algorithm> #include <fstream> using namespace std; #define DATA_NUM 1000000 //生成的随机数的个数 #define MAXN_NUM 1000000 //随机数最大为多少 #define SOURCE "data.txt" //保存随机数的文件名称 #define RESULT "result.txt" //保存排序结果的文件名称 //功能:生成随机数文件 //参数:num 生成随机数的个数 int Bigrand()// 产生很大随机整数 { return RAND_MAX*rand()+rand(); } int Randl_r(int l,int r)//产生很l~r之间的随机数 { return l+Bigrand()%(r-l+1); } void make_data(int num) { int *temp = new int[MAXN_NUM]; if(temp == NULL) { cout << "new error in make_data()" << endl; return; } for(int i = 0; i <MAXN_NUM;i++) { temp[i] = i + 1; } for(int i = 0; i < DATA_NUM; i++) { int t = Randl_r(i,MAXN_NUM) % MAXN_NUM; swap(temp[i], temp[t]); } //写入文件 FILE *fp; fp = fopen(SOURCE, "w"); for(int i = 0; i < DATA_NUM; i++) { fprintf(fp, "%d ", temp[i]); } fclose(fp); cout << "随机数文件生成成功." << endl; } void BitMapSort() { bitset<MAXN_NUM+5> bitmap; bitmap.reset(); FILE *fpsrc; fpsrc = fopen(SOURCE, "r"); int data; while(fscanf(fpsrc, "%d ", &data) != EOF) { if(data <= MAXN_NUM / 2) { bitmap.set(data, 1); } } //将排序好的数写入到结果文件中 FILE *fpdst; fpdst = fopen(RESULT, "w"); for(int i = 0; i <MAXN_NUM+5; i++) { if(bitmap[i] == 1) { fprintf(fpdst, "%d ", i); } } //处理剩下的数据 int res = fseek(fpsrc, 0, SEEK_SET); if(res) { cout << "fseek() error in BitMapSet()." << endl; return; } bitmap.reset(); while(fscanf(fpsrc, "%d ", &data) != EOF) { if(data <= MAXN_NUM&& data > MAXN_NUM/ 2) { data = data -MAXN_NUM / 2; bitmap.set(data, 1); } } for(int i = 0; i <= MAXN_NUM/ 2 + 1; i++) { if(bitmap[i] == 1) { fprintf(fpdst, "%d ", i + MAXN_NUM / 2); } } cout << "排序成功." << endl; fclose(fpdst); fclose(fpdst); } int main() { make_data(DATA_NUM); clock_t start = clock(); BitMapSort(); clock_t end = clock(); cout << "排序所用时间为:" << (end - start) * 1.0 / CLK_TCK << "s" << endl; return 0; }
1.6
如果那个程序员说的不是每个整数最多出现一次,而是每个整数最多出现10次,你又如何建议他呢?你的解决方案如何随着可用存储空间总量的变化而变化呢?
思路:
如果每个整数最多出现10次,那么我们可以用4位(2^3<10<2^4)(半个字节)来统计每个整数的出现次数。可以利用问题5中的方法,利用10000000/2个字节的空间遍历一次来完成对整个文件的排序。当保存的数字量变化时,分成更多的份,就可以再更小的空间内完成,如10000000/2k的空间内。
问题是如何实现?同样,我们还会遇到以下问题:
1) 找到数据i所对应的字节位置
2) 找到数据i对应的字节中位位置
3) 判断某位有几个数字(即这个数字出现了几次)
下面我来解释:
1)因为32位机int有4个字节,32位,我们用一个int可以记录8个数字的情况,所以字节位置可以用 i/8(i>>3)找到,这个不难想。
2)如何找到保存这个数字情况的4位呢?看下面
假设下面这是a[1]的前12位(总共32位,从右向左表示从低到高位),它可以表示8到10这几个数 我们先假设此时已有5个8,1个9,8个10
10 9 8 (这一行表示这四位记录数字i的状态)
2 1 0 (这一行表示四位四位的进行编号,相当于i%8(i&0X07))
我们可以知道数字i的状态由编号为i%8的那四个位来表示
....... 1 0 0 0 0 0 0 1 0 1 0 1 (这一行是i的前12位)
好了,假如此时有有一个9需要加入,我们怎么加?
当然是 ....... 1 0 0 0 0 0 0 1 0 1 0 1
+ 0 0 0 0 0 0 0 1 0 0 0 0
仔细观察其实就是 0 0 0 0 0 0 0 0 0 0 0 1向左移四位,可以表示为1<<(4*(i&0X07));
所以,这道题的置位函数set(int i) 可以表示为 a[i>>3]+=1<<(4*(i&0X07));
然后我们来看清位函数clr(int i)如何写?此时我们已有5个8,2个9,8个10
10 9 8
2 1 0
1 0 0 0 0 0 1 0 0 1 0 1
如果要把9的个数置0而又不影响其他位,就只能用它和另一个数按位与&了(意思就是按位与的另一个数除了需要被置零的那四位为0,其余位都要为1,如(1)所示)
就像这样: 1 0 0 0 0 0 1 0 0 1 0 1
& 1 1 1 1 0 0 0 0 1 1 1 1 (1)
= 1 1 1 1 0 0 0 0 0 1 0 1
可以看出他们按位与后只有表示9的4位都置零了,而其他位并未受影响,如何得到这个数呢?
观察发现可以对 0 0 0 0 1 1 1 1 0 0 0 0取反得到,而0 0 0 0 1 1 1 1 0 0 0 0可以将1 1 1 1 左移四位得到,即(0X0F<<(4*i&(0X07)))
所以clr函数可以用表示为 a[i>>3]&=~(0X0F<<(4*(i&0X07)));
接下来就是搞定测试函数test(int i)了,同样接上面举例,假如要测试存在几个9
10 9 8
2 1 0
1 0 0 0 0 0 1 0 0 1 0 1
那我们只要用1 1 1 1 与表示9的四位按位与就知道有几个了,即
1 0 0 0 0 0 1 0 0 1 0 1
& 0 0 0 0 1 1 1 1 0 0 0 0
= 0 0 0 0 0 0 1 0 0 0 0 0 (2)
可以看出答案就是将(2)式右移4位,于是乎,答案也就呼之欲出了
a[i>>3] & (0X0F<<(4*i&(0X07))))>>4*(i&0X07)
下面给出设置,清除,以及测试函数(但是我发现了一个问题那就是,开数组时必须用无符号型int,即unsigned int,因为有符号型第32位是表示正负的标志位,如果用有符号的,会出错,读者可以自己考虑出错在哪里),有了这3个,第6题就做出了大半,其余的参考第五题。
#define SHIFT 3 #define MASK 0X07 #define MASK1 0X0F void set_(int i)//i的个数加一 { a[i>>SHIFT]+=(1<<(4*(i&MASK))); } void clr(int i)//清0 { a[i>>SHIFT]&=~(MASK1<<4*(i&MASK)); } int test(int i)//返回有几个i { return (a[i>>SHIFT] & (MASK1<<4*(i&MASK)))>>4*(i&MASK); }
1.7
考虑中........
1.8
当那个程序员解决该问题的时候,美国所有的免费电话的区号是800。现在免费电话的区号包括800、877和888,而且还在增多。如何在1MB空间内完成对所有这些免费电话的号码的排序?如何将免费电话号码存储在一个集合中,要求可以实现非常快速的查找以判定一个给定的免费电话号码是否可用或者已经存在?
解答: 每个区号建立一个set,查找的时候效率是O(logn)。
1.9
使用更多的空间来换取更少的运行时间存在一个问题:初始化空间本身需要消耗大量的时间。说明如何设计一种技术,在第一次访问向量的项时将其初始化为0。你的方案应该使用常量时间进行初始化和向量访问,使用的额外空间应正比于向量的大小。因为该方法通过进一步增加空间减少初始化的时间,所以仅在空间很廉价、时间很宝贵且向量很稀疏的情况下才考虑。
||:书上给出的解决方法是使用两个额外的向量:from和to,还有一个变量top。如果对i位置进行初始化,进行以下步骤:
from[i] = top;
to[top] = i;
data[i] = 0;
top++;
说实话,我还在思考中,不过最终,我会完善这个答案的。
1.10
在成本低廉的隔日送达时代之前,商店允许顾客通过电话订购商品,并在几天后上门自取。商店数据库使用客户的电话号码作为其检索的主关键字(客户知道他们的电话号码,并且这些关键字几乎都是唯一的)。你如何组织商店的数据库,以允许高效的插入和检索操作?
||:
根据电话号码的最后两位作为客户的散列索引,进行分类,当顾客打电话下订单的时候,它被放置在一个合适的位置。然后当顾客抵达进行检索商品时,营业员按顺序检索订单,这是经典的用“顺序搜索来解决冲突的开放散列”:通过顺序检索。电话号码的最后两位数字是相当随机的,而电话号码的前两位作为索引行不通,因为很多电话号码的前两位是相同的。(定过外卖没?外送员是怎样确定顾客的?嘿嘿)
1.11
这个嘛,略。。。
1.12
题目就不打详细了,大概意思是载人航天需要在外太空的极端环境下实现书写。民间盛传美国国家航天局花费100万美元研发了一种特殊的钢笔来解决这个问题。那么前苏联如何解决这个问题的?
||:额,这个答案我还小时就知道了,铅笔,但是当时我是觉得这大概是有原因的。。。。额,不装B了,其实在看三傻大闹宝莱坞时我从那个校长嘴里知道了为啥在要研究这这么昂贵的钢笔,它是有原因的,顺便提一下,三傻大闹宝莱坞,不错!
如果你想知道原因,要么去看三傻,要么去看这篇博客:航天员太空用笔:一个被误会10年的故事(推荐看电影,学习也要劳逸结合嘛!)