【算法】复杂度分析

本文围绕代码复杂度分析展开,介绍了时间复杂度和空间复杂度分析的意义,阐述了大O复杂度表示法,讲解了时间复杂度分析的加法和乘法法则,列举了常见时间复杂度量级。还深入探讨了最好、最坏、平均、均摊4种时间复杂度,帮助开发者评估算法执行效率。

第一章、如何分析代码的执行效率和资源消耗

        我们知道,数据结构和算法解决的是“快”和“省”的问题,也就是如何让代码运行得更快,一级如何让代码更节省计算机的存储空间。因此,执行效率是评价算法好坏的一个非常重要的指标。那么,如何衡量算法的执行效率尼?这里就要用到我们本节要讲的内容:时间复杂度分析和空间复杂度分析。

一、复杂度分析的意义

        我们把代码运行一遍,通过监控和统计手段,就能得到算法执行的时间和占用的内存大小,为什么还要做时间复杂度分析,空间复杂度分析呢?这种“纸上谈兵”似的分析方法比实实在在地运行一遍代码得到的数据更准确吗?

        实际上,这是两种不同的评估算法执行效率的方式。对于运行代码来统计复杂度的方法,很多有关数据结构和算法的图书还给它起了一个名字:事后统计法。这种统计方法看似可以给出非常精确的数值,但是却有非常大的局限性。

1、测试结果受测试环境的影响很大

        在测试环境中,硬件的不同得到的测试结果会有很大的差异。例如,我们用同样一段代码分别在安装了Intel Core i9处理器(CPU)和Intel Core i3处理器的计算机上运行,显然,代码在安装了Intel Core i9处理器的计算机上要比在安装了Intel Core i3处理器的计算机上的执行速度快得多。又如,在某台机器上,a代码执行的速度比b代码快,当我们换到另外一台配置不同的机器上时,可能会得到截然相反的运行结果。

2、测试结果受测试数据的影响很大

        我们会在后续章节详细讲解排序算法,这里用它进行举例说明。对同一种排序算法,待排序数据的有序度不一样,排序执行的时间会有很大的差别。在极端情况下,如果数据已经是有序的,那么有些排序算法不需要做任何操作,执行排序的时间就会非常短。除此之外,如果测试数据规模太小,那么测试结果可能无法真实地反应算法的性能。例如,对于小规模的数据排序,插入排序反而比快速排序快!

        因此,我们需要一种不依赖具体的测试环境和测试数据就可以粗略地估计算法执行效率的方法。这就是本节要介绍的时间复杂度分析和空间复杂度分析。

二、大O复杂度表示法

        如何在不运行代码的情况下,用“肉眼”分析代码后得到一段的执行时间尼?下面用一段非常简单的代码来举例,看一下如何估算代码的执行时间。求1~n的累加和的代码如下所示:

public static int cumulativeSum(int n){
        int result = 0;
        for (int i = 1; i <= n; i++){
            result += i;
        }
        return result;
}

        从在CPU上运行的角度来看,这段代码的每一条语句执行类似的操作:读数据--运算--写数据。尽管每一条语句对应的执行时间不一样,但是,这里只是粗略估计,我们可以假设每条语句执行的时间一样,为unit_time。在这个假设的基础上,这段代码的总执行时间是多少尼?

        执行第2,6行代码分别需要1个unit_time的执行时间;第3,4行代码循环运行了n遍,需要 2n x unit_time的执行时间。因此,这段代码的总执行时间为(2n + 2) x unit_time的执行时间。通过上面的举例分析,我们得到一个规律:一段代码的总的执行时间为T(n)(例子中的(2n + 2) x unit_time)与每一条语句的执行次数(累加数)(例子中的2n + 2)成正比。

        按照这个分析思路,我们再来看另一段代码,如下所示:

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

        依旧假设每条语句的执行时间为unit_time,那么这段代码的总的执行时间是多少尼?

        对于第2,3,4,11行代码,每行代码需要1个unit_time的执行时间。第5,6行代码循环执行了n遍,需要2n x unit_time的执行时间。第7,8行代码循环执行了n²遍,需要2n² x unit_time的执行时间。因此,整段代码总的执行时间为T(n) = (2n² + 2n + 4) x unit_time。尽管我们不知道unit_timede 具体值,而且,每一条语句执行时间unit_time可能都不尽相同,但是,通过这两段代码执行时间的推导过程,可以得到一个非常重要的规律:

一段代码的执行时间T(n)与每一条语句总的执行次数(累加数)成正比。

我们可以把这个规律总结成一个公式,如下所示:

T(n) = O(f(n))

        下面具体解释一下公式。其中,T(n)表示代码执行的总时间;n表示数据规模;f(n)表示每条语句执行次数的累加和,这个值与n有关,因此用f(n)这样一个表达式来表示;公式中的O这个符号,表示代码的执行时间T(n) 与 f(n)成正比。

        套用这个大O表示法,第一个例子中的T(n) = (2n + 2) x unit_time = O(2n + 2),第二个例子中的T(n) =  (2n² + 2n + 4) x unit_time = O(2n² + 2n + 4)。实际上,大O时间复杂度并不具体表示代码真正的执行时间,而是表示代码执行时间随着数据规模增大的变化趋势,因此,也称为渐进时间复杂度(asymptotic time complexity),简称时间复杂度。

        当n很大时,读者可以把它想象成10000,100000,公式中的低阶,常量,系数3部分并不控制增长趋势,因此可以忽略。我们只需要记录一个最大量级。如果用大O表示法表示上面的两段代码的时间复杂度,就可以记为:T(n) = O(n) 和 T(n) = O(n²)。        

补充知识:

在数学中,我们经常会听到关于“高阶”、“低阶”、“常量”和“系数”的术语。让我来解释一下:

  1. 高阶(High-order):在多项式或函数中,高阶项是指指数较高的项。例如,在多项式 ax^n+bx^(n−1)+cx^(n−2)+… +d 中,ax^n 就是高阶项,n 是高阶项的指数。通常来说,当 n 越大,该项的影响就越显著,因此被称为“高阶”。

  2. 低阶(Low-order):与高阶相对应,低阶项是指指数较低的项。在上面的多项式中,bx^(n−1) 和 cx^(n−2) 就是低阶项。这些项的影响相对较小,因为它们的指数较低。

  3. 常量(Constant):常量是没有包含任何变量的项,它们是数学表达式中的固定值。在多项式 ax^n+bx^(n−1)+cx^(n−2)+… +d 中,d 就是常量项。

  4. 系数(Coefficient):系数是与变量相乘的数字或参数。在多项式 ax^n+bx^(n−1)+cx^(n−2)+… +d 中,a、b 和 c 都是各自项的系数。系数决定了每个变量项的影响程度,它们可以是实数、复数或其他数学结构的成员。

在一个多项式中,通常高阶项对函数的整体形状和行为有着更显著的影响,而低阶项和常量则在更小的尺度上调整函数的细节。系数则决定了每个项的具体贡献。系数决定了变量的比例关系和对整个公式的影响程度。它们可以改变公式的斜率、曲线形状和整体大小。

三、时间复杂度分析方法

前面介绍了时间复杂度的由来和表示方法。现在,我们介绍一下如何分析一段代码的时间复杂度。下面讲解两个比较实用的法则:加法法则和乘法法则。

1、时间复杂度

时间复杂度是指执行算法所需要的计算工作量

        时间复杂度是用来衡量算法执行时间随着输入大小增加而增加的程度的一个度量。它表示算法的运行时间与输入数据的大小之间的关系。

        在计算时间复杂度时,通常考虑最坏情况下的运行时间,因为这能够给出算法的最差执行时间保证。时间复杂度用大O符号表示,通常写作O(f(n)),其中n表示输入大小,f(n)是一个函数,它描述了算法执行所需的时间与n的关系。

        例如,一个具有时间复杂度O(n)的算法表示,当输入大小增加n倍时,它的运行时间也将增加n倍。而一个具有时间复杂度O(n^2)的算法表示,当输入大小增加n倍时,它的运行时间将增加n

算法复杂度分析主要分为时间复杂度分析和空间复杂度分析,以下是其方法和原理的相关介绍: ### 时间复杂度分析 - **事后统计法的局限性**:事后统计法存在一定局限性,它依赖于测试环境和数据规模等因素,不同的测试环境和数据规模可能导致不同的结果,无法准确反映算法的性能[^1]。 - **复杂度分析的优势**:复杂度分析不依赖具体的测试环境和数据规模,通过分析算法的基本操作执行次数来评估算法的性能,能够更准确地反映算法的效率[^1]。 - **大O表示法**:大O表示法用于描述算法的时间复杂度,它表示算法的执行时间与输入规模之间的增长关系。当数据规模足够大时,算法的实际耗时曲线总被 $C⋅f(n)$ 的上界包裹,其中 $C$ 是一个常数,$f(n)$ 是关于输入规模 $n$ 的函数[^1][^4]。 - **常见复杂度表示形式**: - **$O(1)$**:表示常数时间复杂度算法的执行时间不随输入规模的增加而增加,例如哈希表查找操作,无论数据规模多大,查找操作只需要1次,性能绝对稳定[^1][^4]。 - **$O(n)$**:表示线性时间复杂度算法的执行时间与输入规模成线性增长关系,例如线性遍历操作,输入规模为 $n$ 时,需要进行 $n$ 次操作[^1][^4]。 - **$O(logn)$**:表示对数时间复杂度算法的执行时间随输入规模的对数增长,例如二分查找操作,数据规模翻倍时,操作次数仅增加1次[^1][^4]。 - **$O(n logn)$**:表示线性对数时间复杂度,例如快速排序算法,是高效排序的基准[^1][^4]。 - **$O(n²)$**:表示平方时间复杂度,例如冒泡排序算法,适用于小数据规模[^4]。 - **$O(2ⁿ)$**:表示指数时间复杂度,例如汉诺塔问题,随着输入规模的增加,执行时间会急剧增长,性能不可接受[^4]。 - **均摊分析**:每一次 $O(n)$ 的插入操作,都会跟着 $n - 1$ 次 $O(1)$ 的插入操作,把耗时多的那次操作均摊到接下来的 $n - 1$ 次耗时少的操作上,均摊下来,这一组连续的操作的均摊时间复杂度就是 $O(1)$ [^5]。 ### 空间复杂度分析 - **定义与意义**:空间复杂度衡量的是算法在执行过程中所需的额外空间,通常也使用大O符号进行表示,用于评估算法在内存消耗方面的效率[^3]。 - **常见复杂度表示形式**:与时间复杂度类似,$O(1)$ 表示常数空间复杂度,$O(n)$ 表示线性空间复杂度,$O(n^2)$ 表示平方空间复杂度等[^3]。 - **与时间复杂度的权衡**:在实际应用中,需要根据具体情况权衡时间复杂度和空间复杂度。有时可以通过增加空间复杂度来降低时间复杂度,或者通过增加时间复杂度来降低空间复杂度[^1]。 ### 复合复杂度计算 对于复合操作,需要综合考虑各个子操作的复杂度。例如,在一个复合操作中,外层循环复杂度为 $O(n)$,内层进行二分查找复杂度为 $O(log m)$,内层处理复杂度为 $O(m)$,则整个操作的复杂度需要综合分析这些子操作的复杂度关系 [^4]。以下是一个Python代码示例: ```python def binary_search(arr, target): left, right = 0, len(arr) - 1 while left <= right: mid = (left + right) // 2 if arr[mid] == target: return True elif arr[mid] < target: left = mid + 1 else: right = mid - 1 return False def process(num): # 这里可以是对数字的具体处理操作 pass def complex_operation(matrix, target): count = 0 # 外层循环 O(n) for row in matrix: # 二分查找 O(log m) if binary_search(row, target): # 内层处理 O(m) for num in row: process(num) count += 1 return count ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值