第一章 引论

1. 选择问题(selection problem)

设有一组n个数而要确定其中的第k个最大者。我们称之为选择问题。
方法1:将这n个数读到一个数组里,再通过简单的算法,比如冒泡排序法,以递减顺序将数组排序,然后返回位置k上的元素。
方法2:可以先把前k个元素读入数组(以递减的顺序)对其进行排序。接着,将剩下的元素再逐个读入。当新元素被读到时,如果它小于数组中的第k个元素将被忽略,否则将数组的一个元素挤出数组。当算法终止时,位于第k个位置上的元素作为答案返回。
因为有更好的算法,故此处不实现

2. 字谜问题

解决一个流行的字谜,输入是由一些字母和单词的二维数组组成。目标是要找出字谜中的单词,这些单词可能是水平、垂直、或沿对角线以任何方式放置的。作为例子下表中所示的字谜由单词“this”,”two”,”fat”和“that”组成。单词this从(1,1)开始,并延伸至(1,4),单词two从(1,1)到(3,1),fat从(4,1)到(2,3),that从(4,4)到(1,1)。

1 2 3 4
1 t h i s
2 w a t s
3 o a h g
4 f g d t

方法1:对单词表中的每一个单词,我们检查每个有序三元组(行,列,方向),验证是否有单词存在。

方法2:对每一个有序四元组(行,列,方向,字符数)我们可以测试所指的单词是否在单词表中。
因为有更好的算法,故此处不实现

3. 递归的基本法则

  • 基准情况(base case),也就是递归的终止条件。
  • 不断推进(making progress),若用递归求解问题,则此问题需要朝着基准情况不断的前进。

若递归函数嵌套相同参数的同样的递归函数,此时形成一个循环,就违反了此原则。
此外,有些情况下,并不能归到基准情况,此时递归也是不正确的。比如:
下面是一个递归函数的例子:

  1. int
  2. F(int x){
  3. if(x==0)
  4. return 0;
  5. else
  6. return 2*F(x-1)+x*x;
  7. }

下面是一个无终止递归函数的例子:(因为并不是所有的情况都归到基准情况)

  1. int
  2. Bad(unsigned int N){
  3. if(N==0)
  4. return 0;
  5. else
  6. return Bad(N/3+1)+N/1;
  7. }
  • 合成效益法则(compound interest rule),对于同一问题,切勿出现重复计算的工作,否则将极大的影响效率,比如:递归求解的菲波那切数列。

4. 模运算

同余问题

若N可以整除 A-B,那么我们说 A与B 模 N 同余(congruent),记做 A≡B(mod N)。
也就是说A和B关于N的余数相同。
用此方法可以方便的解决习题 1.8

取模的等价运算

N%10=N-[N/10]*10。([x]意为小于或等于x的最大整数) //因为公式输入问题,有些不标准,但是大概这个意思

习题

说明

有些题目涉及到数学证明,有些比如C语言的文件操作,不太常用,故而不写出。
更多的习题答案可以参考:
数据结构与算法分析(2)算法效率问题引入与递归简论
数据结构与算法分析—c语言描述_课后答案.pdf

1.3 只适用处理IO的PrintDigit 函数,编写一个过程以输出任意实数(可以是负的)

首先,我们看到输出的是任意实数,所以我们可以用 double 型的变量来表示。
再者,要明确 PrintDigit 函数是只处理单个数字(非负)并将其输出到终端,也就是说处理的数字范围是 0<=N<10.
还有,若一个数用 double 类型表示,比如 double x=8.1 ,由于计算机的存储特性,则 x 在计算机中不一定是 8.1,还有可能是一个接近 8.1 的数,比如 8.09999999..,所以我们需要通过某种方式抵消这种误差,不影响我们问题的解决。详细见:只使用I/O的PrintDigit函数,编写一个过程以输出任意实数

  1. #include <iostream>
  2. #include <stdio.h>
  3. #include <string.h>
  4. using namespace std;
  5. void PrintDigit(int n)
  6. {
  7. cout<<n;
  8. }
  9. //DcePlaces 指的是有几位小数
  10. double RoundUp(double N, int DecPlaces) // 用于消除存储误差
  11. {
  12. int i;
  13. double AmountToAdd=0.5;
  14. for(i=0;i<DecPlaces;i++)
  15. AmountToAdd/=10;
  16. return N+AmountToAdd;
  17. }
  18. int IntPart(double N)//取实数的整数部分
  19. {
  20. return (int)N;
  21. }
  22. double DecPart(double N)//取实数的小数部分
  23. {
  24. return N-(int)N;
  25. }
  26. void PrintFractionPart(double FractionPart, int DectPlaces)
  27. {
  28. int i,Adigit;
  29. for (i = 0; i < DectPlaces; i++)//对于小数部分的打印
  30. //将小数部分一个接一个的移入整数位,一次仅仅移入一个,然后调用 PrintDigit 进行打印
  31. //而不是根据小数位数一次性的将所有的小数部分都移入整数部分,进行打印的原因是:
  32. //避免不必要的递归
  33. {
  34. FractionPart*=10;
  35. Adigit=IntPart(FractionPart);
  36. PrintDigit(Adigit);
  37. FractionPart=DecPart(FractionPart);
  38. }
  39. }
  40. void PrintOut(unsigned int n)//递归的打印,契合 PrintDigit 函数的限制条件
  41. {
  42. if(n>=10)
  43. PrintOut(n/10);
  44. PrintDigit(n%10);
  45. }
  46. void PrintReal(double N, int DecPlaces)
  47. {
  48. int IntergerPart;
  49. double FractionPart;
  50. if(N<0)//对于负数的处理
  51. {
  52. putchar('-');
  53. N=-N;
  54. }
  55. N=RoundUp(N, DecPlaces);
  56. IntergerPart=IntPart(N); FractionPart=DecPart(N);
  57. PrintOut(IntergerPart);
  58. if(DecPlaces>0)
  59. putchar('.');
  60. PrintFractionPart(FractionPart, DecPlaces);
  61. }
  62. int main()
  63. {
  64. PrintReal(-8.101,3);
  65. cout<<endl;
  66. return 0;
  67. }

参考了标准答案

2019.12.30 复习所写:

1.3 P9.cpp
感想

  • 函数的命名:如果涉及到某个变量的操作,则采用 动词+名词的形式。若是为了说明函数的概况,则一般采用 形容词+名词的形式。
  • 若函数对某个变量仅仅做了形式上的变换(比如大小写,取整数部分等),则一般应该保留原变量,返回新变量,而不应该在原变量基础上修改。

1.8 求 2^100(mod 5) 是多少

image.png
参考了标准答案