算法与复杂度
本章主要介绍复杂度的相关概念,在此之前,先对数据结构和算法进行了解。
1. 数据结构
数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。
2. 算法
算法是一种对问题求解步骤的描述,来描述将输入的数据转换为想要输出的数据的一个过程。就比如我们想要解决某个问题,就需要将想法变成实际操作,每一步操作都有各自的功能作用,这就是算法。(算法是一个抽象的概念,也就是对一个步骤流程的一种描述)
2.1 算法的基本特性
算法主要有 5 个基本特性:输入、输出、有穷性、确定性、可行性。
- 输入:一个算法有 0 个或多个输入,比如有些算法目的是打印一串字符,不需要输入项;而有些算法就需要输入多个数据来计算一些值。
- 输出:一个算法至少有 1 个或多个输出,目的是反应一个步骤运行完的结果,如果遇见一个没有输出的步骤方法,那还能叫做算法吗😂。
- 有穷性:算法在执行完有限的步骤后自动结束(并且要在一定的时间内完成,总之不要太离谱,几个小时,几天的量,反正我是不能接受的😅),或者 死循环 的情况也是不允许的。
- 确定性:算法的每一个步骤都有确切的含义,不会出现歧异。
- 可行性:算法的每一步都可行,也要能够在有限次数内完成。
2.2 算法的评定
算法的设计并不单单只有一种,这就比如数学中想要得到 5 ,可以使用加减乘除和多种数字组合,不会有一个固定不变的算式。既然算法的设计是多样的,那么他们之间必然会有优劣,算法的评定就有四种:正确性、可读性、鲁棒性、时间复杂度、空间复杂度。
- 正确性:对于正确也许有些不好界定,但是应该要有算法的 5 个基本特性,能够给出问题的正确结果。
- 可读性:算法对于他人阅读、理解的难易程度。要是一个程序中出现四五层嵌套,或者四五级指针,这样可读性就差了很多。
- 鲁棒性:也叫做健壮性、容错性,反应的是对输入数据的不合法的情况的处理能力和反应能力,也就是当有不合法的数据输入时,算法应当做出相应处理。
- 时间复杂度:指算法所需要计算的工作量,反应的是时间效率的高低 。
- 空间复杂度:指算法所要消耗的内存空间,反应的是存储量的高低 。
3. 复杂度
算法有多种操作方式,而为了衡量算法的好坏,就需要从时间和空间上来观察,也称之为 时间复杂度 和 空间复杂度。
3.1 时间复杂度
时间复杂性,也叫做时间复杂度,算法的时间复杂度是一个函数,它定性描述该算法的运行时间。简单来分析就是将一个算法程序运行一遍所需要的时间。如果仅仅只是这样来计算,理论上来说是可以实现的,但是受外界因素影响太大,就比如计算机的硬件性能,软件和测试数据等方面,都会造成影响。
一个算法花费的时间与算法中语句的执行次数成正比,哪个算法中语句执行次数多,它花费时间就多。
关于一个问题规模 n 用函数
T(n)
来表示总的执行次数,算法的时间复杂度,记作T(n)=O(f(n))
。f (n) 表示某一算法的函数,O( f(n) )称作渐近时间复杂度,简称时间复杂度。
3.1.1 推导大O阶
- 用 1 取代运行时间中的所有加法常数。
- 在修改后的运行次数函数中,只保留最高项。
- 如果存在最高阶项,且系数不是 1 ,则去除系数。
3.1.2 常见的时间复杂度
执行次数 | 表示 | 名称 |
---|---|---|
24 | O(1) | 常数阶 |
6log2n+2 | O(logn) | 对数阶 |
2n+5 | O(n) | 线性阶 |
3nlog2n+2 | O(n logn) | 线性对数阶 |
5n2+1 | O(n2) | 平方阶 |
5n3+7 | O(n3) | 立方阶 |
2n | O(2n) | 指数阶 |
- | O(n !) | 阶乘阶 |
- | O(nn) | - |
以上是按照时间复杂度所需时间从小到大排列
下面就对部分阶进行说明:
- 常数阶:
算法1
int a = 2,b = 5;
int c = a + b;
printf("%d", c);
算法2
int x = 8;
x++;
int y = 9 * x;
y++;
printf("%d", y);
算法1执行次数为3,算法2执行次数为5,那他们时间复杂度分别是O(3)、O(5)吗,其实两个算法的时间复杂度都是O(1),因为上面提到过,用 1 取代运行时间中的所有加法常数
。
- 线性阶:
算法3
for (int i = 0; i < n; i++)
{
...... //循环中的代码,符合O(1)[常数阶]
}
算法3是线性阶最直观的一个例子,此循环循环了 n 次,所以时间复杂度为O(n)。
- 对数阶 :
算法4
int count = 1;
while (count < n)
{
count = count * 2;
}
算法4中,count * 2 执行完若干次后,count 会大于或等于 n ,随后跳出循环;其含义是循环一次就乘 2 ,假设循环了 x 次,得出 2x = n ,即 x = log2n;严谨一点来看,整个算法共执行了 log2n + 1 次,根据大O阶的推导,此算法的时间复杂度就为O(logn)。
在线性阶中,只有底数2可以省略,其他底数还是得标上。
- 平方阶 :
算法5
for (int i = 0; i < n;i++)
{
for (int j = 0; j < n; j++)
{
...... //循环中的代码,符合O(1)[常数阶]
}
}
算法5用了两层循环,其时间复杂度为O(n2)。
其他的时间复杂度也不难推导,就比如立方阶,三层循环就是这种情况。
计算时间复杂度,重点是思路,而不是代码,在见到可读性差的代码,计算时间复杂度会困难得多。
3.1.2 时间复杂度的三种情况
时间复杂度三种情况分别是:最好情况、平均情况、最坏情况。这三种情况几乎涵盖了生活中所有的事,最好情况是我们最希望遇到的,平均情况是最常见的,最坏情况也就是边界。
在算法中假设有一个随机数组,我们想要找出某一个值的位置,最好的情况第一个数字就是我们想要的值,平均情况就为数组长度的一半,最坏情况当然就在数组的末端(当然也有找不到的可能,这里暂不考虑)。
在对算法的分析中,一般都是指的最坏时间复杂度。
3.2 空间复杂度
空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度,记做S(n)=O(f(n))。
其中 n 表示问题的规模,f(n)表示关于 n 所占存储空间的函数。
一个算法的空间复杂度只考虑在运行过程中为局部变量分配的存储空间的大小。一个算法的空间复杂度递归空间复杂度计算,也是空间累加,但是不同的是空间可以重复利用。最常见的空间复杂度就有 O(1) 和 O(n),一般的递归运算空间复杂度就是O(n)。
😉
一个算法时间和空间复杂度有一定关联,一般情况下,时间复杂度较好的算法,时间效率高,相应的空间复杂度就会相对底一些。而想要追求空间效率,时间效率就会降低。大部分情况还是以时间复杂度为主,我们要根据实际情况对两者的效率适当分配。