出场人物介绍
小美: 小学 4 年级学生, 参加了学校的编程兴趣小组, 已经了解了 Python 语言的基本语法, 能够看懂一些简单的程序. 她做事风风火火, 对所有的事情都很好奇, 喜欢打破砂锅问到底, 是一个叫人又爱又恨的小丫头.
阿福: 一个酷爱编程的 8 年级男生. 大家都说他长得像国宝大熊猫, 动作缓慢, 憨态可掬. 他做事情确实够慢的, 连说话也慢条斯理, 可是他一点也不担心, 他常常说:"慢就是快, 只要坚持下去, 蜗牛也能爬上金字塔."
古老师: 虽然年近不惑, 但依然对生活充满热情."爱生活爱运动" 是他的人生信条, 和孩子们一起编程是他最大的乐趣. 他神出鬼没, 总是在孩子们最需要帮助的时候出现. 当然, 你也不能动不动就找古老师, 因为他很忙, 非常非常忙. 所以, 遇到问题还是自己先思考吧.
算法进化历程之相亲数对
小美: 上次古老师教给我们利用数学规律解决灯泡开关问题的方法真是巧妙, 尤其是逆向思维求完全平方数个数的方法令人印象深刻, 至今仍在我的脑海中回荡呢.
阿福: 有这么夸张吗? 既然你印象深刻, 那我这里有一道题目你一定会做.
题目 1:
相亲数对. 2500 年前数学大师毕达哥拉斯发现, 220 与 284 两数之间存在下列奇妙的联系:
220 的真因数之和为 1+2+4+5+10+11+20+22+44+55+110=284
284 的真因数之和为 1+2+4+71+142=220
毕达哥拉斯把这样的数对 a,b 称为相亲数: a 的真因数 (小于本身的因数) 之和为 b, 而 b 的真因数之和为 a.
函数功能: 输出 [2,n] 范围内的所有相亲数对.
函数名: amicable_pair(n:int)-> list
参数表: n-- 正整数上限.
返回值: 一个存储了所有相亲数对的列表, 列表的每个元素都是一个存储了相亲数对的元组.
示例: n=2000, 则返回[(220, 284), (1184, 1210)].
小美: 这有什么难的, 只要用变量 i 遍历 [2,n] 范围内的所有整数, 然后把 i 的真因数和存储到变量 s, 再看看 s 的真因数和是不是等于 i 就行了. 为了体现模块化编程的思想, 我们可以把计算真因数和的功能抽象成一个函数.
代码 1:
- def amicable_pair(n:int) -> list:
- def get_sum(num): #计算 num 的真因数和
- s = 1
- for i in range(2, num):
- if num % i == 0:
- s += i
- return s
- res = []
- for i in range(2, n+1):
- s = get_sum(i)
- if i <s and i == get_sum(s):# 确保 i<s, 以避免重复计算数对
- res.append((i, s))
- return res
阿福: 小美, 怪不得人家都说你是个女汉子, 思维总是这样的简单直接. 可你求真因数和的算法也太低效了, 一点也没有用到因数对称分布的特点啊.
小美: 因数对称分布? 哦, 还真是这样, 瞧我这记性! 那我稍微改进一下, 你再看看.
代码 2:
- def amicable_pair2(n:int) -> list:
- def get_sum(num): #计算 num 的真因数和
- sqr = int(num ** 0.5)
- s = 1
- if num == sqr * sqr:
- s += sqr
- for i in range(2, sqr):
- if num % i == 0:
- s += i + num // i
- return s
- res = []
- for i in range(2, n+1):
- s = get_sum(i)
- if i <s and i == get_sum(s):# 确保 i<s, 以避免重复计算数对
- res.append((i, s))
- return res
阿福: 这还差不多!
古老师: 不错, 充分利用了因数对称分布的特点, 还考虑到了完全平方数的平方根只能加 1 次. 小美进步很大啊!
小美: 都是阿福学长指导有方.
古老师: 都是好孩子! 但是你们有没有注意到, 在 for i in range(2, n+1)循环体内, 你们分别以 i 和 s 作为参数调用了函数 get_sum 两次. 而 s 的值在整个循环过程中是有可能重复出现的.
小美: 没错, s 存储了 i 的真因数和, 是有可能重复出现的. 但这也是没有办法的事情啊, 不计算 get_sum(s), 就无法判断 i 和 s 是否为相亲数.
阿福: 有办法! 可以把每个整数的真因数和都存储起来, 这样每个整数都只需调用一次函数 get_sum 了.
小美: 有道理, 那我明白了!
代码 3:
- def amicable_pair3(n:int) -> list:
- def get_sum(num): #计算 num 的真因数和
- sqr = int(num ** 0.5)
- s = 1
- if num == sqr * sqr:
- s += sqr
- for i in range(2, sqr):
- if num % i == 0:
- s += i + num // i
- return s
- res = []
- p = [0] * (n + 1) #用 p[i]来存储 i 的真因数和
- for i in range(2, n+1):
- s = get_sum(i)
- if s <= n: #确保 s<=n, 以避免下标越界
- p[i] = s
- for i in range(2, n+1):
- if p[p[i]] == i and i <p[i]:# 确保 i<p[i], 以避免重复计算数对
- res.append((i, p[i]))
- return res
阿福: 小美动作好快! 我刚想到思路, 你就把代码写出来了.
古老师: 小美的方法固然不错, 但阿福你就没有别的想法了吗?
阿福: 别的想法? 小美的方法效率已经很高了啊.
古老师: 相比算法 2, 算法 3 的效率确实有了大幅度提升, 但它仍然停留在调用函数 get_sum, 计算真因数和的思路上. 可 get_sum 函数本身就是算法的瓶颈所在, 就像在统计完全平方数的问题中, 开根号运算是瓶颈一样.
小美: 开根号运算是瓶颈? 对, 我们上次就是逆向思维, 用乘法运算代替开根号运算的.
阿福: 逆向思维? 该怎么逆向思维呢?
古老师: 放弃原有包袱, 去除 get_sum 函数的思维定式, 不要老想着一次性把整数 i 的真因数和求出来, 这样思想就解放了.
小美: 不要一次性把整数 i 的真因数和求出来? 难道还可以分开求吗?
阿福: 分开求? 还真是可以啊! 我明白了!
代码 4:
- def amicable_pair4(n:int) -> list:
- res = []
- p = [1] * (n + 1) #用 p[i]来存储 i 的真因数和
- for j in range(2, n//2+1):
- for i in range(j*2, n+1, j):
- p[i] += j #因为 i 是 j 的倍数, 故 j 为 i 的某个真因数
- for i in range(2, n+1):
- if i <p[i] <= n and p[p[i]] ==i:# 确保 i<p[i], 以避免重复计算数对
- res.append((i, p[i]))
- return res
小美: 遍历 j, 然后把 j 加到它的倍数 i 的 p[i]上去, 这样可以用加法运算代替求余数运算, 简直是神操作啊! 阿福你实在是太棒了!
古老师: 孺子可教也! 阿福你的确掌握以空间换时间和逆向思维的精髓了. 说起以空间换时间的方法, 我这里恰好有一个绝佳的例子, 可以作为本节课的课后练习. 今天就到这, 下次再见咯.
题目 2:
函数功能: 找出字符串中第一个只出现一次的字符
函数名: first_one(s:str)-> str:
参数表: s -- 由 ASCII 码组成的字符串.
返回值: 返回第一个只出现一次的字符, 如果不存在返回 "No".
示例 1:
s="asdhfasdf", 返回 "h";
示例 2:
s="asdfasdf", 返回 "No";
彩蛋:
小美: 这不就是统计字符串出现的次数吗, 和以空间换时间有什么关系呢?
阿福: 这个我知道, 好像是利用了桶排序的思想, 把每一个字符都看做是一个桶, 该字符每出现一次就装到桶里计数.
小美: 总共 256 个 ASCII 码字符, 那就需要 256 个桶啊?
阿福: 是的, 计数完毕以后再遍历字符串, 就能快速找到第一个只出现一次的字符了.
小美: 原来是这样, 那我明白了.
代码 5:
- def first_one(s):
- lib = [0] * 256
- for e in s:
- lib[ord(e)] += 1
- for e in s:
- if lib[ord(e)] == 1:
- return e
- else:
- return "No"
阿福: 使用列表作为桶, 需要事先规定桶的数量. 还可以使用字典来存储, 以某个字符和其出现的次数作为键值对, 这样可以扩展到 Unicode 字符. 代码如下:
代码 6:
- def first_one2(s):
- lib = {
- }
- for e in s:
- lib[e] = lib.setdefault(e, 0) + 1
- for e in s:
- if lib[e] == 1:
- return e
- else:
- return "No"
来源: http://www.jianshu.com/p/7251f1d4588e