严格来说,我并不知道何谓“水仙花数”,因为以前读书时根本没听过这种数,也不知道这种数有什么特征。后来从事编程之后反而听说了所谓的“水仙花数”。
如果通过网络查询,则发现水仙花数的定义也不统一,比如通过baidu百科查到如下定义:
水仙花数(Narcissistic number)也被称为超完全数字不变数(pluperfect digital invariant,PPDI)、自恋数、自幂数、阿姆斯壮数或阿姆斯特朗数(Armstrong number),水仙花数是指一个 3 位数,它的每个位上的数字的 3次幂之和等于它本身(例如:1^3 + 5^3+ 3^3 = 153)。
但也有资料将“水仙花数”等同于“自幂数”——只要该数的每个位上的数字的N次方(N等于该数的位数)的和等于该数即可。
不管怎样,这些不是我们关注的重点。程序员就是根据客户需求(业务规则)进行实现——规则你们来定,我们负责实现!
通过上面不难发现,判断一个数是否为水仙花数,首先要获得该数在个位、十位、百位……上的数字,然后计算这些数字的N次方,并将它们加起来即可。
先看真正“水仙花”数的简单计算方法:
基于循环来计算严格的“水仙花数”
1 |
1# 方法一 2start = 101 3end = 999 4for i in range(start, end + 1): 5 # 计算百位上的数 6 bai = i // 100 7 # 计算十位、个位上的数 8 shi, ge = (i - bai * 100) // 10, i % 10 9 # 判断是否为水仙花数 10 if ge ** len(str(i)) + shi ** len(str(i)) + bai ** len(str(i)) == i: 11 print(i) |
上面算法只能计算三位的水仙花数,该算法只需要使用单层循环,并通过数学整除、求余来计算百位、十位、个位上的数,然后判断该数是否为水仙花数。
总结来说,这个算法简单、易懂、适合初学者上手学习,而且这个算法只需要单层循环;这个算法最大的问题是不适合计算多位的“自幂数”。
下面对这个方法略作改进。
基于循环来计算“自幂数”(非严格“水仙花数”)
下面方法就是可以计算任意范围(只要不超过Python整数的取值范围)的水仙花数(自幂数),下面这个算法将采用循环来计算各数位上的数值。
1 |
1# 方法二 2end = int(input('请输入最大范围:')) 3for i in range(1, end + 1): 4 # 计算数字i的长度 5 length = len(str(i)) 6 sm = 0 7 temp = i 8 for j in range(length): 9 # 对于10求余等到个位上的数字, 10 # 然后计算length次方,并累加其总和 11 sm += (temp % 10) ** length 12 # 将目标数缩小10倍,下一步将会获取十位上的数字 13 # 依次类推,下一次获取百位、千位上的数 14 temp //= 10 15 # 判断是否为水仙花数 16 if sm == i: 17 print(i) |
这个算法与前面算法基本相似,区别只是这个算法需要通过依次求余来获取个位、十位、百位、千位……上的数,由于程序并不知道要判断的数到底有几位,因此程序使用了循环依次求余来获取个位、十位、百位、千位……上的数。
这个算法是前一个算法的稍作改进。
此外,我们知道Python的字符串也是可迭代对象,当程序迭代字符串时,程序就可以依次获取字符串中的每个字符,因此程序可同构这种方式来获取一个数在在个位、十位、百位……上的数字。
通过遍历字符串来计算“自幂数”(非严格“水仙花数”)
下面程序只是对前一个程序的改变,本程序不再使用数学的求余、整除算法来计算个位、十位、百位……上的数字,而是通过遍历字符串来获取个位、十位、百位……上的数字。
1 |
1# 方法三 2end = int(input('请输入最大范围:')) 3for i in range(1, end + 1): 4 # 计算数字i的长度 5 length = len(str(i)) 6 sm = 0 7 # 将i转成字符串,然后通过遍历字符串来依次获取每位数字 8 for j in str(i): 9 sm += (ord(j) - 48) ** length 10 # 判断是否为水仙花数 11 if sm == i: 12 print(i) |
这个算法与前一个算法的区别在于计算计算个位、十位、百位……上的数字的方法不同,本程序采用的是遍历字符串的方式进行计算。
此外,Python还提供了一个sum()函数来计算列表的总和,因此程序可以将各数位上的值的N次方收集成一个列表,然后利用sum()函数来计算该列表的总和,这样就可判断该数是否为水仙花数了。
利用列表推导式来计算“自幂数”(非严格“水仙花数”)
下面采用一个嵌套的列表推导式来计算水仙花数(自幂数)。
1 |
1# 方法四 2end = int(input('请输入最大范围:')) 3lt =[j for j in range(1, end + 1) if sum([(ord(i) - 48) ** len(str(j)) for i in str(j)]) == j] 4print(lt) |
从上面代码可以看到,该程序只要一行就可以计算所有水仙花数(自幂数),该程序的本质是一个嵌套循环——只不过它是嵌套的列表推导式。
首先列表推导式的语法是:
for表达式用于利用其他区间、元组、列表等可迭代对象创建新的列表,for表达式语法格式如下:
[表达式 for 循环计数器 in 可迭代对象]
由于上面列表推导式存在嵌套,因此我们先看一层,如果将上面推倒使式写成如下形式:
1 |
lt =[j for j in range(1, end + 1) if j % 2 == 0] |
此时该列表内将会收集从1~end的所有偶数(根据if j % 2 == 0),此时该列表推导式只有一层,并没有嵌套。
但我们并不是要简单地收集偶数,而是要收集水仙花数(自幂数),因此程序还得搞一个列表,该列表的元素是个位、十位、百位……上数字的N次方。
如何获取一个数的个位、十位、百位……上数字的N次方呢?前面已经介绍了,使用循环来遍历字符串即可。假如目标数字是j,那下面代码即可获取数值j在个位、十位、百位……上数字的N次方。
1 |
[(ord(i) - 48) ** len(str(j)) for i in str(j)] |
再回头看到前面的列表推导式,它的完整格式其实就是:
1 |
[j for j in range(1, end + 1) if sum(xxx) == j] |
只不过它的xxx就是[(ord(i) – 48) ** len(str(j)) for i in str(j)]。
这个算法比较简洁,只要一行代码即可计算使用列表获取指定范围的所有水仙花数,但有些初学者会反应这个列表推导式不容易看懂,这可能也是这个算法的一个问题:一般来说,我们并不建议使用多层嵌套的列表推导式,因此这样会降低程序的可读性。毕竟,对于实际企业开发来说,程序可读性才是第一位的。
上面这些算法来计算10的5次方以内的“自幂数”时,能拥有较高的效率,但一旦要计算10的8次方、甚至10的20次方以内的“自幂数”时,程序效率会变得非常低——这是由于程序本身采用是循环来判断每个数字,这种循环本身有性能开销,因此效率较低。
高效计算 “自幂数”(非严格“水仙花数”)
下面介绍一种较为高效的算法,这个算法利用了列表来减少计算,程序将“存放数字0-9的num次方的N倍(代表出现次数)的值”使用列表保存下来,这样可避免每次都要重新计算数字0-9的num次方。
此外,该算法还利用了一种预检查的方法来快速排除不符合条件的目标数,这样能更快地加速自幂数的查找效率。
该程序代码如下。
1 |
1# 方法五(高效) 2def narcissus_num(num): 3 # results 存放找到的自幂数 4 results = [] 5 # 定义一个长度为10的列表 6 selected = [0] * 10 7 # power_of_10列表依次保存[0, 10, 100, 1000, 10000, ...] 8 # 该列表中的元素都是10的N次方 9 power_of_10 = [10 ** i for i in range(num + 1)] 10 # pre_table1 存放数字0-9的num次方的N倍(代表出现次数)的值 11 # 例如num为6,pre_table1的元素依次为 12 # [[0**6 * 0, 0**6 * 1, ...0**6 * 6], [1**6 * 0, 1**6 * 1, 1**6 * 2, ...]] 13 pre_table1 = [[i ** num * j for j in range(num + 1)] for i in range(10)] 14 # pre_table2是一个长度为10的列表,每个列表元素又是一个长度为num+1、元素为0的列表 15 pre_table2 = [[0] * (num + 1) for i in range(10)] 16 # num位的自幂数应该在power_of_10[num - 1](10**num-1)~power_of_10[num](10**num)之间 17 min_num = power_of_10[num - 1] 18 max_num = power_of_10[num] 19 # 对pre_table2进行初始化,让它存放pre_table1中各个值除首位外的位数 20 for i in range(10): 21 for j in range(num + 1): 22 for k in range(num, 0, -1): 23 if power_of_10[k] < pre_table1[i][j]: 24 pre_table2[i][j] = k 25 break 26 27 # 检查value是否为自幂数 28 def check_narcissus(value): 29 bit_result = bit_count(value) 30 for i in range(10): 31 if bit_result[i] != selected[i]: 32 return False 33 return True 34 35 # 统计value中数字的个数,返回形如[0, 2, 2, 0, 0, 0, 0, 0, 0, 0]的列表, 36 # 列表中每个元素依次代表value中0、1、2、3、4...的出现次数 37 def bit_count(value): 38 # 定义一个长度为10的列表 39 bit_result = [0] * 10 40 # 依次遍历每个位上的数,并用bit_result列表保存每个数位上的数字的出现次数。 41 for i in str(value): 42 bit_result[int(i)] += 1 43 # 假如最后返回[0, 2, 2, 0, 0, 0, 0, 0, 0, 0] 44 # 那代表该数中1出现2次、2出现2次 45 return bit_result 46 47 def pre_check(cur_index, sum, remain_num): 48 cur_big = pre_table1[cur_index][remain_num] 49 # 如果sum比当前数剩余最大可能数小,说明还有可能找到 50 if sum < cur_big: 51 return True 52 max = sum + cur_big 53 # 去掉cur_big的位数 54 max = max // power_of_10[pre_table2[cur_index][remain_num]] 55 sum = sum // power_of_10[pre_table2[cur_index][remain_num]] 56 # 去掉max,sum不同的尾部。 57 while not max == sum: 58 max = max // 10 59 sum = sum // 10 60 # max,sum头部没有相同部分。 61 if max == 0: 62 return True 63 bit_result = bit_count(max) 64 # 判断大于cur_index 的所有已确定数是否在正常范围。 65 for i in range(9, cur_index, -1): 66 if bit_result[i] > selected[i]: 67 return False 68 # 判断bit_result中小于cur_index的数(从9到0还没有判断的数)的数量是否大于remain_num 69 for i in range(cur_index + 1): 70 remain_num -= bit_result[i] 71 # 小于remain_num,属正常,返回True。 72 return remain_num >= 0 73 74 def search_num(cur_index, sum, remain_num): 75 # 如果sum已经大于max_num最大值,说明已经不可能了,直接返回 76 if sum > max_num: 77 return 78 # 如果sum加上cur_index的num次方remain_num倍,依然小于min_num最小值 79 # 说明已经不可能了,直接返回 80 if (sum + pre_table1[cur_index][remain_num]) < min_num: 81 return 82 83 # 如果不符合预检查,直接跳过 84 if not pre_check(cur_index, sum, remain_num): 85 return 86 87 if remain_num == 0: 88 # 如果检查sum符合自幂数特征,将该数添加到results列表中 89 if sum > min_num and check_narcissus(sum): 90 results.append(sum) 91 return 92 93 if cur_index == 0: 94 selected[0] = remain_num 95 # 递归调用search_num 96 search_num(-1, sum, 0) 97 else: 98 for i in range(remain_num + 1): 99 selected[cur_index] = i 100 search_num(cur_index - 1, sum + pre_table1[cur_index][i], remain_num - i) 101 # 对selected[cur_index]进行复位 102 selected[cur_index] = 0 103 104 # 设定初始值调用search_num,然后返回结果。 105 search_num(9, 0, num) 106 return results 107 108num = int(input('请输入要计算几位的自幂数:')) 109print('%d位的自幂数有:%s' % (num, narcissus_num(num))) |
另外本人还开设了个人公众号:JiandaoStudio ,会在公众号内定期发布行业信息,以及各类免费代码、书籍、大师课程资源。
扫码关注本人微信公众号,有惊喜奥!公众号每天定时发送精致文章!回复关键词可获得海量各类编程开发学习资料!
例如:想获得Python入门至精通学习资料,请回复关键词Python即可。