python算法(基础)---算法分析

算法分析

     1.首先我们通过 ‘计算前n个整数的和’ 的两个不同算法,来体验下同种问题下不同算法之间的差异。

  • 计算前n个整数的和,这里使用初始化值为0的累加器变量来迭代n个整数。(一个简单的累加函数)
def sumOfN(n):
    theSum=0
    for	i in range(1,n+1):
        theSum=theSum	+i
    return theSum
print(sumOfN(10))

在Python中,我们可以通过记录相对于系统的开始时间和结束时间来对函数进行基准测试。在time模块中有一个time函数,它可以在任意被调用的地方返回系统时钟的当前时间(以秒为单位)。通过在开始和结束的时候分别调用两次time函数,然后计算差异,就可以得到一个函数执行花费的精确秒数(大多数情况下是这样)。

import time
def sum(n):
    start=time.time()
    theSum=0
    for i in range(1,n+1):
        theSum=theSum+i
    end =time.time()
    return theSum,end-start
for i in range(5):
    print('总和:%d ;   用时:%10.7f seconds'%sum(10000))

我们执行这个函数5次,每次计算前10000个整数的和将得到如下结果(不同计算机运行的结果不同):

总和:50005000 ;   用时: 0.0009999 seconds
总和:50005000 ;   用时: 0.0009999 seconds
总和:50005000 ;   用时: 0.0009999 seconds
总和:50005000 ;   用时: 0.0009999 seconds
总和:50005000 ;   用时: 0.0010002 seconds

我们发现时间是相当一致的,执行这段代码平均需要0.0009秒。如果我们运行计算前 100,000 个整数的和的函数呢?

总和:5000050000 ;   用时: 0.0079999 seconds
总和:5000050000 ;   用时: 0.0080001 seconds
总和:5000050000 ;   用时: 0.0079999 seconds
总和:5000050000 ;   用时: 0.0079999 seconds
总和:5000050000 ;   用时: 0.0080001 seconds

得到的结果是,运行的时间更长了,但是每次运行所需的时间也是非常一致的。(可自行测试下n=1000000的情况)

  • 这里我们使用封闭方程来计算前n个整数的和,来体验下与上面算法的差异。

封闭方程如下图:

代码实现:

import time
def sum(n):
    start=time.time()
    m=(n * (n + 1)) / 2
    end =time.time()
    return m,end-start
for i in range(5):
    print('总和:%d ;   用时:%10.7f seconds'%sum(10000000000))

如果我们对 sum函数做同样的基准测试,使用 5个不同的n(10,000, 100,000, 1,000,000,10,000,000和 100,000,000),我们得到如下结果:

总和:50005000 ;   用时: 0.0000000 seconds
总和:5000050000 ;   用时: 0.0000000 seconds
总和:500000500000 ;   用时: 0.0000000 seconds
总和:50000005000000 ;   用时: 0.0000000 seconds

在这个输出中有两件事需要重点关注,首先上面记录的执行时间比之前任何例子都短,另外,他们的执行时间和n无关,看起来sum函数几乎不受n的影响。

  • 这个基准测试能告诉我们,使用迭代的解决方案需要做更多的工作,因为一些程序步骤被重复执行。这可能是它需要更长时间的原因。此外,迭代方案执行所需时间随着n递增。

  •  

2.大O符号

对于先前的求和算法,一个比较好的基本计算单位是对执行语句进行计数。在sum中,赋值语句的计数为1(Sum = 0加上n的值(我们执行Sum = Sum + i的次数)。我们通过函数T表示T(n) = 1+ n。参数n通常称为“问题 的规模”,我们称作“T(n)是解决问题大小为n所花费的时间,即1+n步长”。在上面的求和函数中,使用n来表示问题大小是有意义的。我们可以说,100,000个整数和比1000个问题规模大。因此,所需时间也更长。我们的目标是表示出算法的执行时间是如何相对问题规模大小而改变的。当问题规模变大时,T(n)函数某些部分的分量会超过其他部分。函数的数量级表示了随着n的值增加而增加最快的那些部分。数量级通常称为大O符号,写为O(f(n))。它表示对计算中的实际步数的近似。函数f(n)提供了T(n)最主要部分的表示方法。在上述示例中,T(n) = 1+ n。当n变大时,常数1对于最终结果变得越来越不重要。如果我们找的是T(n)的近似值,我们可以删除1,运行时间是O(n)。要注意,1对于T(n)肯定是重要的。但是当n变大时,如果没有它,我们的近似也是准确的。

另外一个示例,假设对于一些算法,确定的步数是T(n) = 5n2+27n+1005。当n很小时,例如1或2,常数 1005似乎是函数的主要部分。然而,随着n变大,n这项变得越来越重要。事实上,当n真的很大时,其他两项在它们确定最终结果中所起的作用变得不重要。当n变大时,为了近似T(n),我们可以忽略其他项,只关注5n2。系数5也变得不重要。我们说,T(n)具有的数量级为f(n) = n2,或者O(n2)。

下面用图来说明n的变化

Figure1表示了Table1中的函数图。注意,当n很小时,函数彼此间不能很好的定义。很难判断哪个是主导的。随着n 的增长,就有一个很明确的关系,很容易看出它们之间的大小关系。

最后一个例子,假设我们有哦如下代码段。虽然这个程序没有做任何事,但是对我们获取实际的代码和性能分析是有益的。

a=5
b=6
c=10
for i in range(n):
    for j in range(n):
        x=i*i
        y=j*j
        z=i*j
for k in range(n):
    w=a*k+45
    v=b*b
d=33

分配操作数,分为四个项的总和。第一个项是常数3,表示片段开始的三个赋值语句。第二项是3n2,因为由于嵌套迭代,有三个语句执行n次。第三项是2n,两个语句迭代n次。最后,第四项是常数1,表示最终赋值语句。最后得出T(n) = 3+3n2+2n +1 = 3n2+2n +4,通过查看指数,我们可以看到n项是显性的,因此这个代码段是O(n2)。当n增大时,所有其他项以及主项上的系数都可以忽略。

3.一个乱序字符串检查的例子:

显示不同量级的算法的一个很好的例子是字符串的乱序检查。乱序字符串是指一个字符串只是另一个字符串的重新排列。例如,'heart'和'earth'就是乱序字符串。'python'和'typhon'也是。为了简单起见,我们假设所讨论的两个字符串具有相等的长度,并且他们由 26个小写字母集合组成。

  • 解法一:检查
def anagramSolution1(s1,s2):
    alist = list(s2)
    pos1 = 0
    stillOK = True
#正序和长度不等的情况
    if s1==s2 or len(s1)!=len(s2):
        return False
    else:
        while pos1 < len(s1) and stillOK:
            pos2 = 0
            found = False
            while pos2 < len(alist) and not found:
                if s1[pos1] == alist[pos2]:
                    found = True
                else:
                    pos2 = pos2 + 1
            if found:
                alist[pos2] = None
                pos1 = pos1 + 1
            else:
                stillOK = False
        return stillOK
print('是否是反序字符串:',anagramSolution1('abcdd','ddcba'),)

为了分析这个算法,我们注意到 s1 的每个字符都会在 s2 中进行最多 n 个字符的迭代。s2 列 表中的 n 个位置将被访问一次来匹配来自 s1 的字符。访问次数可以写成 1 到 n 整数的和, 可以写成

当 n 变大,n2 这项占据主导,1/2 可以忽略。所以这个算法复杂度为 O(n2)。

  • 解法二:排序和比较

另一个解决方案是利用这么一个事实:即使 s1,s2 不同,它们都是由完全相同的字符组成的。所以,我们按照字母顺序从a到z排列每个字符串,如果两个字符串相同,那这两个字符串就是乱序字符串。

def anagramSolution2(s1,s2):
    alist1 = list(s1)
    alist2 = list(s2)
    alist1.sort()
    alist2.sort()
    pos = 0
    matches = True
    while pos < len(s1) and matches:
        if alist1[pos]==alist2[pos]:
            pos = pos + 1
        else:
            matches = False
    return matches
print('是否是反序字符串:',anagramSolution2('abcde','cdabe'))

首先你可能认为这个算法是 O(n),因为只有一个简单的迭代来比较排序后的 n 个字符。但是,调用 Python 排序不是没有成本。正如我们将在后面的章节中看到的,排序通常是 O(n2) 或 O(nlogn)。所以排序操作比迭代花费更多。最后该算法跟排序过程有同样的量级。

  • 解法三:计数和比较

我们最终的解决方法是利用两个乱序字符串具有相同数目的 a, b, c 等字符的事实。我们首先 计算的是每个字母出现的次数。由于有 26 个可能的字符,我们就用一个长度为 26 的列表,每个可能的字符占一个位置。每次看到一个特定的字符,就增加该位置的计数器。最后如果两个列表的计数器一样,则字符串为乱序字符串。

def anagramSolution4(s1,s2):
    if s1==s2:
        return False
    else:
        c1 = [0]*26
        c2 = [0]*26
        for i in range(len(s1)):
#ord()返回值是对应的十进制整数。返回对应的 ASCII 数值,或者 Unicode 数值,如果所给的 Unicode 字符超出了你的 Python 定义范围,则会引发一个 TypeError 的异常。
            pos = ord(s1[i])-ord('a')
            c1[pos] = c1[pos] + 1
        for i in range(len(s2)):
            pos = ord(s2[i])-ord('a')
            c2[pos] = c2[pos] + 1
        j = 0
        stillOK = True
        while j<26 and stillOK:
            if c1[j]==c2[j]:
                j = j + 1
            else:
                stillOK = False
        return stillOK
print('是否是反序字符串:',anagramSolution4('apple','apple'))

同样,这个方案有多个迭代,但是和第一个解法不一样,它不是嵌套的。两个迭代都是n, 第三个迭代,比较两个计数列表,需要 26 步,因为有 26 个字母。一共 T(n)=2n+26 ,即 O(n),我们找到了一个线性量级的算法解决这个问题。

在结束这个例子之前,我们来讨论下空间花费,虽然最后一个方案在线性时间执行,但它需要额外的存储来保存两个字符计数列表。换句话说,该算法牺牲了空间以获得时间。很多情况下,你需要在空间和时间之间做出权衡。这种情况下,额外空间不重要,但是如果有数百万个字符,就需要关注下。

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

狼性书生

谢谢鼓励!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值