算法的时间与空间复杂度

如果你也和我一样,对算法的空间复杂度和时间复杂度一直搞不明白,这篇博客也许就是你的菜~

1、算法概念

算法(Algorithm)是指用来操作数据、解决程序问题的一组方法。对于同一个问题,使用不同的算法,也许最终得到的结果是一样的,但在过程中消耗的资源和时间却会有很大的区别。

2、算法优劣衡量维度

那么我们应该如何去衡量不同算法之间的优劣呢?

主要还是从算法所占用的「时间」和「空间」两个维度去考量。

时间维度:是指执行当前算法所消耗的时间,我们通常用「时间复杂度」来描述。
空间维度:是指执行当前算法需要占用多少内存空间,我们通常用「空间复杂度」来描述。
因此,评价一个算法的效率主要是看它的时间复杂度和空间复杂度情况。然而,有的时候时间和空间却又是「鱼和熊掌」,不可兼得的,那么我们就需要从中去取一个平衡点。

下面我来分别介绍一下「时间复杂度」和「空间复杂度」的计算方式。

2.1、 时间复杂度

一、时间复杂度
我们想要知道一个算法的「时间复杂度」,很多人首先想到的的方法就是把这个算法程序运行一遍,那么它所消耗的时间就自然而然知道了。

这种方式可以吗?当然可以,不过它也有很多弊端。
这种方式非常容易受运行环境的影响,在性能高的机器上跑出来的结果与在性能低的机器上跑的结果相差会很大。而且对测试时使用的数据规模也有很大关系。再者,并我们在写算法的时候,还没有办法完整的去运行呢。

因此,另一种更为通用的方法就出来了:「 大O符号表示法 」,即 T(n) = O(f(n))

我们先来看个例子:

for(i=1; i<=n; ++i)
{
   j = i;
   j++;
}

通过「 大O符号表示法 」,这段代码的时间复杂度为:O(n) ,为什么呢?

在 大O符号表示法中,时间复杂度的公式是: T(n) = O( f(n) ),其中f(n) 表示每行代码执行次数之和,而 O 表示正比例关系,这个公式的全称是:算法的渐进时间复杂度。

我们继续看上面的例子,假设每行代码的执行时间都是一样的,我们用 1颗粒时间 来表示,那么这个例子的第一行耗时是1个颗粒时间,第三行的执行时间是 n个颗粒时间,第四行的执行时间也是 n个颗粒时间(第二行和第五行是符号,暂时忽略),那么总时间就是 1颗粒时间 + n颗粒时间 + n颗粒时间 ,即 (1+2n)个颗粒时间,即: T(n) = (1+2n)*颗粒时间,从这个结果可以看出,这个算法的耗时是随着n的变化而变化,因此,我们可以简化的将这个算法的时间复杂度表示为:T(n) = O(n)

为什么可以这么去简化呢,因为大O符号表示法并不是用于来真实代表算法的执行时间的,它是用来表示代码执行时间的增长变化趋势的。

所以上面的例子中,如果n无限大的时候,T(n) = time(1+2n)中的常量1就没有意义了,倍数2也意义不大。因此直接简化为T(n) = O(n) 就可以了。

常见的时间复杂度量级有:

  • 常数阶O(1)
  • 对数阶O(logN)
  • 线性阶O(n)
  • 线性对数阶O(nlogN)
  • 平方阶O(n²)
  • 立方阶O(n³)
  • K次方阶O(n^k)
  • 指数阶(2^n)
    上面从上至下依次的时间复杂度越来越大,执行的效率越来越低。

下面选取一些较为常用的来讲解一下(没有严格按照顺序):

1. 常数阶O(1)
无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1),如:

int i = 1;
int j = 2;
++i;
j++;
int m = i + j;

上述代码在执行的时候,它消耗的时候并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用O(1)来表示它的时间复杂度。

2. 线性阶O(n)
这个在最开始的代码示例中就讲解过了,如:

for(i=1; i<=n; ++i)
{
   j = i;
   j++;
}

这段代码,for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用O(n)来表示它的时间复杂度。

3. 对数阶O(logN)
还是先来看代码:

int i = 1;
while(i<n)
{
    i = i * 2;
}

从上面代码可以看到,在while循环里面,每次都将 i 乘以 2,乘完之后,i 距离 n 就越来越近了。我们试着求解一下,假设循环x次之后,i 就大于 2 了,此时这个循环就退出了,也就是说 2 的 x 次方等于 n,那么 x = log2^n
也就是说当循环 log2^n 次以后,这个代码就结束了。因此这个代码的时间复杂度为:O(logn)

4. 线性对数阶O(nlogN)
线性对数阶O(nlogN) 其实非常容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)。

就拿上面的代码加一点修改来举例:

for(m=1; m<n; m++)
{
    i = 1;
    while(i<n)
    {
        i = i * 2;
    }
}

5. 平方阶O(n²)
平方阶O(n²) 就更容易理解了,如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²) 了。
举例:

for(x=1; i<=n; x++)
{
   for(i=1; i<=n; i++)
    {
       j = i;
       j++;
    }
}

这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(n*n),即 O(n²)
如果将其中一层循环的n改成m,即:

for(x=1; i<=m; x++)
{
   for(i=1; i<=n; i++)
    {
       j = i;
       j++;
    }
}

那它的时间复杂度就变成了 O(m*n)

立方阶O(n³)、K次方阶O(n^k) 参考上面的O(n²) 去理解就好了,O(n³)相当于三层n循环,其它的类似。

除此之外,其实还有 平均时间复杂度、均摊时间复杂度、最坏时间复杂度、最好时间复杂度 的分析方法,有点复杂,这里就不展开了。

2.2 空间复杂度

二、空间复杂度
既然时间复杂度不是用来计算程序具体耗时的,那么我也应该明白,空间复杂度也不是用来计算程序实际占用的空间的。

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的是一个趋势,我们用 S(n) 来定义。

空间复杂度比较常用的有:O(1)、O(n)、O(n²),我们下面来看看:

1. 空间复杂度 O(1)
如果算法执行所需要的临时空间不随着某个变量n的大小而变化,即此算法空间复杂度为一个常量,可表示为 O(1)
举例:

int i = 1;
int j = 2;
++i;
j++;
int m = i + j;

代码中的 i、j、m 所分配的空间都不随着处理数据量变化,因此它的空间复杂度 S(n) = O(1)

空间复杂度 O(n)
我们先看一个代码:

int[] m = new int[n]
for(i=1; i<=n; ++i)
{
   j = i;
   j++;
}

这段代码中,第一行new了一个数组出来,这个数据占用的大小为n,这段代码的2-6行,虽然有循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,即 S(n) = O(n)

参考链接:https://zhuanlan.zhihu.com/p/50479555

<think>好的,我现在需要向用户介绍均摊时间复杂度。首先,我得回忆一下这个概念的基本定义,确保自己理解正确。均摊时间复杂度是算法分析中的一种方法,用于评估一系列操作的平均性能,特别是在某些操作可能比较耗时,但其他操作比较快速的情况下。它平均时间复杂度不同,平均时间考虑的是所有可能输入的情况,而均摊时间则是考虑最坏情况下每个操作的平均代价。 接下来,我应该考虑如何用通俗易懂的语言解释这个概念,同时结合例子帮助用户理解。比如动态数组(如ArrayList)的扩容机制就是一个经典例子。当数组空间不足时,扩容需要O(n)的时间,但这种情况并不频繁发生,因此可以将这次扩容的开销均摊到之前的多次插入操作中,使得每个插入操作的均摊时间复杂度仍然是O(1)。 然后,我需要确定这个解释的结构是否清晰。可能需要分步骤来说明,比如先介绍基本概念,再比较均摊时间平均时间的区别,接着举例说明,最后总结适用的场景优势。 还要注意用户可能的疑问点,比如为什么均摊时间在某些情况下更实用,或者如何计算均摊时间复杂度。这时候可能需要引入摊还分析的方法,比如聚合分析、记账方法或势能法,但作为简介,可能不需要深入这些方法的细节,但需要提到这些方法的存在。 同时,我需要确保例子足够具体,让用户能够直观理解。比如动态数组的插入操作,每次普通插入是O(1),当容量满时扩容到双倍大小,并将所有元素复制过去,耗时O(n)。假设每次插入操作都分摊一次扩容的成本,那么经过n次插入后,总时间是多少,均摊到每次就是O(1)。 另外,可能还需要提到其他应用场景,比如栈的多次操作中,某些操作可能涉及大量元素的移动,但均摊下来每个操作的时间复杂度较低。 最后,总结均摊时间复杂度的意义,它帮助我们在最坏情况下更准确地评估算法的整体效率,避免因为偶尔的高耗时操作而高估时间复杂度,从而更合理地设计算数据结构。 现在需要检查是否有错误的地方,比如动态数组的例子是否正确,均摊计算是否准确。例如,当数组每次扩容为双倍时,插入n个元素的总时间确实是O(n),所以均摊每个操作是O(1)。这个结论是正确的,因为每次扩容的时间是2^k(假设初始容量为1),总复制次数是1 + 2 + 4 + ... + 2^k ≈ 2n,所以总时间是O(n),均摊到n次插入就是O(1)每次。 另外,需要确保使用正确的LaTeX格式,行内公式用$...$,独立公式用$$...$$并单独成段。例如,在提到时间复杂度时,写成$O(1)$或$O(n)$,而动态数组的例子可能需要展示总时间计算,比如用独立公式表达总时间T(n) = O(n),均摊时间T_amortized = O(1)。 最后,语言要口语化,但结构清晰,分点说明,帮助用户逐步理解概念,同时保持回答的真实可靠。</think>均摊时间复杂度是算法分析中评估操作平均性能的重要方法。它平均时间复杂度的区别在于:**均摊分析关注最坏情况下每个操作的平均开销**,而非所有可能输入的平均值。以下通过结构化的解释帮助你逐步理解: --- ### 一、核心概念 1. **定义** 均摊时间复杂度用于分析**一系列操作**中,将某些高成本操作的开销均摊到所有操作上,得到每个操作的平均成本。 2. **关键点** - 适用于**操作之间存在关联性**的场景(如数据结构的状态变化)。 - 保证**最坏情况下**的整体性能,而非单次操作的最坏情况。 --- ### 二、平均时间复杂度的区别 | **均摊时间复杂度** | **平均时间复杂度** | |---------------------------|-----------------------------| | 分析最坏情况下每个操作的平均开销 | 分析所有可能输入下操作的平均开销 | | 依赖操作间的关联性(如动态数组扩容) | 假设输入服从某种概率分布 | --- ### 三、经典示例:动态数组(如C++的`vector`) 1. **操作背景** - 插入元素时,若数组已满,需扩容至2倍大小并复制原有元素。 - 单次插入的普通时间复杂度:$O(1)$;扩容时时间复杂度:$O(n)$。 2. **均摊分析** - **假设插入$n$个元素**,总时间包括: - $n-1$次普通插入:$O(1)$每次 → 总时间$O(n)$。 - 1次扩容操作:$O(n)$。 - **总时间**:$T(n) = O(n) + O(n) = O(n)$。 - **均摊到每次操作**:$\frac{O(n)}{n} = O(1)$。 $$T_{\text{amortized}} = O(1)$$ --- ### 四、分析方法 1. **聚合分析** 直接计算$n$次操作的总时间,再均摊到每次(如上述动态数组示例)。 2. **记账方法** 为每次操作分配“虚拟费用”,高成本操作由之前的盈余支付。 *例如:动态数组插入时,支付1元用于插入,1元存为扩容“备用金”*。 3. **势能法** 定义“势能函数”描述数据结构的状态,用势能变化反映操作成本。 *公式*: $$\text{均摊成本} = \text{实际成本} + \Delta\Phi$$ (其中$\Delta\Phi$为势能变化) --- ### 五、适用场景 1. **数据结构的动态操作** - 栈的多次`push``pop`(如支持多`pop`的惰性删除)。 - 哈希表的扩容缩容。 2. **算法设计** - 斐波那契堆的插入、合并等操作。 - 并查集的路径压缩优化。 --- ### 六、总结 均摊时间复杂度通过**将偶尔的高开销操作平摊到所有操作**,更真实地反映算法在长期运行中的性能。它特别适合分析需要动态调整资源的数据结构,帮助我们在最坏情况下仍能保证整体高效性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值