总目录
自幂数,就是一个长度为
n
n
n的自然数,等于自身各个位上数字的
n
n
n次幂的和。
例如十进制中:
153
=
1
3
+
5
3
+
3
3
153=1^3+5^3+3^3
153=13+53+33,153是3位数,每一位数的3次幂的和,还是等于其自身,对于3位的自幂,还有一个特殊的名字“水仙花数”(Narcissistic Number)。
求自幂数的算法非常简单,只要遍历区间内所有数字,算出每一位上数字的“数字长度”的幂之和,与原数比较,只要相等即为自幂数。
如果用以往普通Java循环来写,for、while满屏横飞,各种代码结构完全混杂在一起,而用Stream来写,就可以代码写得即简单,又直白,初始逻辑与判断逻辑完成分离,结构的美感就体现出来了。
思考
1.由于位数不同,导致算法不同,所以简化一下,只查找指定位数的自幂数,即指定 n n n的值,如果让 n = 3 n=3 n=3,就是只查找100到999之间的自幂数。接下来的说法都以3位数来举例
2.需要将这个3位数中每一位的数取出来,取出后计算其3次幂的值
3.取每一位数的方法很多,这里使用与100相除的商来取百位,余数再与10相除,商为十位,余数再与1相除,商为个位数的方法
4.使用的方法中要尽量少用for、while语句
步骤1-初始化准备代码
计算3次幂的方法。
private int getPower(int initVal, int len) {
return IntStream.iterate(initVal, v1 -> v1).limit(len).reduce(1, (a, b) -> a * b);
}
- 这个代码可以用Math.power函数来实现,这里是个小炫技,求轻打
- 参数len表示有多少位,initVal表示生成数列中包含的值。
getPower(5, 3)
为例,IntStream.iterate(initVal, v1->v1)
就会生成一个无限流,里面的内容全部都是5。limit(len)
,是让这个无限流,只会有3个5,其他的全抛弃。reduce(1, (a, b) -> a * b)
是把这个流收敛,可以理解成把分散的流变成一个值,这里使用的收敛方法,让流中每个元素相乘在一起,最后就是形成 5 ∗ 5 ∗ 5 5*5*5 5∗5∗5这样的效果,即最后方法得到的是125,5的3次幂的值。- 灵活使用这个方法
getPower(10, 2)
,就可以得到100,是生成百位数时,要用到的第一个除数。 - 当得到了100,就可以得到3位数中最大值999,以及最小值100:
int firstDivide = getPower(10, len - 1);
int max = firstDivide * 10 -1;
int min = firstDivide;
步骤2-核心逻辑判断代码
非常遗憾地告诉各位,在这部分代码中,不得不使用的for循环,我也不想啊,可是现实就是让你低头啊,如果列位有更好解决方案,万望不吝赐教。
private IntPredicate isSelfPower(int len, int firstDivide) {
return e -> {
int dividend = e;// 被除数
int divisor = firstDivide;// 除数
int quotient;// 商
int remainder;// 余数
int sum = 0;
for (int i = 0; i < len; i++) {
quotient = dividend / divisor;
remainder = dividend % divisor;
sum += getPower(quotient, len);
dividend = remainder;
divisor /= 10;
}
return sum == e;
};
}
IntPredicate
是一个函数式接口,返回真或假,而如何判断是应该返回真,还是返回假,全在return语句后面的实现代码中。- 实现代码中循环语句的作用举例来说就是:先解析出百位的数,然后对这个数求其3次幂的值,并加到sum变量中,然后再解析出十位的数,再求3次幂,并加到sum变量中……这样的循环。
- 解析百位数是除以100,解析十位是除以10,解析个位是除以1(这是比较无用的,但可以不用单独处理个位,保证代码的一致性)
- 最后就是比较这个和sum与这个数本身是不是一样,如果是true,则说明是自幂,反之则不是
步骤3-用一行代码实现查找自幂数
IntStream.iterate(min, v -> v + 1).limit(max - min).parallel()
.filter(isSelfPower(len, firstDivide)).forEach(System.out::println);
- 这里面生成的数列的lambda表达式是v -> v+1,这个会生成1,2,3,4,5……这样递增的数列。
- 使用parallel(),可以并行查找,如果把位数设的大一点,应该会有性能提升,但提升了多少,没有测试过,懒啊。
总结
把代码合并起来,就可以计算指定位数的自幂数了,如果设成8位数,可真是要算很长时间的。
之后的优化
- 核心逻辑判断代码是用一个方法返回函数式接口的实例,其实可以用属性来代替
- 在判断逻辑代码中使用了很多变量,也可以大幅度优化,修改后代码如下:
@FunctionalInterface
public interface ThreeParamPredicate<E> {
boolean test(E e1, E e2, E e3);
}
private ThreeParamPredicate<Integer> threeParamPredicate = (len, divisor, dividend) -> {
int sum = 0;
for (int i = 0; i < len; i++) {
int quotient = dividend / divisor;
int remainder = dividend % divisor;
sum += Double.valueOf(Math.pow(quotient, len)).intValue();
dividend = remainder;
divisor /= 10;
}
return sum == dividend;
}
- 举例说明一下这个代码的作用:
- 由于代码需要传入三个参数,而函数式接口中没有提供,所以自己写了一个接收三个入参的函数式接口
- 比如判断153是不是自幂数,首先要把153折成1、5、3三个数
- 循环第1次就是先拆出1这个数来,然后求1的3次幂,加到
sum
变量中 - 循环第2次拆出5来,求出5的3次幂,再加到
sum
变量中 - 循环第3次拆出3来,求出3的3次幂,再加到
sum
变量中 - 最后比较
sum
变量的值与153的比较,相等则说明是自幂数 - 但是这些代码,没有按照希望写出不带for/while语句的代码,所以再次修改如下
private IntPredicate isSelfPower1 = v -> {
String str = String.valueOf(v);
int len = str.length();
int sum = Arrays.stream(str.split(""))
.mapToInt(e -> Double.valueOf(Math.pow(Integer.valueOf(e), len)).intValue()).sum();
return v == sum;
}
- 这个代码意思是,先数字转换成字符串,求出其长度,再通过字符串,把每一个字符拆解出来,再转成数字,求幂值,最后求和
- 通过这个代码优化,可以看出之前写的代码,把数字长度做为参数传进来,还把除数也传入,是完全没必要的
- 这里面完全没有for/while语句了,很好啊!