算法是什么
算法
算法是用来干什么的?
算法是用来处理数据,解决问题的一组方法。对于同一个问题可以使用不同的算法来解决,但是其中消耗的时间和资源却可能不一样。
算法的优劣与否?
一般会通过算法的时间复杂度(程序耗时)和空间复杂度(程序所占用的内存)来衡量算法的优劣
时间复杂度
时间复杂度是什么?
时间复杂度用来反映程序的运行时间。常用的表示方法是大O表示法,即 T(n) = O(f(n))。可以通过下面的一段代码来解释下
1 | for (i <- 1 until n){ |
在大O表示法中,f(n)表示每行代码的执行次数之和,O 表示正比例关系。所以上面的代码的时间复杂度就可以根据代码执行次数来看。for 循环执行n次,循环体里面的每行语句执行n次,所以执行次数是 3n,即时间复杂度是 O(3n),当 n
无穷大时,就可以表示为 O(n)。下面介绍下常见的时间复杂度量级.
tip: 大O表示法主要用来表示程序的执行时间趋势,并不要求精准,因此上面的 O(3n) 可以变为 O(n)。
常数阶 O(1)
1 | i = 1 |
i = 1 只执行一次,时间复杂度 O(1)
线性阶 O(n)
上面提到的单层 for 循环的时间复杂度就是 O(n)
对数阶 O(logN)
1 | i = 1 |
while 循环中,i 经过 log2n(2为底数)次大于 n 并结束循环。时间复杂度为 O(logN)
线性对数阶 O(n * logN)
就是 O(logN) 的代码循环执行了 n 次,比如下面的代码
1 | for(i <- 1 until n) |
k次方阶
像 O(n2), O(n3), O(nK)
1 | for(x <- 1 until n) |
最好,最坏,平均情况时间复杂度
1 | def find(seq:Seq[String], x:String) = { |
find 函数用来寻找指定元素在数组中的位置,时间复杂度是和数组的长度 n 有关。根据上面的讲解我们可以判定为 O(n)。但是事实上情况并不是这样,我们要找的数据元素可能在数组开头,数组结尾,甚至根本没有我们要找的东西。
对应种种不同情况,由此引出了最好,最坏,平均情况时间复杂度的概念。最好的情况是数据在数组开头,我们查找一次就可以,时间复杂度是 O(1)。最坏的情况我们遍历了整个数组才找到,时间复杂度是 O(n)。
find 函数的平均情况时间复杂度可以表示为查找需要遍历的平均数。要查找的变量 x 在序列 seq 中有 n+1 种情况:0~n-1个位置和不在序列中。假设查找值在序列中和不在序列中概率一样都为 1/2,数据出现在在任意位置上的概率为 1/n。
那么任意位置上查找到数据的概率是 1/2n。所以整体的复杂度计算为 11/2n + 21/2n + …. + n*1/2n + n * 1/2 = (3n+1) / 4 ,所以平均时间复杂度为O(n)
很多时候,我们使用一个复杂度就可以满足需求了。只有同一块代码在不同的情况下,时间复杂度有量级的差距,我们才会使用这三种复杂度表示法来区分。
平均时间复杂度经常需要考虑到概率,因此也叫加权平均时间复杂度
均摊时间复杂度
1 | var array = Array.ofDim[Int](10) |
下面分析一下上述函数的的均摊时间复杂度,一般用摊还分析(耗时操作均摊给给非耗时操作)的方法来分析,add 函数用于像数组中添加元素,每当达到数组长度上限时,将数组扩充 2 倍。
可以很容易看到,正常添加情况下,时间复杂度是 O(1)。只有当数组长度达到上限需要复制扩充时,时间复杂度为 O(array.len)。
调用10次,数组扩充到20,复制10次
调用10次,数组扩充到40,复制20次
调用20次,数组扩充到80,复制40次
不考虑第一次特殊情况,每次数组倍增之前,函数都会被调用n/2次,倍增时执行一轮n次拷贝操作。所以平均时间复杂度为 1/(n/2)*(n/2-1)+n/(n/2)
摊还分析,每一次O(n)操作后,都跟着n次O(1)操作,耗时操作均摊给给非耗时操作,就变成了常量级 O(1) 操作
空间复杂度
空间复杂度是程序运行时所占用的存储空间大小的量度,并非具体值,同样反映的也是趋势,用 S(n) 来表示。空间复杂度比较常用的有:O(1)、O(n)、O(n²)
1 | var a = 1 |
a = 1,a 算法执行所需要的临时空间不随着某个变量n的大小而变化,空间复杂度为 O(1)。
第三行声明长度为10的数组,后面的循环不会对数组的空间产生变化,空间复杂度为 O(n)