数值流应用:勾股数

1. 勾股数

  什么是勾股数(毕达哥拉斯三元数)呢?我们得回到从前。在一堂激动人心的数学课上,你了解到,古希腊数学家毕达哥拉斯发现了

某些三元数 (a, b, c) 满足公式 a * a + b * b = c * c ,其中 a 、 b 、 c 都是整数。例如,(3, 4, 5)就是一组有效的勾股数,因

为3 * 3 + 4 * 4 = 5 * 5 或9 + 16 = 25。这样的三元数有无限组。例如,(5, 12, 13)、(6, 8, 10)和(7, 24, 25)都是有效的勾股数。

  勾股数很有用,因为它们描述的正好是直角三角形的三条边长,如图所示。

2. 表示三元数

  那么,怎么入手呢?第一步是定义一个三元数。虽然更恰当的做法是定义一个新的类来表示三元数,但这里你可以使用具有三

个元素的 int 数组,比如 new int[]{3, 4, 5} ,来表示勾股数(3, 4, 5)。现在你就可以用数组索引访问每个元素了。

3. 筛选成立的组合

  假定有人为你提供了三元数中的前两个数字: a 和 b 。怎么知道它是否能形成一组勾股数呢?你需要测试 a * a + b * b 的平方

根是不是整数,也就是说它没有小数部分——在Java里可以使用 expr % 1 表示。如果它不是整数,那就是说 c 不是整数。你可以

用 filter 操作表达这个要求(你稍后会了解到如何将其连接起来成为有效代码):

filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)

  假设周围的代码给 a 提供了一个值,并且 stream 提供了 b 可能出现的值, filter 将只选出那些可以与 a 组成勾股数的 b 。你

可能在想 Math.sqrt(a * a + b * b) % 1 == 0 这一行是怎么回事。简单来说,这是一种测试 Math.sqrt(a * a + b * b) 返回的结果是不

是整数的方法。如果平方根的结果带了小数,如9.1,这个条件就不成立(9.0是可以的)。

4. 生成三元组

  在筛选之后,你知道 a 和 b 能够组成一个正确的组合。现在需要创建一个三元组。你可以使用map 操作,像下面这样把每个元

素转换成一个勾股数组:

stream.filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)
    .map(b -> new int[]{a, b, (int) Math.sqrt(a * a + b * b)});

5. 生成 b 值

  胜利在望!现在你需要生成 b 的值。前面已经看到, Stream.rangeClosed 让你可以在给定区间内生成一个数值流。你可以用它

来给 b 提供数值,这里是1到100:

IntStream.rangeClosed(1, 100)
    .filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)
    .boxed()
    .map(b -> new int[]{a, b, (int) Math.sqrt(a * a + b * b)});

  请注意,你在 filter 之后调用 boxed ,从 rangeClosed 返回的 IntStream 生成一个Stream<Integer> 。这是因为你的 map会为流中

的每个元素返回一个 int 数组。而 IntStream中的 map 方法只能为流中的每个元素返回另一个 int ,这可不是你想要的!你可以用

IntStream的 mapToObj 方法改写它,这个方法会返回一个对象值流:

IntStream.rangeClosed(1, 100)
    .filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)
    .mapToObj(b -> new int[]{a, b, (int) Math.sqrt(a * a + b * b)});

6. 生成值

  这里有一个关键的假设:给出了 a 的值。 现在,只要已知 a 的值,你就有了一个可以生成勾股数的流。如何解决这个问题呢?

就像 b 一样,你需要为 a 生成数值!最终的解决方案如下所示:

Stream<int[]> pythagoreanTriples =
    IntStream.rangeClosed(1, 100).boxed().flatMap(a -> IntStream.rangeClosed(a, 100).filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)
    .mapToObj(b -> new int[]{a, b, (int)Math.sqrt(a * a + b * b)}));

  好的, flatMap 又是怎么回事呢?首先,创建一个从1到100的数值范围来生成 a 的值。对每个给定的 a 值,创建一个三元数流。

要是把 a 的值映射到三元数流的话,就会得到一个由流构成的流。 flatMap 方法在做映射的同时,还会把所有生成的三元数流扁平

化成一个流。这样你就得到了一个三元数流。还要注意,我们把 b 的范围改成了 a 到100。没有必要再从1开始了,否则就会造成

重复的三元数,例如(3,4,5)和(4,3,5)。

7. 运行代码

  现在你可以运行解决方案,并且可以利用我们前面看到的 limit 命令,明确限定从生成的流中要返回多少组勾股数了:

pythagoreanTriples.limit(5)
    .forEach(t -> System.out.println(t[0] + ", " + t[1] + ", " + t[2]));

这会打印:
 3, 4, 5
 5, 12, 13
 6, 8, 10
 7, 24, 25
 8, 15, 17
8. 你还能做得更好吗?

  目前的解决办法并不是最优的,因为你要求两次平方根。让代码更为紧凑的一种可能的方法是,先生成所有的三元数

(a*a, b*b, a*a+b*b) ,然后再筛选符合条件的:

Stream<double[]> pythagoreanTriples2 =
IntStream.rangeClosed(1, 100).boxed()
.flatMap(a ->
IntStream.rangeClosed(a, 100)
.mapToObj(
b -> new double[]{a, b, Math.sqrt(a*a + b*b)})
.filter(t -> t[2] % 1 == 0));

posted @ 2020-01-13 18:30  少说点话  阅读(668)  评论(0编辑  收藏  举报
网站运行: