时间复杂度、空间复杂度概念,计算方法解析

前言❤️❤️

hello hello💕,这里是洋不写bug~😄,欢迎大家点赞👍👍,关注😍😍,收藏🌹🌹
欢迎大家来到数据结构专栏🌹🌹🌹,这篇博客是数据结构专栏的第一篇博客,数据结构是和算法深度绑定的,很多大学这门课叫做《数据结构与算法》,在学习数据结构时,也会随着学习一些高效的算法


这篇博客会详细解析算法评判最基础的两个标准,时间复杂度和空间复杂度🐵
这个专栏的数据结构是代码都是用Java来写的,JavaSE专栏现在已经全部更新完成,铁汁们复习基础知识时非常推荐使用,可以试一下💪💪💪
在这里插入图片描述
🎇个人主页:洋不写bug的博客
🎇所属专栏:数据结构专栏
🎇复习Java基础知识:Java学习之旅,从入门到进阶
🎇铁汁们对于数据结构基础的各种核心知识(不太常用的也有😆),都可以在上面的数据结构专栏学习,专栏正在持续更新中🐵🐵,有问题可以写在评论区或者私信我哦~

1,时间复杂度简介

如果想衡量一段代码的效率如何,那统计这个代码执行完成需要花费多少毫秒肯定是不合适的,因为计算机的配置是有差异的
时间复杂度,就是用来“消除计算机的硬件配置差异”,衡量一段代码执行效率的标准

写一些复杂的代码时,经常涉及到“重复性操作”,时间复杂度就是以重复性操作作为基准单位,衡量重复性操作的执行次数,就可以作为判断代码执行效率的指标

下面这段代码,有for循环和while循环,count++就是一个“重复性操作”,就是一个基准单位

void func1(int N){
    int count = 0;
    for (int i = 0; i < N ; i++) {
        for (int j = 0; j < N ; j++) {
            count++;
        }
    }
    for (int k = 0; k < 2 * N ; k++) {
        count++;
    }
    int M = 10;
    while ((M--) > 0) {
        count++;
    }
    System.out.println(count);
}


count++的执行次数是N ^ 2 + 2N + 10,那这段代码的时间复杂度怎么算呢

当N = 100时,这时候2N + 10相比于N的平方就很小了,在计算时间复杂度时,就会把低次项给直接忽略掉
因此,这段代码的时间复杂度就是O(N^2),时间复杂度并不关心精确的执行次数,只需要粗略知道随着N的增加,执行次数的增长趋势即可



如下所示,count++的执行次数改为4N^2 + 2N + 10,代码的时间复杂度还是O(N ^2),计算时间复杂度时,最高项的系数是不考虑的,因为时间复杂度只需要粗略知道随着N的增加,执行次数的增长趋势即可

void func1(int N){
    int count = 0;
    for (int i = 0; i < 2 * N ; i++) {
        for (int j = 0; j < 2 * N ; j++) {
            count++;
        }
    }
    for (int k = 0; k < 2 * N ; k++) {
        count++;
    }
    int M = 10;
    while ((M--) > 0) {
        count++;
    }
    System.out.println(count);
}



如果执行M + N次,M和N都是未知变量,如下
那么这段代码的时间复杂度就是O(M + N)

void func3(int N, int M) {
    int count = 0;
    for (int k = 0; k < M; k++) {
        count++;
    }
    for (int k = 0; k < N; k++) {
        count++;
    }
    System.out.println(count);
}



代码执行的次数是固定的常数,跟N没关系,如下,count++固定执行100次,可能初学铁汁会认为这里的时间复杂度就是O(100),其实不然
只要是执行次数确定的(也就是执行次数是常数),时间复杂度都是O(1)
因为随着N的增加,执行次数是不会增长的🐵

void func4(int N) {
    int count = 0;
    for (int k = 0; k < 100; k++) {
        count++;
    }
    System.out.println(count);
}

2,冒泡排序的时间复杂度

接着分析下冒泡排序函数的时间复杂度,代码如下:

void bubbleSort(int[] array) {
    for (int end = array.length; end > 0; end--) {
        boolean sorted = true;
        for (int i = 1; i < end; i++) {
            if (array[i - 1] > array[i]) {
                Swap(array, i - 1, i);
                sorted = false;
            }
        }
        if (sorted == true) {
            break;
        }
    }
}

时间复杂度就是看内层循环的次数,稍微观察下,就会发现内层循环的次数是一个等差数列,拿数组的长度当N来看,第一次循环N - 1次,第二次循环N - 2次
以此类推,内层循环总的执行次数就是N - 1 + N - 2 + N - 3 +… + 1
这个根据高中学过的等差数列的和的计算公式,总次数就是0.5N * (N - 1)
那时间复杂度就是O(N ^ 2)



但是,如果数组比较有序的话,那可能中途就通过break跳出循环了,前面计算的是最坏情况下的时间复杂度
如果最好情况下,那就是数组本来就有序,内层循环执行一遍就跳出了,那时间复杂度就是O(N)

这个冒泡排序函数,最坏情况下的时间复杂度是O(N ^ 2),最好情况下的时间复杂度是O(N),其实平均时间复杂度也是O(N ^ 2),这里的平均也就是数组随机打乱来排序,计算出来的(有兴趣的铁汁可以搜下是怎么计算的)

我们是不需要关注考虑平均时间内复杂度是如何计算的,在绝大多数情况下,平均时间复杂度是和最坏时间复杂度是一样的,让计算时间复杂度的时候,直接按照最坏结果计算即可
就好比之前高考填志愿,是考前填的,需要估分,那肯定是要以最近模考最差的一次为参考,会比较稳,以最好的一次做参考肯定是不合适的

3,二分查找的时间复杂度

接着分析下二分查找的时间复杂度,代码如下:

int binarySearch(int[] array, int value) {
    int begin = 0;
    int end = array.length - 1;
    while (begin <= end) {
        int mid = (end + begin) / 2;
        if (array[mid] < value)
            begin = mid + 1;
        else if (array[mid] > value)
            end = mid - 1;
        else
            return mid;
    }
    return -1;
}

二分查找每次while循环中的if执行一次,区间都会缩小一半,也就是每次都能排除一半的元素
计算时间复杂度直接按照最坏情况算,也就是要查找的元素不在数组中,推出2的比较次数次方等于总元素个数

比较次数也就是log2 N,因此,时间复杂度就是O(log2 N)
在计算机中,涉及对数的时间复杂度,大部分情况下都是以2为底的,当以2为底的时候,可以直接省略2不写,写成O(logN)


如果某个算法的时间复杂度是这种对数类型的,就可以认为是一个非常高效的算法
以ln为例(底数是2.7),数据规模是100万时,最多也只需要比较14次,如下图
在这里插入图片描述



而且随着数据规模的大幅增长,执行次数的增长是很少的
就算把数据规模从100万增加到为1亿,会发现执行次数也只是增加了几次而已,如下图
在这里插入图片描述

4,递归下的时间复杂度

有的代码,不一定把循环明面上写出来,而是通过递归的形式
就比如下面这个计算数字阶乘的代码,就是递归调用,这个也算是一种隐形的循环,时间复杂度就是O(N)

long factorial(int N) {
    return N < 2 ? N : factorial(N-1) * N;
}



再比如计算斐波那契数,下面这样写是一个非常低效的写法,传入的数据稍微大一点,计算机就要运行一段时间才能出结果

int fibonacci(int N) {
    return N < 2 ? N : fibonacci(N-1)+fibonacci(N-2);
}

这个代码就是每次递归调用都再扩展成两个递归,新扩展出的递归每个再扩展成两个递归
执行次数是2的N次方,时间复杂度就是O(N ^ 2),这个是非常非常高的,已经远远高于O(N ^ 2)了,甚至是O(N ^ 3)了
在这里插入图片描述



计算第50个斐波那契数,要执行的次数如下图,已经是多少亿亿次了😅
在这里插入图片描述

5,空间复杂度

空间复杂度也是通过O来表示的,主要描述的是问题规模N和消耗的空间资源的变化趋势

空间复杂度考虑的是计算机上的内存空间(一般现在买计算机内存空间就是16G,32G这样,内存空间是关机(断电)后就不会保存的),而不是硬盘空间(例如512G,1T,2T这种,关机后数据还能保存)
创建的变量都是在内存上创建的(堆空间和栈空间都是在内存上的)



注:空间复杂度看的是临时占用的空间随着问题规模变化的增长趋势,是不考虑问题本身的存储的

例如下面这段代码,参数传入了长度为N的数组,那数组占用的存储空间,是不计入时间复杂度中的
看的是for循环中的比较逻辑占用的内存空间,这里随着数组规模的增大,就还是那几个变量,并不会占用的内存空间更多,因此下面代码的空间复杂度就是O(1)

void bubbleSort(int[] array) {
    for (int end = array.length; end > 0; end--) {
        boolean sorted = true;
        for (int i = 1; i < end; i++) {
            if (array[i - 1] > array[i]) {
                swap(array, i - 1, i);
                sorted = false;
            }
        }
        if (sorted == true) {
            break;
        }
    }
}



下面代码是计算斐波那契数效率较高的写法,内存空间主要的消耗就是创建了一个数组,随着问题规模变大,消耗的内存空间就会增加
数组的长度为N + 1,时间复杂度就是O(N)

int[] fibonacci(int n) {
    long[] fibArray = new long[n + 1];
    fibArray[0] = 0;
    fibArray[1] = 1;
    for (int i = 2; i <= n ; i++) {
        fibArray[i] = fibArray[i - 1] + fibArray [i - 2];
    }
    return fibArray;
}



下面这个递归计算数字阶乘的代码,每次递归都会创建一个栈帧,递归的深度是N,也就有N份栈帧,空间复杂度也就是O(N)

long factorial(int N) {
    return N < 2 ? N : factorial(N-1) * N;
}



计算斐波那契数的低效写法如下,可能有的铁汁会认为空间复杂度是O(2 ^ N),其实并不是这样
递归时,空间是可以复用的,因此一般只关心最深的递归的递归深度

int fibonacci(int N) {
    return N < 2 ? N : fibonacci(N-1)+fibonacci(N-2);
}


如下图,这里最深的递归就是最左边f97线,递归深度是N
因此,低效写法的空间复杂度也是O(N)
在这里插入图片描述

结语

时间复杂度和空间复杂度在数据结构的学习中,会被经常提到
在算法题目中,更多提到的还是时间复杂度(空间复杂度一般关注不多,因为现在计算机的内存空间比较大,不需要在写代码时去刻意节省内存),这关系着代码的执行效率,在蓝桥杯中如果代码时间复杂度过高,就会出现部分用例超时的情况,没办法得到全部的分数

以上就是今天的所有内容啦~完结撒花~🥳🎉🎉
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值