最大k乘积的时间复杂度_数据结构与算法(2)时间复杂度与空间复杂度分析

本文深入探讨了时间复杂度分析在算法学习中的重要性,介绍了大O表示法来衡量算法的执行效率,包括常量、系数、低阶项的忽略原则。通过实例解析了如何分析代码的时间复杂度,如关注循环次数最多的代码、执行量级最大的代码以及嵌套代码的时间复杂度。此外,文章还提到了常见的时间复杂度量级,如O(1)、O(Logn)、O(n)、O(nLogn)和O(n^2),并强调了空间复杂度分析的重要性。
摘要由CSDN通过智能技术生成
fa23062396e21c861f193ca4c46a42d6.png

前言

数据结构和算法解决的是执行速度和存储空间的问题,即如何让代码运行更快,如何让存储更省空间。因此,执行效率就是算法非常重要的考量指标,而算法的执行效率考量就必须用到时间复杂度分析和空间复杂度分析。复杂度分析是算法学习的基础,只有学会分析算法的时间复杂度和空间复杂度,才能评判算法的优劣,才能写出更快更省存储空间的算法。

为什么学习复杂度分析

在懂得复杂度分析之前,其实我们也在分析算法的优劣,就是把代码跑一遍,然后统计下执行时间,监控下占用的内存,这种事后统计法并不是不可以,只是这种方式有很大的局限性和不确定性。

  • 测试环境影响

同一算法在不同硬件条件下的测试结果有可能不一致,比如拿Intel Core i9的处理和Intel Core i3的处理器测试同一算法,结果很可能是不同的,甚至都有可能相反。

  • 数据规模影响

有些算法在数据规模比较小的情况下,是看不出有什么优势的,只有数据量成规模后才能拉开和普通算法的差距。

所以我们需要一个不用执行代码,也不用测试数据测试就能粗略估算算法执行效率的方法,这就是时间、空间复杂度分析法。

大O复杂度表示法

算法的执行效率,大多数情况下说的就是算法的执行时间,但是如果不运行代码,我们怎样能看出算法的执行时间呢?

我们来看下面一段代码:

// 求解1+2+3+4+...+nprivate static int add(int n) { int addres = 0; int i = 1; for (; i <= n; i++) { addres = addres + i; } return addres;}

这段代码是求1,2,3,4...n的累加和,我们来估算一下这段代码的执行时间。首先,我们假定每行代码执行的时间都是一样的,定义为exetime,第3行和第4行代码分别需要1个exetime的时间,第5行和第6行执行了n便,也就是各自需要n*exetime的时间,也就是这段代码的总执行时间T就是2n+2个exetime,可以看出来,总的执行时间T和执行次数n成正比,

按照这个思路,我们再来看下面这段代码:

private static int add2(int n) { int sum = 0; int i = 1; int j = 1; for (; i <= n; ++i) { j = 1; for (; j <= n; ++j) { sum = sum + i * j; } } return sum;}

我们依然将每行代码的执行时间定义为exetime,第2、3、4行代码,每行都需要1个exetime,第5行和第6行各自执行了n遍,需要2n * exetime的执行时间,第7行和第8行各自执行了n * n遍,所以需要2 * n^2个exetime,所以整段代码总的执行时间为:

T=(2n^2+2n+3)*exetime。

尽管我们并不知道exetime的具体值,但是通过以上两段代码,我们可以得出一个规律,就是代码的执行时间T和每行代码的执行次数n成正比。我们可以把这个规律表示为一个公式:T(n)=O(f(n))。其中,T(n)表示代码的执行时间,n标识数据规模的大小,f(n)表示每行代码执行次数总和,O则代表执行时间T和代码执行次数f成正比。这就是大O时间复杂度表示法。大O时间复杂度表示法实际上并不具体表示代码的真正执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以,大O时间复杂度也叫渐进时间复杂度,简称时间复杂度。

当n很大时,比如几十万、几百万。公式中的低阶、常量、系数三部分并不会影响时间的增长趋势,所以都可以忽略。我们只需要记录一个最大量级就可以了,如果用大O 表示法表示刚讲的那两段代码的时间复杂度,就可以记为:

T(n) = O(n),T(n) = O(n^2)。

时间复杂度分析

上面我们简单介绍了什么是大O时间复杂度,下边我们看下如何分析一段代码的时间复杂度。

1. 关注循环次数最多的代码

上面我们说了,大O时间复杂度表示法只是表示执行时间随数据规模增长的趋势,通常情况下,我们可以忽略低阶、常量和系数,只需要记录最高阶的量级。同样,在分析一段代码、一个算法的时间复杂度的时候,也只需要关注循环执行次数最多的那段代码就可以了,这段核心代码执行次数n的量级,就是整段代码的时间复杂度。如下面这段代码:

private static int add(int n) { int addres = 0; int i = 1; for (; i <= n; i++) { addres = addres + i; } return addres;}

第2、3行代码的执行时常量级的时间,对时间复杂度没有影响,可以忽略,循环执行的4、5行代码,被执行了n次,时间复杂度就是O(n)。

2. 关注执行量级最大的代码

我们看下面这段代码:

private static int add3(int n) { int sum_1 = 0; int p = 1; for (; p < 100; ++p) { sum_1 = sum_1 + p; }  int sum_2 = 0; int q = 1; for (; q < n; ++q) { sum_2 = sum_2 + q; } int sum_3 = 0; int i = 1; int j = 1; for (; i <= n; ++i) { j = 1; for (; j <= n; ++j) { sum_3 = sum_3 + i * j; } } return sum_1 + sum_2 + sum_3;}

这段代码分为三部分,也就是三个循环体,其余常量级的代码可忽略。第一段代码执行了100次,所以执行时间也是常量级的,可以忽略,其实就算是这段代码执行几千次,几万次乃至几十万次,只要是执行次数是已知的,不随数据规模n变化的,我们都可以认为它的执行时间是常量级的。

第二个循环和第三个循环的时间复杂度是O(n)和O(n^2),综合这三段循环的时间复杂度量级,此时,我们取其中量级最大的时间复杂度,也就是O(n^2),也就是总的时间复杂度等于量级最大的那段代码的时间复杂度。

3. 关注嵌套代码

对于循环内嵌套循环的代码的时间复杂度,我们取内外循环时间复杂度的乘积:

private static int add(int n) { int sum1 = 0; int i = 1; for (; i <= n; i++) { sum1 = sum1 + i; } return sum1;}private static int add4(int n) { int sum4 = 0; int i = 1; for (; i < n; ++i) { sum4 = sum4 + add(i); } return sum4;}

以上代码,两个循环的时间复杂度都是O(n),所以add4函数的时间复杂度就是:

O(n)*O(n)=O(n^2)。

常见时间复杂度分析实例

常见的时间复杂度量级如下,按数量级依次递增:

40b68723188e5eda8268aead68857c10.png

以上时间复杂度量级可以分为两类:多项式量级和非多项式量级,其中,非多项式量级有两个:O(2^n)和O(n!)

我们把时间复杂度为非多项式量级的算法问题叫做NP问题,即非确定多项式问题。这种问题有一个特点,就是当数据规模n越来越大时,执行时间会急剧增加,所以这类算法其实是非常低效的算法,这类算法我们很少接触,这里主要介绍常见的多项式时间复杂度。

  • O(1)
  • 只要代码的执行时间不随数据规模n的增长而增长,时间复杂度都是O(1)。也就是只要代码中没有循环语句或者递归语句,并且其引用的代码中没有循环语句和递归语句,即便有几千几万行代码,时间复杂度都是O(1)。
  • O(Logn)、O(nLogn)
  • 对数阶的时间复杂度非常常见,同时也是最难分析的一种复杂度,下面这段代码就是对数阶的时间复杂度:
i=1;while (i <= n) { i = i * 2;}

以上代码很简单,通过前面讲的时间复杂度的分析方法,我们知道第三行代码是执行次数最多的,所以我们只要知道这行代码执行了多少次,就可以知道这段代码的时间复杂度。从代码中可以看出,变量i的值从1开始,每循环一次就乘以2,当大于n时,循环结束。其实这就是中学时学习的等比数列(其实数列这玩意我都忘得差不多了),变量i的取值其实就是个等比数列,我们把它展开就是:

2^0 * 2^1 * 2^2 * ... * 2^k * 2^x=n

所以我们只需要知道x的值是多少,就知道这段代码执行的次数了。通过2^x=n,求x的值也就是2^x=n,所以这段代码的时间复杂度就是:

O(Log2n)

上面公式中的2是脚标哈。实际上我们不管对数的底数是多少,不管是2为底还是3为底,时间复杂度都记为O(Logn)。到现在,我们应该也能理解O(nLogn)了,如果一段代码的时间复杂度是O(Logn),执行n遍的话,那时间复杂度就是O(nLogn),归并排序、快速排序等时间复杂度都是O(nLogn)。

  • O(m+n)、O(m*n)
  • 如果一段代码的复杂度由两个数据的规模来决定,如下代码:
private static int add6(int m, int n) { int sum_1 = 0; int i = 1; for (; i < m; ++i) { sum_1 = sum_1 + i; } int sum_2 = 0; int j = 1; for (; j < n; ++j) { sum_2 = sum_2 + j; } return sum_1 + sum_2;}

上面代码中,代码的复杂度由m和n两个未知数据规模决定,并且我们无法事先知道m和n哪个数据规模更大,这时候时间复杂度就是O(m+n)。同样,如果是嵌套循环,根据乘法法则:

T1(m)*T2(n) = O(f(m) * f(n))。

空间复杂度分析

空间复杂度全称就是渐进空间复杂度,标识算法的处理数据的存储空间与数据规模之间的增长关系。我们看下面这段代码:

public static void array(int n) { int i = 0; String[] a = new String[n]; for (; i < n; ++i) { a[i] = "我是数据" + i; }}

和时间复杂度的分析方式一样,上面这段代码唯一受数据规模影响的就是第3行代码,数组的大小和数据规模成正比,所以它的时间复杂度就是O(n),常见的空间复杂度就是:

O(1)、O(n)、O(n^2)。

最好、最坏时间复杂度

看下面这段代码:

// n表示数组array的长度int find(int[] array, int n, int x) { int i = 0; int pos = -1; for (; i < n; ++i) { if (array[i] == x) { pos = i; break; } } return pos;}

上面这段代码的时间复杂度是多少呢,小伙伴可能会说不就是O(n)吗,其实不然。如果数组中的第一个元素正好是要查找的变量x,那整个循环也就执行一次,这时候时间复杂度就是O(1),如果点背数组中的最后一个元素才是要查找的元素x,或者数组中压根没有要查找的x,那这时候整个数组都会循环一遍,时间复杂度就是O(n),所以在不同情况下,这段代码的时间复杂度是不一样的。这时候我们将最好的情况,也就是执行一次循环就找到元素x的情况定义为最好情况时间复杂度;将最坏的情况,也就是遍历完整个数组都没找到元素x的情况定义为最坏情况时间复杂度


平均时间复杂度

其实最好、最坏时间复杂度都是极端情况下的时间复杂度,有时候我们更多需要的时候平均情况下的时间复杂度,也就是平均时间复杂度。上面那段代码用平均时间复杂度怎么分析呢,要找到x的位置,有n+1种情况,也就是数组0~n-1的位置以及不在数组中的情况。每种情况下,要查找遍历的元素个数是1+2+3+4+...+n+n,除以总的情况数n+1,就是遍历元素个数的平均值,也就是(下边的公式是个分数,暂不知道微信公众号怎么插入数学公式,凑合下吧):

1+2+3+4+...+n+n/n+1=n(n+3)/2(n+1)

去掉可以忽略的常量,时间复杂度就是O(n)。当然这种推理其实很不严谨,如果将所有的情况都考虑进去就需要用到概率论中的加权平均值来计算,这里我们就不说这么详细了。

均摊时间复杂度

均摊时间复杂度对应的分析方法是摊还分析(也叫平摊分析),最好、最坏、平均时间复杂度用到的场景很有限,只有在特殊情况下才会用到,而均摊时间复杂度更特殊,使用场景更加有限。这里我们只简单介绍一下,不做展开,需要详细了解的小伙伴可以自行查阅资料。

对一个数据结构进行一组连续操作中,大部分情况下时间复杂度都很低,只有个别情况下时间复杂度比较高,而且这些操作之间存在前后连贯的时序关系,这个时候,我们就可以将这一组操作放在一块儿分析,看是否能将较高时间复杂度那次操作的耗时,平摊到其他那些时间复杂度比较低的操作上。而且,在能够应用均摊时间复杂度分析的场合,一般均摊时间复杂度就等于最好情况时间复杂度。其实,均摊时间复杂度就是一种特殊的平均时间复杂度。

时间复杂度分析的必要性

写完代码后,花几分钟的时间分析下其时间、空间复杂度是非常有必要的,虽然复杂度分析只是一个理论模型,只能提供粗略的分析结果,但是可以确定的是一个低阶的时间复杂度的程序大概率是优于一个高阶的时间复杂度程序的,如果养成了复杂度分析的习惯,对我们编写出效率比较高的程序是非常有帮助的。

9a60d3598b35dafd5853db7e7900a895.png

微信公众号:行知老王

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值