编程珠玑——第一章习题解答

1、如果不缺内存,如何使用一个具有库的语言来实现以后总排序算法和排序集合?

答:C++有实现排序的库函数:sort。该函数的实现是快速排序。另外C++的容器Map和set均可以实现排序。由于Map和set的实现是红黑树,所以具有自动排序功能。

快速排序算法实现:

 
  1. void QuickSort(int *array,int left,int right){

  2.  
  3. int i =left;

  4. int j = right;

  5. int temp = array[left];

  6. if(left>=right)return;

  7. while(i!=j){

  8. while(array[j]>=temp&&i<j)

  9. j--;

  10. if(j>i)

  11. array[i]=array[j];

  12. while(array[i]<=temp&&i<j)

  13. i++;

  14. if(i<j)

  15. array[j] = array[i];

  16. }

  17. array[i] = temp;

  18. QuickSort(array,left,i-1);

  19. QuickSort(array,i+1,right);

  20. }

快速排序疑问:

 QuickSort(array,left,i-1);
 QuickSort(array,i,right);

如果改成这样的递归形式,为什么程序会出错??

标准的快速排序一次排序将数组分成三段(左边小于标称值的数组,标称值,右边大于标称值的数组)。如果在递归过程中,如上将数组分成两段(其中一段数组包含标称值),则在算法具体实现的时候会出现问题。

考虑一种特殊的数组 {1,5,6,7,8},这个数组按书上的调用模式QuickSort(array,left,i-1);,QuickSort(array,i,right);;在算法的具体实现中,我们总是将标称值选择为数组的第一个元素。对于这个数组,一次排序之后的结果不变,数组还是{1,5,6,7,8},此时看递归调用的表达形式QuickSort(array,0,-1),QuickSort(array,0,length-1);且不管QuickSort(array,0,-1)这里是否对输入做了边界检查;注意,QuickSort(array,0,length-1),这步递归排序的还是原始的整个数组,这也就意味着递归调用不会有结束的时候,运行程序肯定会出现栈溢出。

实测发现, QuickSort(array,left,i);;QuickSort(array,i+1,right);这样递归调用,程序是没有问题的;此时我们发现表达形式变为了QuickSort(array,0,0),QuickSort(array,1,length-1);其中QuickSort(array,0,0)符合递归的终止条件;QuickSort(array,1,length-1)实际排序的数组长度减了1,那也就意味着递归肯定有结束的时候,所以这个调用在这种快速排序的写法中是没有问题的。但是如果我们将标称值设置为数组的最后一个元素,那么这种写法就会出现栈溢出的情况。

所以,用算法导论上快速排序的写法才是最保险的

2、如何使用位逻辑运算(例如与、或、移位)来实现位向量?(指的是实现位向量的置位,清零,探测的三个操作)

答:

 
  1. class BitVector{

  2. private:

  3. const int shift;

  4. const int mask;

  5. const int bitPerWord;

  6. int *a;

  7.  
  8. public:

  9. const int n;

  10.  
  11. public:

  12. BitVector():shift(5),mask(0x1F),bitPerWord(32),n(10000000)

  13. {

  14. a = new int[1+n/bitPerWord];

  15. for(int i=0;i<n;i++){

  16. clr(i);

  17. }

  18. }

  19. ~BitVector(){

  20.  
  21. }

  22.  
  23. void set( int i){

  24. a[i>>shift] |= (1<<(i&mask));

  25. }

  26.  
  27. void clr(int i){

  28. a[i>>shift] &=~ (1<<(i&mask));

  29. }

  30.  
  31. int test(int i){

  32. return a[i>>shift]&(1<<i&mask);

  33. }

  34. };

总体来说,这个用一个一维数组标书一个二维数组的方法。首先看输入参数i,这个参数是用来指定到底想要操作哪一位bit的。具体来说,i的第五位确定

某一行的具体哪一个元素,i的高位用来确定是具体哪一行(数组a[]中的哪一个元素,这个int类型的元素有32个bit)。

a[i>>shift]  确定取哪一个数组元素,i>>shift表示取i中的高位来作为数组a的下标</span>
(i&mask)   取i的低五位,在置位,清零,探测操作中,都是先取得低5位的数组(表示具体32bit的哪一位),
置位操作是按位或;清零操作是与非,探测是与
 

  

3、运行时效率是设计目标的一个重要组成部分,所得到的程序需要足够高效。在你自己的系统上实现位图程序并度量其运行时间。该时间与系统排序的运行时间以及习题一种排序的运行时间相比如何??假设n为10 000 000,切输入文件包含1 000 000个整数。

答:

位图是一种很特殊的数据结构,可以利用位图来排序,但是这种排序方法对输入的数据是有比较严格的要求(数据不能重复,大致知道数据的范围)。举个例子,假如有一个集合{3,5,7,8,2,1},我们可以用一个8位的二进制向量set[1-8]来表示该集合,如果数据存在,则将set相对应的二进制位置1,否则置0.根据给出的集合得到的set为{1,1,0,1,0,1,1,1},然后再根据set集合的值输出对应的下标即可得到集合{3,5,7,8,2,1}的排序结果。

一.位图的应用:

      1.给40亿个不重复的unsigned int的整数,没有排过序,然后再给一个数,如果快速判断这个数是否在那40亿个数当中。

      因为unsigned int数据的最大范围在在40亿左右,40*10^8/1024*1024*8=476,因此只需申请512M的内存空间,每个bit位表示一个unsigned int。读入40亿个数,并设置相应的bit位为1.然后读取要查询的数,查看该bit是否为1,是1则存在,否则不存在。

      2.给40亿个unsigned int的整数,如何判断这40亿个数中哪些数重复?

      同理,可以申请512M的内存空间,然后读取40亿个整数,并且将相应的bit位置1。如果是第一次读取某个数据,则在将该bit位置1之前,此bit位必定是0;如果是第二次读取该数据,则可根据相应的bit位是否为1判断该数据是否重复。

二.位图的实现

     由于在C语言中没有bit这种数据类型,因此必须通过位操作来实现。

     假如有若干个不重复的正整数,范围在[1-100]之间,因此可以申请一个int数组,int数组大小为100/32+1。

假如有数据32,则应该将逻辑下标为32的二进制位置1,这个逻辑位置在A[1]的最低位(第0位)。

因此要进行置1位操作,必须先确定逻辑位置:字节位置(数组下标)和位位置。

字节位置=数据/32;(采用位运算即右移5位)

位位置=数据%32;(采用位运算即跟0X1F进行与操作)。

其他操作如清0和判断两个操作类似。

用bitset集合实现位图排序:

 
  1. #include<iostream>

  2. #include<bitset>

  3.  
  4. #define MAX 1000000

  5. using namespace std;

  6.  
  7. bitset<MAX+1> bit; //声明一个有(MAX+1)个二进制位的bitset集合,初始默认所有二进制为0

  8.  
  9. int main(int argc,char *argv[]){

  10. int n,i;

  11. while(scanf("%d",&n)!=EOF)

  12. {

  13. bit.set(n,1); //将n位置1

  14. }

  15. for(i = 0;i<MAX+1;i++){

  16. if(bit[i]==1)

  17. printf("%d ",i);

  18. }

  19. system("pause");

  20. }

自己实现bitset的位图程序:

 
  1. #include<iostream>

  2. #include<bitset>

  3.  
  4. #define MAX 1000000

  5. using namespace std;

  6. class BitVector{

  7. private:

  8. const int shift;

  9. const int mask;

  10. const int bitPerWord;

  11. int *a;

  12.  
  13. public:

  14. const int n;

  15.  
  16. public:

  17. BitVector():shift(5),mask(0x1F),bitPerWord(32),n(10000000)

  18. {

  19. a = new int[1+n/bitPerWord];

  20. for(int i=0;i<n;i++){

  21. clr(i);

  22. }

  23. }

  24. ~BitVector(){

  25.  
  26. }

  27.  
  28. void set( int i){

  29. a[i>>shift] |= (1<<(i&mask));

  30. }

  31.  
  32. void clr(int i){

  33. a[i>>shift] &=~ (1<<(i&mask));

  34. }

  35.  
  36. int test(int i){

  37. return a[i>>shift]&(1<<i&mask);

  38. }

  39. };

  40.  
  41.  
  42.  
  43. int main(int argc,char *argv[]){

  44. int n,i;

  45. BitVector bit;

  46.  
  47.  
  48. while(scanf("%d",&n)!=EOF)

  49. {

  50. bit.set(n); //将n位置1

  51. }

  52. for(i = 0;i<MAX+1;i++){

  53. if(bit.test(i)==1)

  54. printf("%d ",i);

  55. }

  56.  
  57. system("pause");

  58.  
  59. }

程序有一点小问题,,继续调试

进过调试之后发现,test函数的编写有问题。

return a[i>>shift]&(1<<i&mask);<span style="white-space:pre">	</span>//这里返回的是2^n,n=i&mask
<span style="font-family:Arial, Helvetica, sans-serif;">应该修改为:return (a[i>>shift]&(1<<(i&mask)))>>(i&mask);<span style="white-space:pre">	</span>//应该还有一个右移n位的操作</span>
 
  1. #include<iostream>

  2. #include<bitset>

  3.  
  4. #define MAX 1000000

  5. using namespace std;

  6. class BitVector{

  7. private:

  8. const int shift;

  9. const int mask;

  10. const int bitPerWord;

  11. int *a;

  12.  
  13. public:

  14. const int n;

  15.  
  16. public:

  17. BitVector():shift(5),mask(0x1F),bitPerWord(32),n(10000000)

  18. {

  19. a = new int[1+n/bitPerWord];

  20. for(int i=0;i<n;i++){

  21. clr(i);

  22. }

  23. }

  24. ~BitVector(){

  25.  
  26. }

  27.  
  28. void set( int i){

  29. a[i>>shift] |= (1<<(i&mask));

  30. }

  31.  
  32. void clr(int i){

  33. a[i>>shift] &=~(1<<(i&mask));

  34. }

  35.  
  36. int test(int i){

  37. return (a[i>>shift]&(1<<(i&mask)))>>(i&mask);

  38. }

  39. };

  40.  
  41.  
  42.  
  43. int main(int argc,char *argv[]){

  44. int n,i;

  45. BitVector bit;

  46.  
  47.  
  48. while(scanf("%d",&n)!=EOF)

  49. {

  50. bit.set(n); //将n位置1

  51.  
  52. }

  53. for(i = 0;i<MAX+1;i++){

  54. if(bit.test(i)==1)

  55. printf("%d ",i);

  56. }

  57.  
  58. system("pause");

  59.  
  60. }

4、如果认真考虑了习题3,你将会面对生成小于n且没有重复的k个整数的问题。最简单的方法就是使用前k个正整数。这个极端的数据集合将不会明显地改变位图方法的运行时间,但是可能会歪曲系统排序的运行时间。如何生成位于0至n-1之间的k个不同的随机顺序的随机整数?尽量使你的程序简短且有效。

答:

解决这个问题可以使用以空间换时间的方式,基本的思想是 利用洗牌的原理,将n个数(0至n-1)按次序排好,依次让每个数和一个随机挑选出的位子进行互换,这样肯定不会重复,而且次序被打乱,具有随性。 只用交换k次,就可以取出k个小于n的互不相同的随机数。(洗牌算法)

Java代码实现:

 
  1. public static void main(String[] agrs) {

  2. long start = System.currentTimeMillis();

  3. int n = 10000000;

  4. int k = 1000000;

  5. boolean[] appear = new boolean[n];

  6. Random rand = new Random();

  7. int[] jj = new int[k];

  8. for (int i = 0; i < k; i++) {

  9. int j = -1;

  10. while (appear[j = rand.nextInt(n)])

  11. ;

  12. jj[i] = j;

  13. appear[j] = true; //标记法

  14. }

  15.  
  16. System.out.println("jj'length:" + jj.length);

  17. System.out.println(System.currentTimeMillis() - start);

  18. }

5、那个程序员说他有1MB的可用存储空间,但是我们概要描述的代码需要1.25MB的空间,他可以不费力气地索取到额外的空间。

答: 用位向量表示10 000 000个整数需要10 000 000个位,10000000 bits = 1.192MB.所以程序员可利用的1MB空间是显然不够的,可以采用两趟算法,第一趟利用5000000个位来排序0~4999999之间的整数,然后在第二趟中排序排序5000000~9999999之间的整数

  k趟算法可以在kn的时间开销和n/k的空间开销内完成对最多n个小于n的无重复正整数的排序。

 
  1. #include<iostream>

  2. #include<stdio.h>

  3. #include<bitset>

  4.  
  5. #define MAX 5000000 //分两次获取

  6. using namespace std;

  7.  
  8. bitset<MAX+1> bit; //声明一个有(MAX+1)个二进制位的bitset集合,初始默认所有二进制为0

  9.  
  10. int main(int argc,char *argv[]){

  11. int n,i;

  12. FILE *fp;

  13. fp = freopen("C:\\Users\\zj\\Desktop\\DATA.txt","r",stdin);

  14. while(scanf("%d",&n)!=EOF)

  15. {

  16. if(n<=MAX)

  17. bit.set(n,1); //将n位置1

  18. else if(n>2*MAX){

  19. printf("输入数据有误,超出范围");

  20. return 0;

  21. }

  22. }

  23. for(i = 0;i<MAX+1;i++){

  24. if(bit[i]==1)

  25. {

  26. printf("%d ",i);

  27. bit.reset(i);

  28. }

  29. }

  30.  
  31. fseek(fp ,0,SEEK_SET);

  32.  
  33. while(scanf("%d",&n)!=EOF)

  34. {

  35. if(n>MAX)

  36. {

  37. bit.set(n-MAX,1);

  38. }

  39. }

  40.  
  41. for(i = 0;i<MAX+1;i++){

  42. if(bit[i]==1)

  43. printf("%d ",i+MAX);

  44.  
  45. }

  46. freopen( "CON", "r", stdin ); //转化为标准输入

  47. system("pause");

  48. }

6、如果那个程序员说的不是每个整数最多出现一次,而是每个整数最多出现10次,你又如何建议他?你的解决方案如何随着可用存储空间总量的变化而变化?

答:首先使用4个bit来统计每个整数出现的次数。

7、本书1.4节中存在一些缺陷。首先是假定在输入中没有出现两次的整数。如果某个数出现超过一次的话,会发生什么?在这种情况下,如何修改程序来调用错误处理函数?

当输入整数小于零或大于等于n时,又会发生什么?如果某个输入不是数值又如何?在这些情况下,程序应该如何处理?程序还应该包含哪些明智的检查?描述一些用于测试程序的小型数据集合。并说明如何正确处理上述以及其他的不良情况。

答:首先应该做输入类型检查,输入边界检查

8、当那个程序员解决该问题的时候,美国所有免费电话的区号都是800.现在免费电话的区号包括800、877和888,而且还在增多。如何在1MB空间内完成对所有这些免费电话

号码的排序?如何将免费号码存储在一个集合中,要求可以实现非常快速的查找以判定一个给定的免费电话号码是否可用或者已经存在?

答:

9、使用更多的空间来换取更少的运行时间存在一个问题;初始化空间本身需要消耗大量的时间。说明如何设计一种技术,在第一次访问向量的项时将其初始化为0.你的方案应该使用常量时间进行初始化和向量访问,使用的额外空间应正比于向量的大小。因为该方法通过进一步增加空间来减少初始化的时间,所以仅在空间很廉价,时间很宝贵且向量

很稀疏的情况下才考虑使用。

答:这个以空间换时间的数据结构是很Amusing的。首先应考虑初始化数组导致的直接结果是什么? 就是第一次访问某个元素的时候,这个元素的值是初始化后的值。由此就想到了,我们提出的算法应该能够实现:当我们第一次访问某个元素的时候,这个元素的值是初始化后的值。这个实现的关键点是:如何判断是第一次访问,即如何判断数组中的元素是初始化过还是没有初始化过。思考如下几点:

1)当时我看这道题的时候第一个反应就是:再建立一个数组,然后第一次访问时候将已初始化元素的数组下标存储在里面,每次访问的时候查询这个数组,要是数组下标在里面,就说明这个数组下标的元素已经被初始化了。相信很多人都有同样的想法,但是如果这样做的话,虽然节省了初始化的时间,但是每次访问都要在存已访问元素下标的数组里进行线性搜索,这样的时间也是浪费不少,如何不用线性时间而用常数时间就能判断是否一个数组元素是否初始化过呢

2)然后我就又想到,还不如建立一个和需要访问数组等长的数组,在访问某个元素的时候先看这个数组,要是这个数组的对应元素标记为1则表示已经初始化过了,没有就把相应元素置为1。不过同样 ,这个等长的数组没有被初始化过,那么它里面存储的随机数有很大的概率就是1,这样很容易误判。

3)然后我看了习题答案后,恍然大悟。作者通过巧妙地利用from和to数组以及整数top保障了方法的可靠性。

       分析:首先,我们应该对题目进行彻底分析---我们需要访问的是一个长度(假设为n)非常大的数组,一般而言对数组中某个元素访问前我们必须要进行初始化,但是当n值非常大而程序对time要求较严格时,对所有的数组元素都进行统一的初始化是不可取的。为了达到程序对time的要求,我们应该对需要访问的元素(它的个数相对于n来说很小)进行初始化。其次,对元素初始化的判断---为了提高判断的准确性,答案引入了两个数组from和to以及整数top,且对于元素data[i]已初始化的条件是from[i]<top && to[from[i]]==i。现在让我们来具体分析这些规则是如何被应用的:假设当我们第一次访问的数组元素下标为1时,先判断初始化条件(此时数组from和to都没有初始化,当然time也不允许让我们多管闲事),一般而言from[1]中的随机数是大于top(现在为0)的,但我们不能保证,于是我们加入了第二个判断条件--to[from[1]]==1,对于这个表达式我们两次取随机值且让后者等于1,这样的概率有但几乎为0!因此,data[1]未被初始化,于是执行from[1]=top; to[top]=1; data[1]=0; top++;这样做的目的就是保证以后再次访问data[1]时不需要再初始化(条件满足了直接读取即可)。最后,对于该方法的可靠性分析---让我们先来分析一下整数top的作用,不难发现,top记录了当前data中已初始化元素的个数,但主要是保证了from中已初始化的元素都小于top(通过from[i]=top; top++),这给我们的判断条件(from[i]<top)提供了一定的可靠性,当然再加上第二到保险(to[from[i]]==i)使得此方法可靠性值得信赖!

       答案:借助于两个额外的n元向量from、to和一个整数top,我们就可以使用标识来初始化向量data[0....n-1]。如果元素data[i]已始化,那么from[i] < top并且 to[from[i]] =i.因此,from是一个简单的标识,to和top一起确保了from中不会被写入内存里的随机内容,变量top初始为0。下图中data的空白项未被始化:

[cpp] view plaincopy

  1. from[i] =top;    
  2. to[top] = i;    
  3. data[i] = 0;    
  4. top ++;   

       假如我现在要判断data[5]=8是否被初始化过,那么我先看to[from[5]]是否等于5,同理data[3]就判断to[from[3]]是否等于3就可以了。

10、在成本低廉的隔日送达时代之前,商店允许顾客通过电话订购商品,并在几天后上门自取。商店的数据库使用客户的电话号码作为其检索的主关键字(客户知道他们自己的电话号码,而且这些关键字集合都是唯一的)。你如何组织商店的数据库,以允许高效的插入和检索操作?

答:

11、在20世纪80年代早期,工程师们每天都要将许多由计算机辅助设计(CAD)系统生成的图纸从工厂送到位于XXX的测试站。虽然仅有40公里远,但使用汽车快递服务每天都需要一个小时的时间(由于交通阻塞和山路崎岖),花费100美元。请给出信道数据传输方案并估计每一种方案的费用。

答:个人觉得时间与金钱的关系类似于空间与时间的关系。最优的方法一定是在两者之间折中,如果实时性要求没有那么高,可以找便宜的方式运。

12、载人航天的先驱意识到需要在外太空的极端环境下实现顺利书写。民间盛传美国花费100万美元研发出了一种特殊的钢笔来解决这个问题。那么,前苏联又会如何解决相同的问题?

答:为什么要用钢笔??用铅笔不就行了。。省钱省力

猜你喜欢

转载自blog.csdn.net/qq_30796379/article/details/81270828