算法| 再也不怕被问时间复杂度了 (上)

简介: 算法| 再也不怕被问时间复杂度了 (上)

为什么需要复杂度分析?


可能对于第一次接触这个概念的同学来说,第一个问题就是,我为什么要需要这个东西呢?我把代码跑一次,统计一下消耗时间不就完了吗?这样不是更准确吗?如果你现在也是这么想的,那么请耐心的往下看


首先来说,直接统计代码的运行时间绝对是没问题的,相应的空间消耗程度也可以通过监控内存来获取。这种统计方法甚至还有一个专门的名字叫"事后统计法"。但是这种方法存在两个很大的弊端


1.受环境影响严重,如机器配置

一个相同的算法,在不同配置的机器上运行时间肯定是不同的。这一点不用多说


2.受数据量影响严重

举个极端的例子如下:

public void demo01(List<Integer> list) {
    for (Integer integer : list) {
        System.out.println(integer);
    }
}
public void demo02(List<Integer> list) {
    int count = 0;
    for (Integer integer : list) {
        count++;
        System.out.println(integer);
        if(count>100000){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

我们可以看到demo01,demo02两个算法(姑且叫它们算法,只是为了说明这种情况,实际情况下没人会这么写)


这两个算法的目的都是为了遍历集合中的所有元素,当元素的数量不高于100000时,这两个算法运行的效率肯定


是差不多的,但是一旦数据规模超出100000时,这两个算法的效率就没有可比性了

综上所述,为了屏蔽外在环境的影响,只是单纯的评判算法的优劣,我们的时间空间复杂度应运而生


如何对一个算法进行复杂度分析?


在知道了为什么会有复杂度的出现后,我们就需要学习下怎么对一个算法进行复杂度的分析。


先来看一个简单的例子:

public int sum(int n) {
    int sum = 0;
    for (int i = 0; i < n; i++) {
        sum += n;
    }
    return sum;
}

如果对时间复杂度有所了解的话,一眼就能看出结果为O(n)。如果你对复杂度不了解的话,不用急,跟着我的思路来~


我们可以假设每行代码所有消耗的时间都为一个时间单元—u,那么对于这个程序其复杂度不难得出为:(1+n+n+1)*u=(2n+2)*u,第一行程序运行了1次,第二行跟第三行都运行了n次,return语句执行一次,所以结果为(2n+2)*u=n*(2u)+2u(其中u为常数),那么其时间复杂度就为O(n)


可能有的同学会有疑问:


1.为什么你可以假设每行代码的运行时间都一样呢?很明显for (int i = 0; i < n; i++)比int sum = 0消耗的时间长呀!这样的假设也能成立吗?

2.为什么计算结果为n*(2u)+2u(其中u为常数),其时间复杂度就为O(n)呢?


在这里我们先解释第二个问题,什么是大O?


粗略的讲,大O代表的算法的复杂度变化的趋势,在上面的例子中,我们计算出结果为n*(2u)+2u(其中u为常数),这是一个一次函数,我们可以称其变化是线性的,当n趋向无穷大时,我们可以忽略常数项,记为O(n)。同样,当我们计算出结果为一个二次函数时,这个时间我们可以忽略其低阶项跟常数项,记为:

image.png

这个在之后会再介绍,这里只是提一下


当我们了解到了 什么是大O后,我相信第一个问题也很好解释了


我们可以假设第一行代码消耗的时间为x,那么第二行代码消耗的时间可以表示为k*x(其中k大于0,k为常数),第三行的代码同样可以表示为z*x(z>0,k为常数),第四行的同样表示为v*x(v>0,k为常数),那么总消耗时间可以表示为

image.png

很明显,这也是一个关于n的1次函数,所以我们也可以记其时间复杂度为O(n)


通过上面的例子,我们已经对时间复杂度及其计算有了一个粗略的了解,下面我们继续通过几个例子来深入了解时间复杂度,并学习几种常见的时间复杂度


几种简单的时间复杂度:


1.常数阶:

O ( 1 ) O(1)

image.png

首先,我们要明确一点,O(1)并不代表代码只执行一行,它的意思是说,该算法的执行时间跟外界因素(数据大小,调用次数)无关,举例如下:

 public int sum(int n) {
        int sum = 0;
        for (int i = 0; i < 3; i++) {
            sum += n;
        }
        return sum;
    }

在上面的例子中,不如n多大,该方法被调用多少次,每次调用的复杂度永远是1+2*3+1=8,是一个常数,这种情况下,我们记其时间复杂度为O(1)

1.线性:

image.png

之前的例子就已经分析过了,这里不再赘述

2.二次:

image.png

我们看一个例子:

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

我们按照我们的方法计算其复杂度:

第一行代码执行一次:1

第二行代码执行n次:n

第三行代码执行n次:n

第四行代码执行:n乘n次(外圈每执行一次,内圈执行n次,所以为n乘n次)

第五行代码也是:n乘n次

return语句执行1次:1

所以时间复杂度为:

image.png

根据我们之前说的,忽略其低阶项及常数项,可记为:

image.png

到这里我们可以发现,因为我们往往要忽略低阶项跟常数项,只保留最高阶,相当于我们只需统计执行次数最多的代码,在上面的例子中我们很容易发现,执行最多次数的代码就是内嵌的循环,为二次函数,所以很快得出结论,时间复杂度为O(n^2)


1.三次:

在学习了线性及二次时间复杂度了之后,三次也很容易理解了,最简单的例子就是循环嵌套了三层,这里不再赘述


2.指数:(增长速度非常恐怖),这里给出一个简单的例子,读者自行思考即可,不恰当的递归也会导致指数级时间复杂度

    public int sum(int n) {
        int sum = 0;
        int count = 2;
        for (int i = 0; i <n ; i++) {
            count = count*2;
        }
        for (int i = 0; i < count; i++) {
            sum += n;
            for (int j = 0; j < n; j++) {
                sum += j;
            }
        }
        return sum;
    }

稍微复杂点的时间复杂度:


image.png

上面的时间复杂度都涉及到了对数,所以为了了解上面几种时间复杂度,我们首先要知道什么情况下会出现对数的时间复杂度,示例如下:

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

根据我们之前的分析,我们主要关系i=i*2这行代码的运行次数,假设其运行次数为x,则有如下公式:

image.png

则:

image.png

所以此示例中时间复杂度可记为:

image.png

我们知道,任意对数的底是可以替换的,如:

image.png

所以,我们统一将这种时间复杂度记为:

image.png

到现在,我们已经知道了对数级的时间复杂度的由来,那么形如:

image.png

这种形式的时间复杂度又是怎么来的呢?

我们要记住一个公式:

嵌套代码的复杂度等于嵌套内外代码复杂度的乘积,什么意思呢?我们举例说明:

public void test(int n){
    for (int i = 0; i < n; i++) {
        int x = 1;
        while (x<n){
            x= x*2;
        }
    }
}

可以看到,我们将一个时间复杂度为logn的算法循环执行了n次,那么这个算法的复杂度就为:

image.png

同样,我们也能知道:

image.png

的由来了,我也不再赘述了

在文章的最后,我想应该将常用的算法复杂度按增长速度从底到高整理了一下,让大家对这些复杂度能有个评判标准,由底到高分别为:

1.长度,O(n)

2.对数,O(logn)

3.对数平方数

image.png

4.线性:O(n)

5.image.png

6.二次

7.三次

8.指数


结尾:


在这篇文章中,主要跟大家一起学习了一些基础的时间复杂度的分析,但是知道这些是远远不够的,复杂度的分析是我们学习算法的基石,预计还会有一篇文章,主要对最好,最坏,平均,均摊时间复杂度进行分析,另外会跟大家一起分析一个算法复杂度的题目,希望大家能跟我一起学到东西!



相关文章
|
2月前
|
机器学习/深度学习 缓存 算法
Python算法设计中的时间复杂度与空间复杂度,你真的理解对了吗?
【10月更文挑战第4天】在Python编程中,算法的设计与优化至关重要,尤其在数据处理、科学计算及机器学习领域。本文探讨了评估算法性能的核心指标——时间复杂度和空间复杂度。通过详细解释两者的概念,并提供快速排序和字符串反转的示例代码,帮助读者深入理解这些概念。同时,文章还讨论了如何在实际应用中平衡时间和空间复杂度,以实现最优性能。
68 6
|
4月前
|
机器学习/深度学习 算法 程序员
读《趣学算法》:重开算法之门,时间复杂度与空间复杂度
本文是作者阅读《趣学算法》后的笔记,介绍了算法复杂度的基本概念,包括时间复杂度和空间复杂度的不同阶表示,并通过具体例子展示了如何计算和理解算法的效率。
68 2
读《趣学算法》:重开算法之门,时间复杂度与空间复杂度
|
2月前
|
搜索推荐 算法
插入排序算法的平均时间复杂度解析
【10月更文挑战第12天】 插入排序是一种简单直观的排序算法,通过不断将未排序元素插入到已排序部分的合适位置来完成排序。其平均时间复杂度为$O(n^2)$,适用于小规模或部分有序的数据。尽管效率不高,但在特定场景下仍具优势。
|
5月前
|
机器学习/深度学习 存储 算法
颠覆认知!Python算法设计中的时间复杂度与空间复杂度,你真的理解对了吗?
【7月更文挑战第22天】在Python算法设计中,时间与空间复杂度是评估算法效能的核心。时间复杂度不仅限于大O表示法,还涵盖平均与最坏情况分析。空间复杂度虽关注额外存储,但也反映内存效率。平衡二者需视场景而定,如利用原地算法减少内存消耗,或牺牲空间加速执行。算法优化技巧,如分治与动态规划,助你在资源与速度间找寻最优解,从而高效应对大数据挑战。
54 3
|
2月前
|
机器学习/深度学习 存储 缓存
数据结构与算法学习十:排序算法介绍、时间频度、时间复杂度、常用时间复杂度介绍
文章主要介绍了排序算法的分类、时间复杂度的概念和计算方法,以及常见的时间复杂度级别,并简单提及了空间复杂度。
33 1
数据结构与算法学习十:排序算法介绍、时间频度、时间复杂度、常用时间复杂度介绍
|
2月前
|
存储 算法
算法的时间复杂度和空间复杂度
本文详细讨论了算法的时间复杂度和空间复杂度,包括它们的概念、计算方法和常见复杂度的对比,并通过多个实例解释了如何计算算法的时间和空间复杂度。
93 0
算法的时间复杂度和空间复杂度
|
3月前
|
算法 Python
震惊!Python 算法设计背后,时间复杂度与空间复杂度的惊天秘密大起底!
在 Python 算法设计中,理解并巧妙运用时间复杂度和空间复杂度的知识,是实现高效、优雅代码的必经之路。通过不断地实践和优化,我们能够在这两个因素之间找到最佳的平衡点,创造出性能卓越的程序。
40 4
|
2月前
|
算法 C语言
深入理解算法效率:时间复杂度与空间复杂度
深入理解算法效率:时间复杂度与空间复杂度
|
4月前
|
机器学习/深度学习 存储 算法
算法时间复杂度分析
这篇文章讲解了如何分析算法的时间复杂度,包括关注循环执行次数最多的代码段、总复杂度的确定、嵌套代码复杂度的计算方法,并提供了大O阶的推导步骤和常见时间复杂度的列表,同时还介绍了空间复杂度的概念及其重要性。
|
4月前
|
搜索推荐
九大排序算法时间复杂度、空间复杂度、稳定性
九大排序算法的时间复杂度、空间复杂度和稳定性,提供了对各种排序方法效率和特性的比较分析。
162 1