算法中的时间复杂度和空间复杂度计算方式

您所在的位置:网站首页 雪下的那么大 算法中的时间复杂度和空间复杂度计算方式

算法中的时间复杂度和空间复杂度计算方式

2023-03-02 11:53| 来源: 网络整理| 查看: 265

1 前言

作为一名“程序猿”,大家应该都听过这么一句话:程序=数据结构+算法。

这句话是由瑞士计算机科学家尼古拉斯·沃斯(Niklaus Wirth)在 1984 年获得图灵奖时说的一句话,这位大佬还以这句话为名出了一本书《Algorithms + Data Structures=Programs》,从此这句话就成为了大家耳熟能详的一句名言。

随着时间的推移,不管这句话是不是非常准确,但至少能说明数据结构与算法对程序来说是非常核心的基础,如果我们想要写出更多优秀优雅的代码,那么数据结构与算法是必须要掌握好的。

2 为什么要学习算法

很多人可能觉得,我不会算法,代码一样写得很"溜",算法这东西似乎用处不大。现在互联网的发达,我们想要什么几乎都可以在网上找到现成的,各种框架功能十分强大,似乎看起来确实不用算法也可以写出“好代码”。然而假如我们不懂算法,比如项目中用到了排序,我们如何评估代码的执行效率?再比如最常用的 ArrayList 和 LinkedList,我们该如何选择,又比如说我们需要去集合中找某一个数,又该如何写出性能优秀的代码呢?

同样的代码,如何判断谁的代码是优秀的代码?可读性,可扩展性,健壮性可能都可以用来判定,然而这些东西我觉得并不能直接体现出你代码的优秀,因为对用户而言,访问你的代码响应速度快那就是优秀的代码,相反,动辄响应几秒甚至更长时间的接口,恐怕就算你可读性再好,再健壮也称不上是好代码。

所以说一段代码是否优秀,最直接的判断标准就是性能,而如果要写出高性能的代码,那么就必须要了解算法,而且抛开这个因素,但凡不想一辈子都写 CRUD 代码的,也需要去了解算法,我们使用的很多框架和中间件底层都有数据结构和算法的身影,学好算法对我们源码阅读时理解其设计思想也是大有裨益的。

要说功利性的目的,那就是面试,目前很多大厂的面试,算法基本必面,所以想进大厂的话,咱们也得好好学学算法。

3 算法难学吗

提到算法,很多人的第一反应就是太难学了,学不会,或者说经常是看完就忘了,但是其实对于我们一个普通的开发者而言,因为并不需要我们去发明算法,我们需要的仅仅只是去灵活的运用算法,所以并不需要非常扎实的数据基础,当然基本的数学常识还是要有的。

如果说需要去发明设计一款算法,那就要去推导去证明算法的可行性,这种是需要具有非常扎实的数学基础的,一般人确实无法做到,然而我们普通程序员口中提到算法无非是二分查找法,哈希算法等,高级一点的就还有回溯,贪心,动态规划等等,这些所谓的算法都是已经有现成的公式了,我们要做的无非就是理解它,然后灵活的运用它。这就和我们以前学习数学公式一样,给你一个公式,然后你去做题,做题的过程其实就是去灵活的运用这个公式。

算法也是同理,都是有特定方法和特定思路的,我们也并不需要去推导证明这种方式为什么可行,所以学习算法没有其他诀窍,就是先理解思路,然后多练,等熟练了,自然就可以灵活运用了,也不会说学了立刻就忘了。学完就忘无非两个原因,一是没理解,而是没有练习巩固。

4 复杂度分析

数据结构与算法经常是放在一起讲,这两者是没办法独立的,因为算法是为了达到某种目的的一种实现方式,而数据结构是一种载体,也就是说算法必须依赖数据结构这种载体,否则就是空谈。换句话说:数据结构是为算法服务的,而算法又需要作用在特定的数据结构之上。

一个算法到底好不好,我们如何去评价?前面我们提到了,你的代码好不好,最直观的就是看响应速度,算法也一样,同样实现一个目的(比如说排序),谁的算法速度快,我们就可以认为谁的算法更优,如果说两种算法实现的速度差不多,那么我们还可以去评价算法所占用的空间,谁占用的空间少,那么就可以认为谁的算法更优,这就是算法的基础:时间复杂度和空间复杂度。

学习算法之前,我们必须要学会如何分析时间复杂度和空间复杂度(也就是“快”和“省”),否则自己写出来的算法自己都不知道算法的效率。

5 时间复杂度大 O表示法

接触过算法的都知道,算法的时间复杂度是用大写的“O”来表示的,比如:O(1),O(n),O(logn),O(nlogn),O(n²) 等等。

时间复杂度的全称是渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系,上面的这种时间复杂度表示法并不能真正反应一个算法的执行时间,反应的只是一个趋势,所以我们在分析复杂度的时候要关注“变”,忽略“不变”。

变指的是变量,也就是一段代码的执行时间是随着变量的变化而变化的,而不变指的是常量,也就是不论我的变量如何改变,执行时间都不会改变。

接下来我们就实际的来分析下常用时间复杂度的例子来练习一下。

5.1 O(1) 常数阶

0(1) 复杂度算法也称之为常数阶算法。这里的 1 是用来代指常量,也就是说这个算法的效率是固定的,无论你的数据量如何变化,效率都一样,这种复杂度也是最优的一种算法。

public static void print(int n){ int a = 1; int b = 2; int c = 3; int sum = a + b + c; System.out.println(sum); } 上面的示例中不论有多少行代码,时间复杂度都是属于常数阶。换言之:只要代码不存在循环,递归等循环类调用,不论代码有多少行,其复杂度都是常数阶。

5.2 O(n) 线性阶

O(n) 复杂度算法也称之为线性阶。比如下面这个示例我们应该怎么分析复杂度呢?

public static void print1(int n){ int a = 0; for (int i=0;i int i=1; while (i i = i * 3; } 上面得到的 log3n 我们可以再做进一步的转换:log3n=log32 * log2n,而 log32(注意这几个地方的 3 是底数,在下面) 是一个常量,常量可以省略,所以也就得到了:O(log3n)=O(log2n)。同样的道理,不论底数是多少,其实最终都可以转化成和 O(log2n) 相等,正因为如此,为了方便,我们算法中通常就会省略底数,直接写作 O(logn)。

上面的数学公式大家如果忘了或者看不懂也没关系,只要记住不论对数的底数是多少,我们都算作 O(logn),而对于一个算法的复杂度是否是对数阶,还有一个简易的判断方法:当循环中下标以指定倍数形式衰减,那么这就是一个对数阶。

5.5 O(nlogn) 线性对数阶

如果理解了上面的对数阶,那么这种线性对数阶就非常好理解了,只需要在对数阶的算法中再嵌一层循环就是线性对数阶:

for (int j=1;j i = i * 2; } } 分析了前面这些最常用的时间复杂度,其实我们可以得到以下规律:

只要是常量级别,不论多大,效率都是一样的(如:常量阶复杂度例子)。 分析一段代码的时间复杂度,只需要分析执行次数最多的一段代码(如:所以例子中我们只分析了循环体中代码执行次数)。 嵌套代码的复杂度等于嵌套内外代码复杂度的乘积(如:分析线性对数阶复杂度例子)。

5.6 其他复杂度

除了上面常用的复杂度之外,另外还有指数阶,阶层阶,根号阶等,这些接触的相对会较少,我们就不特意做分析了,如果大家感兴趣的话,可以自己去了解下。

5.7 组合式复杂度分析

前面我们分析的都是只有一段代码比较复杂的情况下得到的复杂度结果,那么假如我一个算法中,有多段代码都比较复杂呢?这时候复杂度该如何分析?

取最大复杂度作为整个算法复杂度

我们先看下面这个例子:

public static void print1(int n){ for (int i=0;i System.out.println(j); }

for (int p=0;p System.out.println(j); } for (int k=0;k if (null == arr || arr.length == 0){ return -1; } for (int i=0;i return i; } } return -1; } 这个方法就是在一个指定数组中找到指定元素的下标,找不到就返回 -1,这个方法比较简单,应该比较好理解。

注意这个方法中的循环体,如果找到元素,那么就直接返回,这就会有一个现象,那就是我这个循环体到底会循环多少次是不确定的,可能是 1 次,也可能是 n(假设数组的长度) 次,所以假如我们要找的元素就在数组中的第一个位置,那么我循环一次就找到了,这个算法的复杂度就是 O(1),这就是最好情况时间复杂度。

6.2 最坏时间复杂度

理解了最好时间复杂度,那么最坏时间复杂度也很好理解了,那就是数组中不存在我要找到元素,或者说最后一个值才是我要找的元素,那么这样我就必须循环完整个数组,那么时间复杂度就是 O(n),这也就是最坏时间复杂度。

6.3 平均时间复杂度

最好时间复杂度和最坏时间复杂度毕竟只有特殊情况才会发生,概率还是相对较小,所以我们很容易就想到我们也需要有一个平均时间复杂度。

我们简单的来分析一下,为了便于分析,我们假设一个元素在数组和不在数组中的概率都为 1/2,然后假如在数组在,那么又假设元素出现在每个位置的概率也是一样的,也就是每个位置出现元素的概率为: 1/n。

所以最终得到的平均时间复杂度应该等于元素在数组中和元素不在数组中两种情况相加。

6.4 元素在数组中的复杂度

因为元素在数组中的概率为 1/2,然后在每个位置出现的概率也为 1/n。假如元素出现在第一个位置,复杂度为 1*(1/2n);假如元素出现在第二个位置,复杂度为 2 * (1/2n),最终得到当前场景下时间复杂度为:1*(1/2n) + 2 * (1/2n) + … + n*(1/2n)=(n+1)/4。

6.5 元素不在数组中的复杂度

前面已经假定了元素不在数组中的概率为 1/2,所以当前场景下的时间复杂度为:n * (1/2),因为元素不在数组中,那么这个算法必然会将整个循环执行完毕,也就循环是 n 次。

最后我们把两种情况的复杂度之和相加就得到了平均时间复杂度:(n+1)/4 + n/2 = (3n+1)/4,最终我们将常数类的系数忽略掉,就得到了平均时间复杂度为 O(n)。

6.7 均摊时间复杂度

均摊时间复杂度的算法需要使用摊还分析法,计算方式相对有点复杂,而且使用场景很有限,本文就不做过多介绍了。

7 空间复杂度

空间复杂度全称就是渐进空间复杂度,用来表示算法的存储空间与数据规模之间的增长关系。和时间复杂度一样,空间复杂度也是用大 O 进行表示。

其实学会了分析时间复杂度,那么空间复杂度的分析就简单了,主要就看我们在一个算法当中到底有没有使用到了额外的空间来进行存储数据,然后判断这个额外空间的大小会不会随着 n 的变化而变化,从而得到空间复杂度。

我们来看一个给数组赋值例子,假设这就是一个算法,我们可以来分析下这个算法的空间复杂度:

public static void init(int n){ int a = 0; int arr[] = new int[n]; for (int i=0;i



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3