COMP9021 Principles of Programming WEEK3

1.寻找符合条件的平方数组

requirement:找三个完全平方数,它们的数字从1-9中选择,且不重复,找到所有满足条件的平方数组。例如16,25,73984符合条件。

1.1 确定范围

要寻找符合条件的数字,一定要用到循环,这样就要先找到循环的范围。下限很容易确定,是数字1,上限的确定需要思考。
上限一定小于986754321,可这个数字不合适,循环和计算时间往往是成指数倍的关系,要尽可能缩小范围。接着思考,共有三个数,且都是完全平方数,那么最大的数应该是从986754321中拿掉两个最小的完全平方数剩下的部分。最小的完全平方数是1,4,这样就确定了上限的数字,应该是9876532。

1.2 确定条件

为了满足题中要求的不重复的1-9的数字,选中平方数使用的数字需要记录。接下来考虑到细节,虽然1-9的数字是题中要求的,但是在遍历数字的过程中一定还会有数字0,所以初始出现的数字不能是空集,应该是一个包含数字0的数据类型,比如dict或者set。
接着写一个函数记录出现过的数字:

def f(number, digits_seen_so_far):
    number_as_string = str(number)
    new_digits = digits_seen_so_far | set(number_as_string)
    #符号|用来快速连接set,比如{'1', '2'} | {'3'},输出{'1', '2', '3'}
    #符号|其实是OR,同样还有AND,用&,例如,{1, 2} & {1, 2, 3},输出是{2}
    if len(new_digits) == len(digits_seen_so_far) + len(number_as_string):
        return new_digits
    else:
        return None

1.3 寻找合适的数组

使用string进行比较,寻找合适的数组

from math import sqrt

def f(number, digits_seen_so_far):
    number_as_string = str(number)
    new_digits = digits_seen_so_far | set(number_as_string)
    if len(new_digits) == len(digits_seen_so_far) + len(number_as_string):
        return new_digits
    else:
        return None

N = round(sqrt(9876532))
#不要忘记round,不然N是float,不可以直接用于range函数
all_digits = {str(i) for i in range(10)}

for a in range(1, N+1):
    digits_seen_0 = {'0'}
    #初始条件要包含数字0,注意前后set中的数据类型保持一致,都是str
    x = a * a
    digits_seen_1 = f(x, digits_seen_0)
    if not digits_seen_1:
        continue
    for b in range(a + 1, N + 1):
        y = b * b
        digits_seen_2 = f(y, digits_seen_1)
        if not digits_seen_2:
            continue
        for c in range(b + 1, N + 1):
            z = c * c
            digits_seen_3 = f(z, digits_seen_2):
            if not digits_seen_3:
                continue
            if digits_seen_3 == all_digits:
                print(f'{x}, {y}, and {z} is a solution')

1.4 使用set优化程序

使用string比较判断出现过的数字效率低,换成set比较,使用int数据类型。

from math import sqrt

def f(number, digits_seen_so_far):
    while number:
        d = number % 10
        if d in digits_seen_so_far:
            return
        digits_seen_so_far.add(d)
        #{set}.add(number)是set添加元素的方法
        number //= 10
    return digits_seen_so_far

N = round(sqrt(9876532))
all_digits = set(range(10))

for a in range(1, N + 1):
    digits_seen_0 = {0}
    x = a * a
    digits_seen_1 = f(x, digits_seen_0)
    if not digits_seen_1:
        continue
    for b in range(a + 1, N + 1):
        y = b * b
        digits_seen_2 = f(y, digits_seen_1)
        if not digits_seen_2:
            continue
        for c in range(b + 1, N + 1):
            z = c * c
            digits_seen_3 = f(z, digits_seen_2)
            if not digits_seen_3:
                continue
            if digits_seen_3 == all_digits:
                print(f'{x}, {y} and {z} is a solution')

1.5 使用binary进阶优化

set的算法速度已经很快,但是对于更加庞大的运算依然有提升空间。程序的优化往往是从方便程序员思考,走向模拟计算机内存处理过程而实现的。
这个问题判断数字是否出现过,可以进一步进阶到二进制中判断。
引入位运算的概念:

1 << 0, 1 << 1, 1 << 2, 1 << 3
>>>(1, 2, 4, 8)
#含义是向左侧0,1,2,3位添加数字1,这样在binary中1代表1,10代表2,100代表4,1000代表8

这个题目中数字要求是1-9,所以可以把它替代为二进制的9位数,例如,如过出现数字1,则1<<1,已有的数字是10;再出现数字3,则1<<3,已有的数字是1010。直到9个数字都出现,那么二进制显示的数字是1111111110。如果在考虑数字0,可以放在defalut中,初始是二进制的1,它等于十进制的1;终止是二进制的111111111,它是十进制的2**10 - 1。
接着思考如何判断一个数字是否出现过,可以使用&,添加一个出现的数字,使用OR运算。例如,出现过的数字是1,2,3,那么二进制中的数字是1110。此时新的数字出现3,对应的二进制是1000,判断3是否出现过对比的是1000的1所对应的位置上的数字,1110对应的是数字1,这样1&1为True,说明出现过,如果原来没有出现过3,那么记录中的这一位应该是数字0,则0&1为False,说明没出现过。
这样程序优化为:

from math import sqrt

def f(number, digits_seen_so_far):
    while number:
        d = number % 10
        if (1 << d) & digits_seen_so_far:
            return
        digits_seen_so_far |=  1 << d
        number //= 10
    return digits_seen_so_far

N = round(sqrt(9876532))
all_digits = 2 ** 10 - 1

for a in range(1, N + 1):
    digits_seen_0 = 1
    x = a * a
    digits_seen_1 = f(x, digits_seen_0)
    if not digits_seen_1:
        continue
    for b in range(a + 1, N + 1):
        y = b * b
        digits_seen_2 = f(y, digits_seen_1)
        if not digits_seen_2:
            continue
        for c in range(b + 1, N + 1):
            z = c * c
            digits_seen_3 = f(z, digits_seen_2)
            if not digits_seen_3:
                continue
            if digits_seen_3 == all_digits:
                print(f'{x}, {y} and {z} is a solution')

2.寻找质数

例如,寻找50以内的质数,那么循环范围很容易确定,是range(2, 51),最简单的方法是遍历所有数字,只要有除了自身以外的数能整除,则不是质数。

def print_primes(L):
    for i in range(2, len(L)):
        if L[i]:
            print(i, end = ' ')
    print()
L = [True] * 50
print_primes(L)
for i in range(2, len(L)):
    j = 2
    while i * j < len(L):
        L[i * j] = False
        j += 1
    print_primes(L)

但很显然这样的两个循环嵌套做了很多重复运算。
优化程序,首先思考给定50的范围,循环范围是否可以缩小,之前是直接用range(2, 51),可实际上25之后就不需要遍历,因为25*2=50。

from math import sqrt

def print_primes(L):
    for i in range(2, len(L)):
        if L[i]:
            print(i, end = ' ')
    print()
L = [True] * 51
print_primes(L)
max_number = len(L) - 1
upper_bound = round(sqrt(max_number))
for i in range(2, upper_bound + 1):
    j = 2
    while i * j <= max_number:
        L[i * j] = False
        j += 1
    print_primes(L)

进一步思考发现,两个循环遍历i和j,j可以从i * i开始,如果i不是质数,则可以跳过。

from math import sqrt

def print_primes(L):
    for i in range(2, len(L)):
        if L[i]:
            print(i, end = ' ')
    print()
L = [True] * 26
print_primes(L)
max_number = len(L) - 1
upper_bound = round(sqrt(max_number))
for i in range(2, upper_bound + 1):
    if not L[i]:
        continue
    j = i * i
    while i * j <= max_number:
        L[i * j] = False
        j += 1
    print_primes(L)

3.Python运行效率初探

python初始会有一个固定的运算空间,当越界的时候会重新规划申请更多的运算空间,所以运行效率不是平稳改变的,在越界的时候会花更多的时间进行布置。
例如,list的append只需要在最后加一个数,程序复杂度应该是线性的,但实际上会有波动,这些波动就是python在重新安排运算空间。

%matplotlib inline
from matplotlib.pyplot import plot

from time import time

data = []
for i in range(1000, 50001, 1000):
    L = []
    before = time()
    for _ in range(i):
        L.append(None)
    after = time()
    data.append((i, after - before))
plot(*tuple(zip(*data)))
print()

例如,list的insert运算每次都要把所有存在于list中的数向后一位保存,复杂度是指数性的。

%matplotlib inline
from matplotlib.pyplot import plot

from time import time

data = []
for i in range(1000, 50001, 1000):
    L = []
    before = time()
    for _ in range(i):
        L.insert(0, None)
    after = time()
    data.append((i, after - before))
plot(*tuple(zip(*data)))
print()
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容