大家都知道各种数据结构和算法的出现是为了让代码运行得更快,让代码存储时候更省空间,所以统计算法的执行效率和资源消耗是一个重要的参考指标.
怎么分析和统计算法的执行效率和资源消耗?
有一种是执行代码通过监控和统计得出来的数据叫"事后统计法",不过这个方法局限性很大,测试结果非常依赖测试环境的配置,而且受数据规模大小的影响也很大.所以,我们需要一个不用具体的测试数据来测试,就可以粗略地估计算法的执行效率的方法。
这就是我们今天要讲的时间、空间复杂度分析方法。
大 O 时间复杂度表示法
算法的执行效率,也就是算法代码执行的时间。但是,如何在不运行代码的情况下,直观的得到一段代码的执行时间呢?
以下是一段累加1~n之间的和的代码
public class demo {
int cal(int n) {
int sum = 0;
int i = 1;
for (; i <= n; ++i) {
sum = sum + i;
}
return sum;
}
}
大家一起来估算一下这段代码的执行时间。
假设每行代码执行的时间都一样是T,
3、4行分别是1time,5、6是循环要重复运行,所以是 2n*time 整理下来是 (2+2n)*time
所以可以看出来执行代码的时间和执行每行代码的次数是成正比的
接着看一个嵌套循环的例子
int cal2(int n) {
int sum = 0;
int i = 1;
int j = 1;
for (; i <= n; ++i) {
j = 1;
for (; j <= n; ++j) {
sum = sum + i * j;
}
}
return sum;
}
2、3、4 分别是1time,5、6是循环要重复运行,所以是 2n*time,7、8 行代码也是循环运行的,执行了 2n*time,
但7、8 是在循环里执行的,所以整理之后执行效率是(3+2n+2n²)*time
虽然不知道time具体值是多少,但通过以上示例的规律总结出一个公式,大O,T(n)=O(f(n))
公式中的 O,表示代码的执行时间 T(n) 与 f(n) 表达式成正比。
所以,第一个例子中的 T(n) = O(2n+2),第二个例子中的 T(n) = O(2n²+2n+3)。这就是大 O 时间复杂度表示法。
大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以,也叫作渐进时间复杂度(asymptotic time complexity),简称时间复杂度
大O时间复杂度表示法O(f(n))中的f(n)的值可以为1、n、logn、n²等,因此我们分别可以称为他们为
O(1)常数阶、O(2ⁿ)指数阶(非多项式量级)、
O(logn)对数阶、O(n!)阶乘阶(非多项式量级)、
O(n)线性阶、
O(nlogn)线性对数阶、
O(n²)平方阶、O(n³)立方阶、次方阶、
O(√n)平方根阶
未标注小括号的是多项式量级,当数据规模 n 越来越大时,非多项式量级算法的执行时间会急剧增加,求解问题的执行时间会无限增长。所以,非多项式时间复杂度的算法其实是非常低效的算法。
那么如何通过大O时间复杂度表示法推导分析出(f(n))的值呢?通过以下3个规律来进行分析
1.用常数1来取代运行时间中所有加法常数。
2.修改后的运行次数函数中,只保留最高阶项
3.如果最高阶项存在且不是1,则去除与这个项相乘的常数。
O(1)常数阶的示例
public class demo {
int cal(int n) {
int sum = 0; //执行一次
int i = 1; //执行一次
sum = sum + i; //执行一次
System.out.println (sum); //执行一次
return sum;
}
}
我们称上述示例的时间复杂度为O(1)
上面算法的运行次数为f(n)=4,根据推导大O阶的规则1,我们需要将常数4用1来取代,则这个算法的时间复杂度为O(1)。即使 sum = sum + i;这条语句再执行10遍,也和n的值并没有关系,所以这个算法的时间复杂度仍旧是O(1),我们可以称之为常数阶
O(logn)对数阶的示例
int cal5(int n) {
int sum = 1;
while (sum<=n){
sum=sum*2;
}
return sum;
}
从第三,四行的代码可以看出来,每循环一次就乘以 2。当大于 n 时,循环结束,这其实就是一个等比数列,通过 2^x=n得出x=log₂n,实际上,不管是以 2 为底、以 3 为底,还是以 10 为底,在对数阶时间复杂度的表示方法里,我们忽略对数的“底”,统一表示为 O(logn)。
O(n)线性阶的示例
我们在拿刚才示例重新分析一下,
public class demo {
int cal(int n) {
int sum = 0;
int i = 1;
for (; i <= n; ++i) {
sum = sum + i;
}
return sum;
}
}
3、4行分别是常数,5、6是被循环了n次,所以总的时间复杂度就是 O(n),线性阶.
O(nlogn)线性对数阶示例
对数阶+线性阶就是线性对数阶示例
int cal5(int n) {
int sum = 1;
int i = 1;
for (; i < n; ++i) {
while (sum<=n){
sum=sum*2;
}
}
return sum;
}
O(nlogn) 也是一种非常常见的算法时间复杂度。比如,归并排序、快速排序的时间复杂度都是 O(nlogn)。
O(n²)平方阶的示例
int cal4(int n) {
int sum = 0;
int i = 1;
for (; i < n; ++i) {
sum = sum + fff(i);
}
return sum;
}
int fff(int n) {
int sum = 0;
int i = 1;
for (; i < n; ++i) {
sum = sum + i;
}
return sum;
}
第五行有一个方法fff,内部也有一个循环,所以是一个嵌套循环,那么他的时间复杂度就O(n*n) = O(n²)平方阶
空间复杂度
空间复杂度和时间复杂度所表达的方式一样的也叫作渐进空间复杂度(asymptotic space complexity)表示算法的存储空间与数据规模之间的增长关系
理解了时间复杂度,空间复杂度就非常简单了
示例
int[] a = new int[6];这行代码的空间复杂度就是 O(1)
int[] a = new int[n];这行代码的空间复杂度就是 O(n)
是不是很好理解呢?
上述文章如果哪里有错误欢迎留言纠正,看了不理解的话就多看几次