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

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

为什么需要复杂度分析?


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


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


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.指数


结尾:


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



相关文章
|
1月前
|
机器学习/深度学习 存储 缓存
数据结构从入门到精通——算法的时间复杂度和空间复杂度
算法的时间复杂度和空间复杂度是评估算法性能的两个重要指标。时间复杂度主要关注算法执行过程中所需的时间随输入规模的变化情况,而空间复杂度则关注算法执行过程中所需的最大存储空间或内存空间。
71 0
|
3月前
|
机器学习/深度学习 存储 算法
数据结构 | 算法的时间复杂度和空间复杂度【详解】(二)
数据结构 | 算法的时间复杂度和空间复杂度【详解】(二)
|
3月前
|
机器学习/深度学习 存储 算法
数据结构 | 算法的时间复杂度和空间复杂度【详解】(一)
什么是数据结构? 数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。
|
3月前
|
机器学习/深度学习 算法
算法的时间复杂度
算法的时间复杂度
39 0
|
4月前
|
机器学习/深度学习 存储 算法
【算法基础】常数操作 时间复杂度 选择排序 冒泡排序 插入排序 位运算
【算法基础】常数操作 时间复杂度 选择排序 冒泡排序 插入排序 位运算
|
5月前
|
机器学习/深度学习 算法
数据结构与算法之时间复杂度和空间复杂度(C语言版)
数据结构与算法之时间复杂度和空间复杂度(C语言版)
数据结构与算法之时间复杂度和空间复杂度(C语言版)
|
4月前
|
算法 Python
Python 数据结构和算法:解释什么是 Big O 表示法?举例说明几种常见的时间复杂度。
Python 数据结构和算法:解释什么是 Big O 表示法?举例说明几种常见的时间复杂度。
|
17天前
|
算法 数据处理 C语言
【数据结构与算法】快速排序(详解:快排的Hoare原版,挖坑法和双指针法|避免快排最坏时间复杂度的两种解决方案|小区间优化|非递归的快排)
【数据结构与算法】快速排序(详解:快排的Hoare原版,挖坑法和双指针法|避免快排最坏时间复杂度的两种解决方案|小区间优化|非递归的快排)
|
1月前
|
算法
TOP-K问题和向上调整算法和向下调整算法的时间复杂度问题的分析
TOP-K问题和向上调整算法和向下调整算法的时间复杂度问题的分析
19 1
|
1月前
|
机器学习/深度学习 存储 缓存
数据结构--算法的时间复杂度和空间复杂度
数据结构--算法的时间复杂度和空间复杂度