Python经典练习题——求水仙花数

版权声明:原创文章,转载请注明: 转载自 JiandaoStudio的博客https://blog.csdn.net/weixin_41213648 https://blog.csdn.net/weixin_41213648/article/details/90725733

严格来说,我并不知道何谓“水仙花数”,因为以前读书时根本没听过这种数,也不知道这种数有什么特征。后来从事编程之后反而听说了所谓的“水仙花数”。

如果通过网络查询,则发现水仙花数的定义也不统一,比如通过baidu百科查到如下定义:

水仙花数(Narcissistic number)也被称为超完全数字不变数(pluperfect digital invariant,PPDI)、自恋数、自幂数、阿姆斯壮数或阿姆斯特朗数(Armstrong number),水仙花数是指一个 3 位数,它的每个位上的数字的 3次幂之和等于它本身(例如:1^3 + 5^3+ 3^3 = 153)。

但也有资料将“水仙花数”等同于“自幂数”——只要该数的每个位上的数字的N次方(N等于该数的位数)的和等于该数即可。

不管怎样,这些不是我们关注的重点。程序员就是根据客户需求(业务规则)进行实现——规则你们来定,我们负责实现!

通过上面不难发现,判断一个数是否为水仙花数,首先要获得该数在个位、十位、百位……上的数字,然后计算这些数字的N次方,并将它们加起来即可。

先看真正“水仙花”数的简单计算方法:

基于循环来计算严格的“水仙花数”

 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# 方法二
 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# 方法三
 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# 方法四
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 可迭代对象]

由于上面列表推导式存在嵌套,因此我们先看一层,如果将上面推倒使式写成如下形式:

lt =[j for j in range(1, end + 1) if j % 2 == 0]

此时该列表内将会收集从1~end的所有偶数(根据if j % 2 == 0),此时该列表推导式只有一层,并没有嵌套。

但我们并不是要简单地收集偶数,而是要收集水仙花数(自幂数),因此程序还得搞一个列表,该列表的元素是个位、十位、百位……上数字的N次方。

如何获取一个数的个位、十位、百位……上数字的N次方呢?前面已经介绍了,使用循环来遍历字符串即可。假如目标数字是j,那下面代码即可获取数值j在个位、十位、百位……上数字的N次方。

[(ord(i) - 48) ** len(str(j)) for i in str(j)]

再回头看到前面的列表推导式,它的完整格式其实就是:

[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# 方法五(高效)
  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即可。

猜你喜欢

转载自blog.csdn.net/weixin_41213648/article/details/90725733