Java-数学学习手册-二-

58 阅读22分钟

Java 数学学习手册(二)

原文:Learn Java with Math

协议:CC BY-NC-SA 4.0

十三、Java 基础项目

  1. 编写代码打印出下图。

img/485723_1_En_13_Figa_HTML.jpg

  1. 编写代码来绘制以下形状。

img/485723_1_En_13_Figb_HTML.jpg

  1. 写代码做这个三角形。

img/485723_1_En_13_Figc_HTML.jpg

  1. 写一个函数来检查一个整数是否能被另一个整数整除。

    例如:

    (1)给定输入 10,5,输出为“是,2”;

    (2)给定输入 11,2,输出为"否";

  2. 写 Java 代码画圣诞树。

img/485723_1_En_13_Figd_HTML.jpg

  1. 写代码找出给定正整数的所有因子。

    例如,当用户输入“10”时,你的程序应该输出 1,2,5,10

  2. 编写一个方法,该方法接受一个月(即 1 到 12 之间的整数)作为参数,并返回当前年份中该月的天数。

  3. 编写代码,生成从控制台输入的两个数字的乘积:

    请输入两个数进行乘法运算

    下一个数字- > 7

    下一个数字- > 15

    产品= 105

  4. 写一个检查正整数是否是质数的方法。

  5. 编写一个方法,将一个整数转换成用二进制表示的字符串。例如,给定一个参数“19”,它应该返回“10011”。

十四、Java 基础解决方案

以下是本书 Java 基础部分前几章的解决方案。

第章第五章:变量

  1. 数字电子设备的信号(开/关)

  2. OOP 基于类;国际网络路跑联盟

  3. (e)

  4. (b)

第七章:输入和输出

  1. name     age    height
    Anthony  17     5'9"
    Belly    17     5'6"
    Bighead  16     6'
    
    
  2. a   b   c
    \\
    '
    """
    
    

第八章:循环结构–For 循环

  1. 内部循环有三次迭代,没有回车;外部循环有两次迭代。

  2. 减去一个后,你会发现一个清晰的模式。

  3. 立方数模式

  4. 所有数字加 1

  5. 嵌套循环

第九章:循环结构–While 循环

  1. 永远(死循环)

  2. 三次,x = 2,4,16

  3. 五次,最终输出是“bbbbbabbbbb”

  4. 六次,当 x = 100,50,25,12,6,3

  5. 8 3 ←中间有个空格

第十章:逻辑控制结构

十五、莱特兄弟的掷硬币游戏

编程帮助我们理解和解释许多复杂的问题。你可以找到一个有趣的在线视频,“抛硬币难题”,它讲述了一个历史故事,并用分析方法解释了一个概率问题的解决方案。这个故事是关于莱特兄弟,奥维尔和威尔伯,他们玩抛硬币游戏来决定谁应该首先开始新的飞行实验。他们连续掷硬币,直到奥维尔连续得到双头,或者威尔伯在“相邻”序列中得到一个头和一个尾。虽然视频使用了概率和代数概念来计算莱特兄弟的获胜优势,但我们将尝试使用 Java 编程进行实验。下面的方法模拟了莱特兄弟的游戏,并分析了他们的结果(注意有用的注释,用//标记,这样我们可以更容易地阅读代码)。

private static int count_a, count_b, count_ab = 0;
private static int totalsteps_a, totalsteps_b = 0;

/// whoever gets below pattern first wins, or tie if both of them reach targeted patterns at the same round
/// a: HH wins; b: HT wins; Use boolean 'true': head, 'false': tail
public static void flipCoin()
{
       Random r = new Random();
       /// initial value, or first round result
       boolean current_a = r.nextBoolean();
       boolean current_b = r.nextBoolean();

       boolean win_a = false;
       boolean win_b = false;
       int round = 1;
       while(true) {
              round++;
              boolean next_a = r.nextBoolean();
              boolean next_b = r.nextBoolean();
              if (current_a && next_a) {
                     win_a = true;
              }
              if (current_b && !next_b) {
                     win_b = true;
              }
              if (win_a && win_b) {
                     System.out.println("Both WIN! - round: " + round);

                     count_ab++;
                     totalsteps_a += round;
                     totalsteps_b += round;
                     break;
              }
              if (win_a && !win_b) {
                     System.out.println("A WIN! - round: " + round);
                     count_a++;
                     totalsteps_a += round;
                     break;
              }
              if (!win_a && win_b) {
                     System.out.println("B WIN! - round: " + round);
                     count_b++;
                     totalsteps_b += round;
                     break;
              }
              current_a = next_a;
              current_b = next_b;
       }
}

使用下面的main方法,我们可以收集样本并得到统计摘要。

public static void main(String[] args) {
       final int MAX = 10000;
       for(int i=0; i < MAX; i++) {
              flipCoin();
       }
       System.out.println("Summary");
       System.out.println("Total samples: " + MAX);
       System.out.println("Winning counts: a - " + count_a + "; b - " + count_b + "; ab - " + count_ab);
       int probability_a = count_a * 100 / (count_a + count_b);
       int probability_b = count_b * 100 / (count_a + count_b);
       System.out.println("Winning probability: HH=" + probability_a + "%; HT=" + probability_b + "%.");
       double average_a = totalsteps_a / (count_a + count_ab);
       double average_b = totalsteps_b / (count_b + count_ab);
       System.out.println("Average rounds to win: HH=" + average_a + "; HT=" + average_b + ".");
}

在我们用不同的参数运行了许多实验之后,我们了解了实际发生的情况,然后可以得出结论,威尔伯赢得赌注的机会明显更高(大约 62%比 37%)。输出应该如下所示。

.........
B WIN! - round: 3
A WIN! - round: 2
A WIN! - round: 3
B WIN! - round: 2
B WIN! - round: 4
A WIN! - round: 4
B WIN! - round: 2
B WIN! - round: 2
B WIN! - round: 3
B WIN! - round: 2
A WIN! - round: 2
A WIN! - round: 2
Both WIN! - round: 2
B WIN! - round: 3
Summary
Total samples: 10000

Winning counts: a - 3213; b - 5386; ab - 1401
Winning probability: HH=37%; HT=62%.
Average rounds to win: HH=2.0; HT=3.0.

十六、毕达哥拉斯三元组

勾股定理在小学到中学的学生中是众所周知的,因为它看起来很优雅,适用于所有的直角三角形。

数学:毕达哥拉斯三元组

在直角三角形内,a 和 b 是两条边,c 是斜边。

  • a2+b2= c2

当 a,b,c 是满足勾股定理的正整数时,(a,b,c)称为“勾股三元组”。显然(3,4,5)是第一个毕达哥拉斯三元组,接下来是(5,12,13),(6,8,10),以此类推。毕达哥拉斯三元组的数目是无限的。

例子

那么,我们如何找到所有低于 100 的毕达哥拉斯三元组呢?

回答

我们可以将(3,4,5)乘以任意整数得到(6,8,10),(9,12,15),…,(57,76,95)。我们可以用(5,12,13)作为另一个基三元组得到(10,24,26),…,(35,84,91)。等等。

但是这种方法将要求我们首先找出所有的基本毕达哥拉斯三元组。因此,我们基本上必须检查 a 的每个小于 100 的正整数,然后计算 b 和 c,假设 a < b < c。顺便说一下,a = b 是不可能的。然而,有了编程方法,它不再是一个具有挑战性的数学问题。

这是找出满足勾股定理的所有可能三元组(a,b,c)的方法。

       private static int allPythagoreanNumbers(int upperBound) {
              int count = 0;
              for(int a = 1; a < upperBound; a++) {
                     for(int b = a; b < upperBound; b++) {
                            for(int c = b; c < upperBound; c++) {
                                   if (a * a + b * b == c * c) {
                                          System.out.println("("+a+", "+b+", "+c+")");
                                          count++;
                                   }
                            }
                     }
              }
              return count;
       }

       public static void main(String[] args) {
              System.out.println("Total count: " + allPythagoreanNumbers(100));
       }

它将输出如下内容:

  • (3, 4, 5)

  • (5, 12, 13)

  • (6, 8, 10)

  • (7, 24, 25)

  • (8, 15, 17)

  • (9, 12, 15)

  • (9, 40, 41)

  • (10, 24, 26)

  • (11, 60, 61)

  • (12, 16, 20)

  • (12, 35, 37)

  • (13, 84, 85)

  • (14, 48, 50)

  • (15, 20, 25)

  • (15, 36, 39)

  • (16, 30, 34)

  • (16, 63, 65)

  • (18, 24, 30)

  • (18, 80, 82)

  • (20, 21, 29)

  • (20, 48, 52)

  • (21, 28, 35)

  • (21, 72, 75)

  • (24, 32, 40)

  • (24, 45, 51)

  • (24, 70, 74)

  • (25, 60, 65)

  • (27, 36, 45)

  • (28, 45, 53)

  • (30, 40, 50)

  • (30, 72, 78)

  • (32, 60, 68)

  • (33, 44, 55)

  • (33, 56, 65)

  • (35, 84, 91)

  • (36, 48, 60)

  • (36, 77, 85)

  • (39, 52, 65)

  • (39, 80, 89)

  • (40, 42, 58)

  • (40, 75, 85)

  • (42, 56, 70)

  • (45, 60, 75)

  • (48, 55, 73)

  • (48, 64, 80)

  • (51, 68, 85)

  • (54, 72, 90)

  • (57, 76, 95)

  • (60, 63, 87)

  • (65, 72, 97)

  • 总数:50

这只是我们如何使用程序解决问题的众多演示之一。

问题

  1. 在示例中,我们使用三个for-循环从 1 到 99 迭代 a、b、c。你如何通过减少到两个for-循环来改进它?

  2. 利用例子中的思想,我们如何找出所有小于 100 的毕达哥拉斯素数?毕达哥拉斯素数解释如下。

数学:毕达哥拉斯质数

毕达哥拉斯素数是两个平方的和。并且,它需要是 4n + 1 的形式,其中 n 是正整数。毕达哥拉斯素数的例子有 5,13,17,29,37 和 41。

印度人

利用示例代码,看看如何进行小的更改来找到解决方案。

十七、强类型编程

正如我们在本书第一部分开始时了解到的,Java 编程语言已经定义了整数、双精度、布尔、字符串等。,作为基本类型。在 Java 中,如果没有预先定义变量的类型,我们就不能给变量赋值。只有在一个变量被一个类型明确定义后,例如,integer,我们才被允许给它分配一个整数值,并开始在计算中使用它作为一个整数。从逻辑上讲,一旦定义了变量的类型,就不能再给它赋不同类型的值。例如,如果一个变量被定义为布尔值,它就不能被赋予整数值。否则,我们将得到一个类型不匹配的编译错误。

铅字铸造

但是,如果一个变量被定义为 double,我们怎么给它赋一个整数值呢?让我们做一些实验。

       public static void main(String[] args) {
              double a = 5;
              System.out.println(a);
              double b = 3 * 5;
              System.out.println(b);
              double x = 5 / 3;
              System.out.println(x);
              double y = (double)(5 / 3);
              System.out.println(y);
              double z = (double)5 / 3;
              System.out.println(z);
              double t = 5.0 / 3;
              System.out.println(t);
              double u = 5 / 3d;
              System.out.println(u);
       }

这是输出:

img/485723_1_En_17_Figa_HTML.jpg

下面的模式是我们从这个实验中学到的:

  • 当整数 5 被赋给双精度类型变量时,该变量将得到一个带小数点表示的等效值,即 double value 5.0。

  • 当一个整数除以另一个整数时,结果遵循相同的整数类型,例如 5 / 3 = 1。但是,当分数“5 / 3”被赋给双精度类型变量时,结果值将自动转换为双精度值 1.0。

  • (double)(5 / 3)(5/ 3)的整数结果转换为双精度值。这叫做类型转换。结果是 1.0,一个双精度值。

我们如何从 5 / 3 得出一个精确的值?诀窍是用(double)5 / 3,而不是5 / 3(double)5 / 3的结果是一个双精度值。或者,您可以使用 5.0 / 3 来产生相同的结果。另一种方式是5d / 3,或5 / 3d。两个表达式的结果都是相同的双精度值。

数学:直线的斜率

在 x-y 2D 笛卡尔坐标系中,点(x1,y1)和(x2,y2)之间的直线的斜率等于(y2 - y1) / (x2 - x1)。

例子

实现一个名为double getSlope()的公共方法,该方法返回一条线的斜率。如果两个点有相同的 x 坐标,分母为零,斜率未定义,所以在这种情况下应该抛出一个IllegalArgumentException。这将停止您的程序运行,并显示指定的错误信息。

回答

在一个Line类中,我们定义了两个点和一个构造函数:

       private Point p1;
       private Point p2;
       public Line(Point p1, Point p2) {
              this.p1 = p1;
              this.p2 = p2;
       }

Point类被设计为:

public class Point {
       private int x;
       private int y;
       public Point() {
       }

       public void setX(int x) {
              this.x = x;
       }

       public int getX() {
              return x;
       }

       public void setY(int y) {
              this.y = y;
       }

       public int getY() {
              return y;
       }
}

现在我们在Line类中添加一个名为getSlope()的方法。

public double getSlope() {
       if (this.p1.getX() == this.p2.getX()) {
              throw new IllegalArgumentException("Denominator cannot be 0");
       }
       return (double)(this.p2.getY() - this.p1.getY()) / (this.p2.getX() - this.p1.getX());
}

该方法看起来很容易,但棘手的部分是我们将两个整数相除的结果转换为 double 值,也就是说,

(double)(this.p2.getY() - this.p1.getY()) / (this.p2.getX() - this.p1.getX())

数学:共线性

如果可以画一条直线来连接点,那么这些点就是共线的。两个基本的例子是三个点具有相同的 x 或 y 坐标。更一般的情况可以通过计算每对点之间的直线的斜率并检查所有对点的斜率是否相同来确定。

我们使用公式(y2 - y1) / (x2 - x1)来确定两点(x1,y1)和(x2,y2)之间的斜率。

将以下方法添加到您的Line类中:

public boolean isCollinear(Point p)

如果给定点与这条线的点共线,它需要返回 true。

十八、条件语句

如何识别和表示 x 和 y 这两个数中较大的那个数?

数学:假设和结论

在数学公式中,我们必须引入绝对符号来构成表达式:

x 和 y 之间较大的数字img/485723_1_En_18_Figa_HTML.gif \frac{\left(\mathbf{x}+\mathbf{y}\right)+\mid \mathbf{x}\hbox{--} \mathbf{y}\mid }{\mathbf{2}}

回忆一下if / else结构:

if (x >= y) {
       // x is the bigger number
} else {
       // y is the bigger number
}

它非常简单易懂。

if / else结构遵循从假设到结论的常见实验。

       if (<Hypothesis>) {
              <Conclusion>
       } else {              // the hypothesis is NOT valid
              <Different conclusion>
       }

<Hypothesis>部分需要是一个布尔值,在一个数学表达式中可以包含一个变量或多个变量。

条件语句有几种类型的结构(有些你已经见过,有些对你来说是新的)。

  • 简单的if,或if / else子句。

  • 稍微复杂一点的if / else if阶梯。

    if (...) {
           ......
    } else if (...) {
           ......
    } else if (...) {
           ......
    } else {
           ......
    }
    
    
  • 嵌套的if / else语句。

    if (...) {
           ......
    } else {
           if (...) {
                  ......
           } else {
                  ......       ......
           }
    }
    
    

最后一种模式用于实现树状结构。当我们决定使用哪种模式时,这将取决于我们所解决的问题的类型。

例子

下面这段代码有什么问题吗?

       if (i > 50) {
              <do something...>
       } else if (i > 100) {
              <do something...>
       } else {
              <do something...>
       }

回答

i <= 50的时候,永远不会是i > 100,所以中间的else if分支其实是一条死路。一个简单的修正应该是交换代码中“50”和“100”的位置。而且注意这个,当它在下面的代码块中说else if (i > 50) {...}的时候,实际上是指 50 < i < = 100。

       if (i > 100) {
              <do something...>
       } else if (i > 50) {
              <do something...>
       } else {
              <do something...>
       }

例子

创建一种将学生的分数(0 到 100 的整数)映射到标准 GPA 分数的方法。

回答

第一种解决方案(v0)使用了几个if子句。问题是,例如,当分数为 69 时,它必须执行所有四个if条款。这不是一个有效的方法。

       public static char getGpaScore_v0(int points) {
              if (points > 89) {
                     return 'A';
              }
              if (points < 90 && points > 79)       {
                     return 'B';
              }
              if (points < 80 && points > 69)       {
                     return 'C';
              }
              if (points < 70 && points > 64)       {
                     return 'D';
              }
              // if (points < 65) <-- this line can be omitted
              return 'F';
       }

第二个解决方案(v1)利用了一个嵌套的"if / else"语句。它解决了从早期版本中观察到的问题- v0

       public static char getGpaScore_v1(int points) {
              if (points > 89) {
                     return 'A';
              } else {
                     if (points > 79) {
                            return 'B';
                     } else {
                            if (points > 69) {
                                   return 'C';
                            } else {
                                   if (points > 64) {
                                          return 'D';
                                   } else {
                                          return 'F';
                                   }
                            }
                     }
              }
       }

为了给代码结构提供更好的可读性,引入了第三种解决方案(v2),如下所示。

       /*
        * 90 to 100 --- A
        * 80 to 89        --- B
        * 70 to 79        --- C
        * 65 to 69  --- D
        * below 65  --- F
        */
       public static char getGpaScore_v2(int points) {
              if (points > 89) {
                     return 'A';
              } else if (points > 79) {
                     return 'B';
              } else if (points > 69) {
                     return 'C';
              } else if (points > 64) {
                     return 'D';
              } else {
                     return 'F';
              }
       }

数学:象限

在笛卡尔坐标系中,象限由 x 和 y 坐标是正数还是负数决定。有四个象限,由 x 轴和 y 轴分隔。具体来说,所有的点(x > 0,y > 0)都属于象限 I(或第 1 象限);所有的点(x < 0, y > 0)都属于第二象限(或第二象限);所有点(x < 0, y < 0) belong to quadrant III (or 3rd quadrant); and all the points (x > 0,y < 0)属于象限 IV(或第四象限)

例子

能不能写一个方法来识别任意给定点(x,y)在坐标系上属于哪个象限?x 和 y 都是实数。如果一个点落在 x 轴或 y 轴上,那么该方法应该返回 0。

回答

在这个例子中有两个变量,xy,。将 x 和 y 定义为浮点数类型。进行如下所示的案例分析:

情况 1:当一个点落在 x 轴或 y 轴上时img/485723_1_En_18_Figa_HTML.gif y = 0 或 x = 0

情况 2:当一个点落在第一象限img/485723_1_En_18_Figa_HTML.gif x > 0,y > 0 时

情况 3:当一个点落在第二象限img/485723_1_En_18_Figa_HTML.gif x < 0,y > 0 时

情况 4:当一个点落在第三象限img/485723_1_En_18_Figa_HTML.gif x < 0,y < 0 时

情况 5:当一个点落在第四象限img/485723_1_En_18_Figa_HTML.gif x > 0,y < 0 时

将情况 2 和情况 5 组合成 x > 0 的类别,将情况 3 和情况 4 组合成 x < 0 的类别,产生如下代码结构:

       private static int quadrant(float x, float y) {
              if (x == 0 || y == 0) {
                     return 0;
              }
              else if (x > 0)              // x > 0 and y <> 0
              {
                     if (y > 0) {
                            return 1;
                     }
                     return 4;
              }
              else                         // x < 0 and y <> 0
              {
                     if (y > 0) {
                            return 2;
                     }
                     return 3;
              }
       }

它使用float来保存xy值,尽管它也可以使用double来这样做。floatdouble都是用于存储浮点数的数值数据类型。double类型需要两倍于float类型的空间,因为每种float类型的数据用 32 位表示,而一种double类型的数据使用 64 位

三元运算符

Java 使您能够直接从布尔表达式中赋值(真或假)。这被称为三元运算符。例如:

       int a, b, max;
       max = a < b? b : a;

这暗示着,当a < bmax = b;否则max = a

该语法保存一个if / else语句。例如,我们可以使用下面的方法来获得绝对值:

       public int getAbsolutionValue(int a) {
              if (a < 0) {
                     return -a;
              }
              else {
                     Return a;
              }
       }

但是通过使用三元运算符,我们只用一行代码就可以完成:

       a = a < 0 ? -a : a;

问题

  1. 请重写如下代码,以提高其逻辑性和可读性(num为整数值)。

           if (num < 10 && num > 0) {
                  System.out.println("It's an one digit number");
           }
           else if (num < 100 && num > 9) {
                  System.out.println("It's a two digit number");
           }
           else if (num < 1000 && num > 99) {
                  System.out.println("It's a three digit number");
           }
           else if (num < 10000 && num > 999) {
                  System.out.println("It's a four digit number");
           }
           else {
                  System.out.println("The number is not between 1 & 9999");
           }
    
    
  2. Take the following three if statements:

    if (a == 0 && b == 0) {...}
    if (a == 0 && b != 0) {...}
    if (a != 0 && b != 0) {...}
    
    

    请简化代码逻辑,将它们组合在一起。

十九、switch语句

利用switch条件语句代替if语句有时可以呈现更清晰的代码逻辑。当我们有一个变量或一个包含变量的表达式,这些变量可能有不同的结果值,后面跟着不同的动作,这是一个使用switch的好机会。

switch (<expression>) {
       case <result 1>:
              <action 1>;
              break;
       case <result 2>:
              <action 2>;
              break;
       ......
       case <result n>:
              <action n>;
              break;
       default:
              <other action>;
              break;
}

switch 语句的一个简单应用是在x = 1x = 2x = 3、……的情况下采取不同的动作,如下所示:

       switch (x) {
              case 1: ...;
              case 2: ...;
              case 3: ...;
              default: ...;
       }

例子

编写一个方法,在给定整数值输入的情况下,用英语单词表达式打印出月份。

回答

我们可以使用if / else梯形语句将一个整数转换成月份名称。

       public static void tellNameOfMonthByIfElse(int month) {
              if (month == 1) {
                     System.out.println("January");
              } else if (month == 2) {
                     System.out.println("February");
              } else if (month == 3) {
                     System.out.println("March");
              } else if (month == 4) {
                     System.out.println("April");
              } else if (month == 5) {
                     System.out.println("May");
              } else if (month == 6) {
                     System.out.println("June");
              } else if (month == 7) {
                     System.out.println("July");
              } else if (month == 8) {
                     System.out.println("August");
              } else if (month == 9) {
                     System.out.println("September");
              } else if (month == 10) {
                     System.out.println("October");
              } else if (month == 11) {
                     System.out.println("November");
              } else if (month == 12) {
                     System.out.println("December");
              } else {
                     System.out.println("Unknown month");
              }
       }

如果我们利用switch条件语句来做同样的翻译,它也会工作得很好。

public static void tellNameOfMonthBySwitch(int month) {
        String nameOfMonth;
        switch (month) {
            case 1:  nameOfMonth = "January";
                     break;
            case 2:  nameOfMonth = "February";
                     break;
            case 3:  nameOfMonth = "March";
                     break;
            case 4:  nameOfMonth = "April";
                     break;
            case 5:  nameOfMonth = "May";
                     break;
            case 6:  nameOfMonth = "June";
                     break;
            case 7:  nameOfMonth = "July";
                     break;
            case 8:  nameOfMonth = "August";
                     break;
            case 9:  nameOfMonth = "September";
                     break;
            case 10: nameOfMonth = "October";
                     break;
            case 11: nameOfMonth = "November";
                     break;
            case 12: nameOfMonth = "December";
                     break;
            default: nameOfMonth = "Unknown month";
                     break;
        }
        System.out.println(nameOfMonth);
}

有一个更好的方法。不如我们定义一个数组来存储用英文表示所有月份的名称字符串列表,也就是数组nameOfMonth。我们实际上是在整数(从 0 到 12)和月份名称字符串之间构建一个映射表。由于数组从索引 0 开始,我们有意将"none"赋给数组中的第一个元素。

       private static String[] nameOfMonth = new String[] {
                     "none", "January", "February", "March", "April", "May", "June",
                     "July", "August", "September", "October", "November", "December"
              };

       public static void main(String[] args) {
              System.out.println(nameOfMonth[1]);    // January
              System.out.println(nameOfMonth[8]);    // August
       }

例子

给定两个整数输入:年和月,编写一个方法来返回一个月中的天数。

回答

使用switch条件语句:

       public static int tellNumberOfDaysByYearMonth(int year, int month) {
              int numOfDays = 0;
              switch (month) {
                   case 1: case 3: case 5:
                   case 7: case 8: case 10:
                   case 12:
                          numOfDays = 31;
                          break;
                   case 4: case 6:
                   case 9: case 11:
                          numOfDays = 30;
                          break;
                   case 2:
                          if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
                                 numOfDays = 29;
                          } else
                                 numOfDays = 28;
                          }
                          break;
                   default:
                          break;
              }
              return numOfDays;
       }

请注意,它有一个特殊的逻辑来处理闰年的二月。由于计算二月份总天数的复杂性,当我们将前面提到的静态数组方法应用于这种情况时,我们必须执行以下操作:

private static int[] numberOfDaysByMonth = new int[] {
                    0,      // none
                    31,     // January
                    28,     // February
                    31,     // March
                    30,     // April
                    31,     // May
                    30,     // June
                    31,     // July
                    31,     // August
                    30,     // September
                    31,     // October
                    30,     // November
                    31      // December
};

在使用整数数组之前,我们需要在运行时根据年份值修改二月的值:

       if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
              numberOfDaysByMonth[2] = 29;
       }

问题

使用 switch 条件语句编写以下代码。

char color ='C';
       if (color=='R') {
              System.out.println("The color is red");
       }
       else if(color=='G') {
       System.out.println("The color is green");
       }
       else if(color=='B') {
              System.out.println("The color is black");
       }
       else {
       System.out.println("Some other color");
       }

二十、追踪移动物体

Java 提供了一个基本的编码框架,比如forwhile循环和ifswitch条件语句。我们可以利用它们来记录移动物体的时间。首先,我们将处理一个常见的数学问题——弹跳球场景。

数学:弹跳球

在纯数学方法中,我们会建立一个表格来记录每次反弹后的高度。并没有那么难,但是如果我们改变了原始问题设置中的高度值,我们将不得不手动重新计算同一表格中的值。

例子

一个球从 3 米高处落下。在第一次弹跳时,它上升到 2 米的高度。它一直下落,并反弹到上次反弹时达到的 2/3 高度。在哪个弹跳上会上升到不到 0.5 米的高度?这个问题选自过去的 AMC 8(8 年级以下的美国数学竞赛)

回答

这种编程方法将大大减少重复性的手工工作。

       public static void main(String[] args) {
              System.out.println(ballBouncing(3.0));
       }

       private static int ballBouncing(double originalHeight) {
              double currentHeight = originalHeight;
              int count = 0;
              while(currentHeight > 0.5) {
                     currentHeight = currentHeight * 2 / 3;
                     count++;
                     System.out.println("Bounce No=" + count +
                                   "; current height=" + currentHeight);
              }
              return count;
       }

当您执行它时,输出显示每次反弹后的当前高度。

Bounce No=1; current height=2.0
Bounce No=2; current height=1.3333333333333333
Bounce No=3; current height=0.8888888888888888
Bounce No=4; current height=0.5925925925925926
Bounce No=5; current height=0.3950617283950617
5

改变参数originalHeight并重新执行相同的程序会迅速输出详细的结果。这比用传统的数学方法在纸上求解要有效得多。

例子

一只蜗牛试图从井里爬出来。每天它爬上井边 4 英尺,每晚它滑下井 2 英尺 6 英寸。如果蜗牛早上从 40 英尺深的地方开始,它需要多少天才能从井里出来?这个问题选自一个数学竞赛。

回答

为了保持使用整数值,我们通过计算将英尺转换为英寸。注意,我们通过使用关键字final将井的深度设置为一个常量变量。我们在蜗牛每天爬上来之后,滑下去之前,检查它是否到达了井口。

       private static void snail() {
              final int DEPTH = 12 * 40;
              int currentHeight = 0;
              int numOfDays = 0;
              while (currentHeight < DEPTH) {
                     currentHeight += 12 * 4;
                     numOfDays++;
                     if (currentHeight >= DEPTH) {
                            break;
                     }
                     currentHeight -= 12 * 2 + 6;
                     System.out.println("No. " + numOfDays + " day - " +
                                   (DEPTH - currentHeight) + " inches to the top.");
              }
              System.out.println("No. " + numOfDays + " day - at the top.");
       }

这是程序运行时的部分输出:

............

No. 1 day - 462 inches to the top.
No. 2 day - 444 inches to the top.
No. 3 day - 426 inches to the top.
No. 4 day - 408 inches to the top.
No. 5 day - 390 inches to the top.
No. 6 day - 372 inches to the top.
No. 7 day - 354 inches to the top.
No. 8 day - 336 inches to the top.
No. 9 day - 318 inches to the top.
No. 10 day - 300 inches to the top.
No. 11 day - 282 inches to the top.
No. 12 day - 264 inches to the top.
No. 13 day - 246 inches to the top.
No. 14 day - 228 inches to the top.
No. 15 day - 210 inches to the top.
No. 16 day - 192 inches to the top.
No. 17 day - 174 inches to the top.
No. 18 day - 156 inches to the top.
No. 19 day - 138 inches to the top.
No. 20 day - 120 inches to the top.
No. 21 day - 102 inches to the top.
No. 22 day - 84 inches to the top.
No. 23 day - 66 inches to the top.
No. 24 day - 48 inches to the top.
No. 25 day - at the top

.

二十一、计算

我们学习了许多数学方法来解决计数问题。其中有些问题需要对排列组合有深入的理解。在本章中,我们将学习如何使用编程解决计数问题的例子。

例子

公共汽车票价分别为 4 美元和 6 美元。总共卖出了 45 张票,赚了 230 美元。卖出了多少张 4 美元的票?(2007/MathIsCool 问题在 http://academicsarecool.com )

回答

这可以通过单个循环来解决。我们将变量tickets设置为4.00门票的数量。因为票的总数是45,所以4.00 门票的数量。因为票的总数是 45,所以4.00 的票的数量不能大于 45。所以,tickets是 46 以下的整数。

private static void calculateBusTickets() {
       for(int tickets = 0; tickets < 46; tickets++) {
              int totalMoney = 4 * tickets + 6 * (45 - tickets);
              if (totalMoney == 230) {
                     System.out.println(tickets + " $4.00 tickets were sold.");
                     break;
              }
       }
}

例子

椅子有 4 条腿,凳子有 3 条腿,桌子有 1 条腿。生日聚会,每桌 4 把椅子,总共 18 件家具。其中一个孩子数出总共有 60 条腿。有多少凳子?(2016/MathIsCool 问题在 http://academicsarecool.com )

回答

在下面的方法中,变量tables表示表格的数量。那么,椅子的数量是 4 × tables,凳子的数量是(18—tables—4 *tables)。

private static void countFurniture() {
       for(int tables = 0; tables < 19; tables++) {
              if (tables + 4 * 4 * tables + 3 * (18 - tables - 4 * tables) == 60) {
                     System.out.println((18 - tables) + " stools.");
                     break;
              }
       }
}

例子

多项选择题考试由 20 道题组成。每个正确答案的得分为+5,每个错误答案的得分为-2,每个未回答的问题的得分为 0。约翰的考试分数是 48 分。他最多能答对多少个问题?(1987/AMC8 问题在 https://artofproblemsolving.com/wiki/index.php/1987_AJHSME )

回答

与前两个例子不同,在这个例子中,我们将在循环中使用两个变量cw。假设正确答案的数量是c,错误答案的数量是w。它们的总和不能大于 20,即问题总数。因为它可能有不止一个解,我们不使用break在它找到第一个解后立即退出程序。这是一种不同于前面例子的方法。

private static void scoring() {
       for(int c = 0; c < 20; c++) {
              for(int w = 0; w < 20 - c; w++) {
                     if (5 * c - 2 * w == 48) {
                            System.out.println("Correct answers: " + c
                                          + "; wrong answers: " + w);
                     }
              }
       }
}

输出是:

       Correct answers: 10; wrong answers: 1
       Correct answers: 12; wrong answers: 6

例子

有多少个不同的四位数能被 3 整除,并且最后两位是 23?(2003/10B AMC 问题在 https://artofproblemsolving.com/wiki/index.php/2003_AMC_8 )

回答

我们需要注意这个问题中的措辞,“不同的四位数”。策略是将所有条件分成两部分。

  • 4 位数能被 3 整除,最后两位是“23”。

  • 四位数都不一样。

private static void countNumbers() {
       int totalCount = 0;
       for(int i = 1000; i < 10000; i++) {
              if (i % 3 == 0 && i % 100 == 23) {
                     int firstDigit = i / 1000;
                     int secondDigit = i / 100 % 10;
                     if (firstDigit != secondDigit &&
                            firstDigit != 2 &&
                            firstDigit != 3 &&
                            secondDigit != 2 &&
                            secondDigit != 3) {
                            totalCount++;
                            System.out.println(i);
                     }
              }
       }
       System.out.println("Total count = " + totalCount);
}

它的输出是:

1023
1623
1923
4023
4623
4923
5823
6123
6423
6723
7023
7623
7923
8523
9123
9423
9723
Total count = 17

我们使用一个if子句来验证所有四个数字都是不同的。

img/485723_1_En_21_Figa_HTML.jpg

或者,我们可以创建一个通用的方法来检查它。

       if (isDistinct(firstDigit, secondDigit, 2, 3)) {
              ......
       }

这是isDistinct(...)的实现。

       private static boolean isDistinct(int a, int b, int c, int d) {
              if (a == b) {
                     return false;
              } else if (a == c) {
                     return false;
              } else if (a == d) {
                     return false;
              } else if (b == c) {
                     return false;
              } else if (b == d) {
                     return false;
              } else if (c == d) {
                     return false;
              } else {
                     return true;
              }
       }

countNumbers2()中的一个改进版本将是:

private static void countNumbers2() {
       int totalCount = 0;
       for(int i = 1000; i < 10000; i++) {
              if (i % 3 == 0 && i % 100 == 23) {
                     int firstDigit = i / 1000;
                     int secondDigit = i / 100 % 10;
                     if (isDistinct(firstDigit, secondDigit, 2, 3)) {
                            totalCount++;
                            System.out.println(i);
                     }
              }
       }
       System.out.println("Total count = " + totalCount);
}

例子

露丝有 10 枚硬币,不是 5 分、10 分就是 25 分。她有 N 个五分镍币、D 个一角硬币和 Q 个二角五分硬币,其中 N、D 和 Q 都不相同,并且都至少是 1。令人惊讶的是,如果她有 Q 个五分镍币、N 个一角硬币和 D 个二角五分硬币,她会有同样多的钱。露丝有多少美分?(2012 年 http://academicsarecool.com 的 MathIsCool 问题)

回答

在以下解决方案中,将使用两个for-回路。

private static void countCoins() {
       for(int n = 1; n < 9; n++) {
              for(int d = 1; d < 10 - n; d++) {
                     int q = 10 - n - d;
                     if (5*n + 10*d + 25*q == 5*q + 10*n + 25*d){
                            System.out.println((5*n+10*d+25*q)+" cents.");
                            System.out.println("N="+n+"; D="+d+"; Q="+q);
                     }
              }
       }
}

输出是:

155 cents.
N=1; D=5; Q=4

例子

三个朋友总共有六支一模一样的铅笔,每个人至少有一支铅笔。这种情况会以多少种方式发生?(2004 年 AMC8 题在 https://artofproblemsolving.com/wiki/index.php/2004_AMC_8 )

回答

我们使用两个for-循环来模拟我们如何将六支相同的铅笔分配给三个人,分别用变量firstsecondthird来表示。

private static void countWays() {
       int count = 0;
       for(int first=0; first <= 6; first++) {
              for(int second = 0; second <= 6 - first; second++) {
                     int third = 6 - first - second;
                     if (first > 0 && second > 0 && third > 0) {
                            count++;
                                   System.out.println("first=" + first + "; second=" + second + "; third=" + third);                     }
              }
       }
       System.out.println("Total count=" + count);
}

输出是:

first=1; second=1; third=4
first=1; second=2; third=3
first=1; second=3; third=2
first=1; second=4; third=1
first=2; second=1; third=3
first=2; second=2; third=2
first=2; second=3; third=1
first=3; second=1; third=2
first=3; second=2; third=1
first=4; second=1; third=1
Total count=10

例子

七块不同的糖果被分在三个袋子里。红色的袋子和蓝色的袋子必须每人收到至少一块糖果;白色袋子可以保持为空的。有多少种可能的安排?(2010/10B AMC 问题在 https://artofproblemsolving.com/wiki/index.php/2010_AMC_10B )

回答

首先,我们设计一个实验。在这个实验中,我们希望将七个不同的字符串(“A”、“B”、“C”、“D”、“E”、“F”、“G”)放入三个字符串数组中。这七根线代表七种不同的糖果。这三个数组分别代表红色、蓝色和白色的袋子。放置的顺序并不重要,但是我们需要确保在放置完成后只有最后一个数组可以为空。目标是找到不同位置的数量。

当我们将“A”放入三个数组中的一个时,我们需要一个 for 循环来处理三个不同的数组。然后我们需要另一个for-循环来放置“B”,以此类推;我们总共需要七个 for 循环。在每个for-循环中,我们将字符串追加到现有数组中,即bag[i]。但是完成之后,我们需要将它从数组字符串的尾部移除,以便它尝试下一个选项。这就是我们使用bag[i].replace("A", "")的原因。这同样适用于其他六根弦。

我们想出了一个“直截了当”但看起来很丑的版本,如下所示。

注意

包–红色:0,蓝色:1,白色:2;循环中的 3 代表三个字符串数组,即三个袋子。

/// BAG - red: 0, blue: 1, white: 2
private static void distributeCandy() {
    int count = 0;
    String[] bag = { "", "", "" };
    for(int i=0; i < 3; i++) {
        bag[i] += "A";
        for(int j=0; j < 3; j++) {
            bag[j] += "B";
            for(int k=0; k < 3; k++) {
                bag[k] += "C";
                for(int l=0; l < 3; l++) {
                    bag[l] += "D";
        for(int m=0; m < 3; m++) {
            bag[m] += "E";
            for(int n=0; n < 3; n++) {
                bag[n] += "F";
                for(int p=0; p < 3; p++) {
                    bag[p] += "G";
                    if(bag[0].length() > 0 && bag[1].length() > 0) {
                                  count++;
                        System.out.println("Red=" + bag[0] + " Blue=" + bag[1] + " White=" + bag[2]);
                    }
                    bag[p] = bag[p].replace("G", "");    }
             bag[n] = bag[n].replace("F", "");   }
        bag[m] = bag[m].replace("E", "");    }
                bag[l] = bag[l].replace("D", "");    }
            bag[k] = bag[k].replace("C", "");    }
        bag[j] = bag[j].replace("B", "");   }
    bag[i] = bag[i].replace("A", "");    }
    System.out.println("Total count: " + count);
}

代码结构看起来太复杂了。嵌套的for-循环太多。一个更好的想法是应用递归方法来提高它的简单性。现在看新版本:

private static int count = 0;
private static String[] bag = { "", "", "" };
private static String[] CANDY = new String[] { "A", "B", "C", "D", "E", "F", "G" };
private static String RemoveLastChar(String s) {
       if (s == null && s.length() < 1) {
              System.out.println("Input string is invalid!");
              return "";
       }
       return s.substring(0, s.length() - 1);
}
private static void distributeCandies_Recursive(int pointer) {
       for(int i=0; i < 3; i++) {
              bag[i] += CANDY[pointer];
              if (pointer == CANDY.length - 1) {
                     if (bag[0].length() > 0 && bag[1].length() > 0) {
                            count++;
                            System.out.println("Red=" + bag[0] + " Blue=" + bag[1] + " White=" + bag[2]);
                     }
              }
              else {
                     distributeCandies_Recursive((pointer + 1));
              }
              bag[i] = RemoveLastChar(bag[i]);
       }
}

然后,我们在执行的main函数中包含下面两行。

distributeCandies_Recursive(0);
System.out.println("Total count: " + count);

例子

随机选择 1000 到 10000 之间的回文。被 7 整除的概率是多少?(2010/10B AMC 问题在 https://artofproblemsolving.com/wiki/index.php/2010_AMC_10B )

回答

回文数字是一个从左到右和从右到左读起来一样的数字。

isPalindrome()方法中,我们反转数字串,并与原始数字串进行比较。如果反转后的字符串与原来的字符串相同,则被识别为回文字符串。" 8558 "它的反串“8558”和它自己是一样的。

我们引入StringBuffer类来利用它的reverse()方法。范围[1000,10000]内的每个数字在被传递给isPalindrome()方法之前都被转换为字符串类型。该解决方案可以应用于任何范围的整数。

public static void main(String[] args) {
       countDivisibility();
}

private static boolean isPalindrome(String numberStr) {
       String reversed = new StringBuffer(numberStr).reverse().toString();
       return reversed.equals(numberStr);
}

private static void countDivisibility() {
       int count = 0;
       int total = 0;
       for(int i = 1000; i < 10001; i++) {
              if(isPalindrome(Integer.toString(i))) {
                     total++;
                     if (i % 7 == 0) {
                            count++;
                            System.out.println(i);
                     }
              }
       }
       System.out.println("Probability=" + count + "/" + total);
}

方法isPalindrome2()中有一种不同的方式,它包含与isPalindrome()方法相同的功能。除了使用StringBuffer,你可以简单地比较每个字符的前半部分和后半部分。

private static boolean isPalindrome2(String s) {
       int len = s.length();
       for( int i = 0; i < len / 2; i++ ) {
                     if (s.charAt(i) != s.charAt(len - i - 1)) {
                                    return false;
                     }
       }
       return true;
}

例子

随机选择一个以 10 为基数的三位数 n。n 的 9 进制表示和 11 进制表示都是三位数的概率是多少?(2003/10A AMC 问题在 https://artofproblemsolving.com/wiki/index.php/2003_AMC_10A_Problems )

回答

十进制三位数的总数是 900。十进制的三位数 124 是九进制的 147,十一进制的 103;10 进制 720,9 进制 880,11 进制 5A5。

关键的方法是countBase10Numbers():

private static void countBase10Numbers() {
       int count = 0;
       for(int i = 100; i < 1000; i++) {
              String base9Number = convertToBaseN(i, 9);
              String base11Number = convertToBaseN(i, 11);
              if (base9Number.length() == 3 &&
                     base11Number.length() == 3) {
                     count++;
                     System.out.println(i + " -> " + base9Number +
                                   "; " + base11Number);
              }
       }
       System.out.println(count + " out of " + (1000 - 100));
}

支撑方式是convertToBaseN()

private static String convertToBaseN(int base10, int n) {
       if(n < 2 || n > 16) {
              return "";
       }
       String baseN = myOneDigit[base10 % n];
       base10 = base10 / n;
       while(base10 > 0) {
              baseN = myOneDigit[base10 % n] + baseN;
              base10 = base10 / n;
       }
       return baseN;
}

myOneDigit数组是:

private static String[] myOneDigit =
{ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F" };

它实际上相当于一个实现了switch条件语句的方法:

private static String convertDigit(int digit) {
       String s = "";
       switch(digit) {
              case 10:
                     s = "A";
                     break;
              case 11:
                     s = "B";
                     break;
              case 12:
                     s = "C";
                     break;
              case 13:
                     s = "D";
                     break;
              case 14:
                     s = "E";
                     break;
              case 15:
                     s = "F";
                     break;
              default:
                     s = Integer.toString(digit);
                     break;
       }
       return s;
}

显然,使用字符串数组的方法更简单。

问题

  1. Richa 和 Yashvi 将和他们的学校一起去牙买加。他们计划参加一个儿童门票 1.5 美元、成人门票 4 美元的博览会。在特定的一天,2200 人进入交易会,并收取 5050 美元。有多少孩子参加了?(2017 MathIsCool)

  2. 在一次有 10 道题的数学竞赛中,一个学生答对一题得 5 分,答错一题得 2 分。如果奥利维亚回答了所有问题,她的分数是 29,那么她有多少个正确答案?(2002 年 AMC8)

  3. 有多少不超过 2001 的正整数是 3 或 4 的倍数而不是 5 的倍数?(2001 年 AMC10)

  4. 有多少个三位数的正数恰好包含两个不同的数字(例如,343 或 772,而不是 589 或 111)?(2006 MathIsCool)

  5. 丽贝卡去商店买了五株植物。如果商店出售三种类型的植物,她可以购买多少种不同的植物组合?(2005 MathIsCool)

二十二、因式分解

在学校数学中,我们通常遵循一个程序来寻找任何给定正整数的所有因子。这个过程叫做因式分解,根据整数有多大,需要相当多的计算。使用 Java 编程环境,让我们创建一个简单的程序来为我们做同样的工作。我们想写代码来找出任意正整数的所有因子。当用户输入“10”时,程序应该输出:1、2、5 和 10。作为第一步,我们需要定义过程,然后用 Java 代码实现它。

数学:寻找因子

回想一下我们在学校是如何手动寻找因子的,我们用一个整数从最小的(即“1”)到最大的(即给定的整数本身),一个一个地,来检查它是否能被给定的数整除。当答案是肯定的时候,我们知道这是一个因素。否则,我们跳过它,转到下一个数字。

我们创建下面的代码块来完成这个过程。我们将这个版本的代码标记为“v1”,并计划从这里进行改进。

private static int listFactors_v1(int n) {
       int counter = 0;
       for (int i = 1; i <= n; i++) {
              if (n % i == 0) {
                     if (counter > 0) {
                            System.out.print(", ");
                     }
                     System.out.print(i);
                     counter++;
              }
       }
       System.out.println();
       System.out.println("Number of factors: " + counter);
       return counter;
}

main方法将如下所示:

public static void main(String[] args) {
       Scanner input = new Scanner(System.in);
       int iterations = 0;
       while (true) {
              iterations++;
              System.out.println("Enter an integer number:");
              int k = input.nextInt();
              if (k < 0) {
                     k = -k;
              }
       }

       System.out.println("Number of factors: " + listFactors_v1(k));
       input.close();
}

当您编译并执行代码时,您将看到如下输出:

Enter an integer number:
2018
1, 2, 1009, 2018
Number of factors: 4

不要认为我们有解决这个问题的完美方案。实际上,它远未完成。

数学:将问题减半

当我们迭代从 1 到 n 的每一个单个数字时,为了找到 n 的所有可能的除数,我们观察到任何大于 n/2 的整数都不会是 n 的除数,因此,我们只需要检查从 1 到 n/2,而不是 n,这个变化将节省程序中一半的迭代次数。因此,我们现在在下面的 2.1 版本中有了直接的改进。

private static int listFactors_v21(int n) {
       int counter = 0;
       for (int i = 1; i <= n / 2; i++) {
              if (n % i == 0) {
                     if (counter > 0) {
                            System.out.print(", ");
                     }
                     System.out.print(i);
                     counter++;
              }
       }
       System.out.println(", " + n);
       counter++;
       System.out.println("Number of factors: " + counter);
       return counter;
}

除了算法的改变,我们还将删除一个if子句,以稍微降低复杂性。然后我们会拿出下面的 2.2 版本,稍加修改。

private static int listFactors_v22(int n) {
       System.out.print("1"); // "1" is always the 1st factor
       int counter = 1;
       for (int i = 2; i <= n / 2; i++) {
              if (n % i == 0) {
                     System.out.print(", " + i);
                     counter++;
              }
       }
       System.out.println(", " + n); // n is always the last factor
       counter++;
       System.out.println("Number of factors: " + counter);
       return counter;
}

够好吗?其实不是。

数学:使用平方根

如果我们记得在数学中如何测试正整数是否为质数,我们只使用从 2,3 到 n 的平方根的整数。我们将在这里应用相同的逻辑来寻找一对因子,以减少迭代次数。

private static int listFactors_v31(int n) {
       int counter = 0;
       for (int i = 1; i <= Math.sqrt(n); i++) {
              if (n % i == 0) {
                     if (counter > 0) {
                            System.out.print(", ");
                     }
                     System.out.print(i);
                     counter++;
                     if (i != n / i) {
                            System.out.print(", " + n / i);
                            counter++;
                     }
              }
       }
       System.out.println();
       System.out.println("Number of factors: " + counter);
       return counter;
}

目前的 3.1 版本显然更好,因为它只检查 n 的平方根,而不是 n/2。以 n=100 为例,现在我们从 1 到 10 进行检查,而不是从 1 到 50。显然,迭代次数的减少是显著的。但是我们很快发现了一个问题。当前解决方案的输出是:

img/485723_1_En_22_Figa_HTML.jpg

因子列表并不像我们希望的那样按升序排列,只是因为它成对地打印出因子。为了解决这个问题,我们将创建两个字符串。一个字符串存储每对因子中较小的一个。另一个字符串存储每对中较大的一个。

private static int listFactors_v32(int n) {
       String s1 = "1";
       String s2 = Integer.toString(n);
       int counter = 2;
       for (int i = 2; i <= Math.sqrt(n); i++) {
              if (n % i == 0) {
                     s1 += ", " + i;
                     counter++;
                     if (i != n / i) {
                            s2 = n / i + ", " + s2;
                            counter++;
                     }
              }
       }
       System.out.println(s1 + ", " + s2);
       System.out.println("Number of factors: " + counter);
       return counter;
}

我们能从目前的 3.2 版本中做进一步的改进吗?答案还是肯定的。我们没有将每对除数中较小的数存储到字符串中,而是直接将其发送到控制台。这将节省一个字符串的内存空间。这是另一个“小”的变化,但当我们处理一个可能包含大量因素的大数字时,这可能是一个巨大的节省。我们最终登陆了 3.3 版:

private static int listFactors_v33(int n) {
       String s = Integer.toString(n);
       int counter = 2;
       System.out.print("1");
       for (int i = 2; i <= Math.sqrt(n); i++) {
              if (n % i == 0) {
                     System.out.print(", " + i);
                     counter++;
                     if (i != n / i) {
                            s = n / i + ", " + s;
                            counter++;
                     }
              }
       }
       System.out.println(", " + s);
       System.out.println("Number of factors: " + counter);
       return counter;
}

以下是我们为解决这一问题所做工作的总结:

  • 我们将整数的实际上限从 n 减少到 n/2,然后减少到 n 的平方根。

  • 我们避免了使用额外的字符串进行临时存储。

  • 我们一直在思考如何根据三个基本规则来改进我们的实现:

    1. 采用我们所知的最佳算法;

    2. 优化代码,消耗更少的内存,运行更快;

    3. 编写易于理解的代码,以便将来维护。

所有这些努力都促成了一个优化良好的代码。这的确是编程的艺术。

二十三、PI 的探索性实验

为了监测自然环境变化对鱼类生命周期的影响,科学家们必须一直跟踪鱼类的数量。一个湖里有一种叫 AA 的鱼供研究。科学家们将一小群标记为 AA 的鱼放归湖中,它们的总数等于#(total _ labelled)。一段时间后,他们从湖中随机捕获一些鱼 AA 作为样本,其总数等于#(total_captured)。在这些样本鱼 AA 中,他们整理出标签鱼 AA,其总数等于#(labelled _ inter _ captured)。

数学:计算人口

假设所有的鱼,包括标记的和未标记的,均匀地分布在湖中,我们使用下面的公式计算出湖中鱼 AA 的当前数量。

 #(total_captured) * #(total_labeled) / #(labeled_among_captured)

该公式以简单的比率形式表示。湖中分布越均匀的鱼 AA,公式产生的结果就越准确。它本质上是一种统计思想,使用少量样本数据来预测大图中可能很大的总数。它试图以最小的误差对不可测量的物体进行测量。

鱼类实验的本质是基于概率论。它也适用于许多其他有趣的问题领域。其中之一是计算圆周率:3.14159.........

例子

怎么做基本编程才能算出圆周率的值?

数学:概率论中的圆周率

我们将一个圆内接成一个正方形,在笛卡尔坐标平面中紧靠 x 轴和 y 轴。假设正方形的长度(即圆的直径)为 n,圆的面积可以用公式表示为:

  • p × (n/2) 2

p 是要搞清楚的东西,就是圆周率。

如果在正方形内随机选择一个点,该点正好在圆内的概率是多少?

学过基本的几何概率就知道答案了。应该是圆和正方形的面积比。由于正方形的面积是 n 2 ,所以比例会是 p/4。

  • p×(n/2)2:n2= p:4

img/485723_1_En_23_Figa_HTML.png

回答

概率 p/4 表明,如果我们尽可能多地重复相同的点选择过程,圆内选择的点数与正方形内选择的点数之比将接近(并最终等于)p/4。因此,一旦我们找出比率,我们就知道圆周率的近似值。这是我们将在程序中使用的算法。

public static double computePi(int total, int n) {
       int count = 0;
       for(int i=0; i < total; i++) {
              double x = n * Math.random();
              double y = n * Math.random();
              if ((x - n/2) * (x - n/2) + (y - n/2) * (y - n/2)
                            < (n/2) * (n/2)) {
                     count++;
              }
       }
       return (double)count * 4 / total;
}

在这种方法中,

  • 整数参数值total是实验的总数(即所选样本点的总数)。

  • 整数参数值n是正方形的边长,或者圆的直径。

  • Math.random()是来自java.util.Random包的 Java 内置函数。它生成一个介于 0 和 1 之间的双精度随机数。乘以n使 x 和 y 坐标值在 0 和n之间。

  • 不等式“(x-n/2)2+(y-n/2)2<(n/2)2”是检查该点是否位于圆心在(n/2,n/2)的圆内。

    您可以通过跟随main方法()中的行来调用该方法

    :

    public static void main(String[] args) {
           int onehMillion = 100 * 1000 * 1000;
           for(int i=0; i < 10; i++) {
                  System.out.println(computePi(onehMillion, 100));
           }
    }
    
    
  • n = 100是边长。它不会直接影响公式。您可以使用其他数字,如 10 或 2 或任何偶数(由于“n/2”),用于扩展实验目的。

  • int onehMillion = 100 * 1000 * 1000等于 1 亿。它表示我们在一次探索性测试中选取的总点数。100 * 1000 * 1000是一个乘法操作,将在编译期间运行之前执行。当前的乘法表达式在编码中不存在额外的计算时间。

  • for -loop ()多次驱动相同的实验,即 10 次。

控制台上的输出如下所示:

3.14150492
3.14166436
3.14157872
3.14143904
3.14174756
3.14153872
3.14161072
3.14155196
3.14198448
3.14158056

该方法使用不到 10 行代码,完成执行并输出估计的 Pi 值大约需要 5 秒钟。

最后但同样重要的是,如果totali的值太大,我们需要将int改为long。记住int类型的数据最大可达 32 位,相当于 2 32 。当总数大于这个数时,我们将需要使用long类型,它支持 64 位(等于 2 64 )。

public static double computePi(long total, int n) {
       long count = 0;
       for(long i=0; i < total; i++) {
              double x = n * Math.random();
              double y = n * Math.random();
              if ((x - n/2) * (x - n/2) + (y - n/2) * (y - n/2)
                            < (n/2) * (n/2)) {
                     count++;
              }
       }
       return (double)count * 4 / total;
}

参数的长值类型需要作为...L传递,如下所示。然而,这将需要更长的时间来执行,除非它运行在一个高计算能力的 PC 上。

public static void main(String[] args) {
       long hugeNumber = 1000 * 1000 * 1000 * 1000L;
       for(int i=0; i < 10; i++) {
              System.out.println(computePi(hugeNumber, 100));
       }
}

如果我们增加total的值,它将通过更多的点覆盖更多的区域,并返回更精确的圆周率结果。对于我们在这个程序中选择的 1 亿个点,它几乎保证找出 Pi = 3.141...,精确到千分之一。为了达到更高的精度,我们需要一台更强大的计算机。至少我们知道,有了理想的计算平台,我们将能够在指定的精度水平上确定圆周率值。

从这个例子中,我们了解到,只要我们知道曲线的函数模型,我们可以应用相同的比率和概率概念来找出由任何曲线包围的区域。

例子

牢记概率概念,用 Java 编程求直线 x = 0,y = 0,曲线 y = -2x 2 + 12x -18 之间的面积。

img/485723_1_En_23_Figb_HTML.jpg

回答

这种方法已经帮助我们解决了一个问题,而一个纯数学的解决方案原本需要微积分的知识。

       public static double computeArea(int total) {
              int count = 0;
              for(int i=0; i < total; i++) {
                     double x = Math.random() * 3;
                     double y = Math.random() * -18;
                     if (-2 * x * x + 12 * x - 18 < y) {
                            count++;
                     }
              }
              return (double)count * 54 / total;
       }

问题

创建一个程序,找出欧拉数 e。