《算法与数据结构》学习笔记1---时间复杂度与空间复杂度(上)

前言

    数据结构和算法的重要性相信学计算机的人都知道,一直以来算法和数据结构就是我的心病,相信和大家一样 这门课要说学吧,太难,自己啃不下来。但是不学吧,感觉又好像怎么着似的,弄的人心理不舒服。今在某软件上花重金买了一门《数据结构和算法》课,鼓起勇气认真的学习一次算法和数据结构,希望能对自己有所帮助。

正文

    数据结构和算法本身解决的是“快”和“省”的问题,即如何让自己写的程序运行的更快,如何让程序能够更加节省空间。执行效率是算法一个非常重要的老师指标。那么如何衡量程序的执行效率呢?

  • 时间复杂度
  • 空间复杂度

为什么:程序的执行时间和占用的内在大小可以通过将程序运行一次而得到,一些数据结构和算法书中把这种方法叫做事后统计法。虽然这种方法是可行的,但是这种统计方法也有很大的局限性。

  1. 测试结果依赖于测试环境
    通常测试环境的硬件不同,测试结果也会有不同。例如,一段相同的程序用i9和i3处理器来运行,显而易见当然是i9处理器执行快。
  2. 测试测试结果受数据规模的影响较大
    对于同一个排序算法,待排序数据的有序度不同,排序的执行时间就会有很大的差别。极端情况下,如果数据已经是有序的,那排序算法不需要做任何的操作,执行时间就会非常短。另外,如果测试数据规模太小,测试结果可能无法真实的反应算法的性能。

    因此,我们需要通过时间和空间复杂度分析方法来更加合理的了解程序。

大O复杂度:算法的执行效率,粗略的讲,就是指算法的程序执行的时间。
下面来看一段程序:
《算法与数据结构》学习笔记1---时间复杂度与空间复杂度(上)_第1张图片
    粗糙的估计,假设CPU执行第行程序的时间都是一样的,为单位时间。因此通过分析可知上面这段程序的总的执行时间T(n)=2n+2。因此可看出,代码的执行时间T(n)与每行代码的执行次数成正比。
《算法与数据结构》学习笔记1---时间复杂度与空间复杂度(上)_第2张图片
    依然假设每行的执行时间为单位时间,整体代码的总的执行时间T(n)=(2n2+2n+3)
虽然不知道单位时间的具体值是多少 ,但是可以得出一个结论:所有代码的执行时间T(n)与每行代码的执行次数n成正比。
此刻,大O登场:

                         T(n) = O(f(n))

    其中,T(n)表示代码的执行时间,n表示数据规模,f(n)表示每行代码执行的次数总和,O表示代码的执行时间T(n)与f(n)表达式成正比。

    第一个例子中 T(n) = O(2n+2),第二个例子中 T(n) = O(2n2+2n+3)。这就是大O时间复杂度表示法,它并不是代码的真正的执行时间,而是代码执行时间随数据规模增长的变化的趋势,也叫渐进时间复杂度,简称时间复杂度。
当n很大时,例如1000,10000。而公式中的低阶、常量、系数并不影响增长的趋势,所以都可以忽略,我们只需要记一个最大量级就可以了。因此以上两段程序的时间复杂度为T(n) = O(n) ;T(n) = O(n2)

时间复杂度分析:分析一段代码的时间复杂度,这里介绍三种方法:

  1. 只关注循环次数最多的一段代码
    以第一段代码为例,sum和i的执行时间都是常量级,真正的大佬是循环。循环处的两行代码被执行了n次,所以总的时间复杂度是O(n)。

   2.加法法则:总复杂度等于量级最大的那段代码的复杂度

int cal(int n) {
   int sum_1 = 0;
   int p = 1;
   for  (; p<100;++p) {
       sum_1 = sum_1 + p;
   }
   "以上代码循环执行100次执行时间为一个常量,与n的规模无关"
   int sum_2 = 0;
   int q = 1;
   for  (; q<n;++q) {
       sum_2 = sum_2 + q;
   }
   "复杂度为O(n)"
   int sum_3 = 0;
   int i = 1;
   int j = 1;
   for  (; i<=n;++i) {
       j = 1;
        for  (; j<=n;++j) {
            sum_3 = sum_3 + i*j
        }
   "复杂度为O(n)"
   }
}
 return sum_1 + sum_2 +sum_3
}

所以这段代码总的时间复杂度为O(n2)
    3. 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积。

int cal(int n) {
    int ret = 0;
    int i =1;
    for (; i< n; ++i) {
        ret = ret + f(i)
    }
}
int f(int n) {
    int sum = 0;
    int i =1;
        for (; i< n; ++i) {
        sum = sum + i
    }
    return sum;
}

    以上程序函数f嵌套在函数cal中,在函数cal()中,先假设函数f()为一个普通操作,此时函数cal()的时间复杂度为O(n),而函数f的时间复杂度为O(n),所以整个cal()函数的时间复杂度为O(n2)。

常见的时间复杂度实例分析

  • 常量阶O(1)
  • 对数阶O(log n)
  • 线性阶O(n)
  • 线性对数阶O(n*log n)
  • 平方阶O(n2)、立方阶O(n3)、…K次方阶O(nk)
  • 指数阶O(2n)
  • 阶乘阶O(n!)

    其中,1-5为多项式量级;6、7为非多项式量级。当数据规模n起来越大时,非多项式量级算法的执行时间会急剧增加,求解问题的执行时间会无限增长。所以,非多项式时间复杂度的算法是非常低效的算法。

  • O(1)
    只要程序的执行时间不会随n的增长而增长,这样的程序的时间复杂度都记为O(1),它只是常量级时间复杂度的一种表示方法,并不是指只执行了一行程序。
    一般而言,只要算法中没有循环语句、递归语句,即使程序再多,时间复杂度也是O(1)。
  • O(log n)、O(n*log n)
i = 1;
while (i <=n) {
   i = i * 2
}

    为了计算时间复杂度,现需要计算第三行程序被执行了多少次。20 * 21 * …2k=n, 2x = n. 所以,代码的时间复杂度为O(log2 n)
若把代码中的2换成3,则时间复杂度变为O(log3 n)。实际上,不管是以几为底,可以把所有对数时间复杂度都记为O(log n)。

  • O(m+n)、O(m*n)
int cal(int m, int n) {
    int sum_1 = 0;
    int i = 1;
    for (; i<m;++i) {
        sum_1 = sum_1 + i;
    }

    int sum_2 = 0;
    int j = 1;
    for (; j<n;++j) {
        sum_2 = sum_2 + j;
    }

   return sum_1 + sum_2
}

    因为m、n都表示两个数据规模,且我们无法事先评估它们的大小,所以在表示复杂度时不能简单的利用加法法则,活力掉其中的一个,所以。上述代码的时间复杂度为O(m+n)

空间复杂度:全称渐进空间复杂度,表示算法的存储空间与数据规模之间的增长关系。

void print(int n) {
    int i = 0;
    int[] a = new int[n]
    for (i;i<n;++i) {
        a[i] = i * i;
    }
    for (i = n-1; i>=0; --i) {
        print out a[i]
    }
}

    第二行代码申请了一个空间存储变量i,但是它是常量阶的,和数据规模n没有关系,可忽略。第三行申请了一个大小为N的int类型数组,其余代码都有占用更多的空间,所以这段代码的空间复杂度为O(n)。
    常见的空间复杂度为O(1)、O(n)、O(n2)。
    常见的复杂度并不多,从低阶到高阶有O(1)、O(log n)、O(n)、O(n*log n)、O(n2)
《算法与数据结构》学习笔记1---时间复杂度与空间复杂度(上)_第3张图片
对于复杂度分析法则,看到网友做了一个很不错的总结,这里列出来 :

  1. 单段代码看高频:比如循环
  2. 多段代码取最大:比如 一段代码中有单循环和多循环,那么取多重循环的复杂度。
  3. 嵌套代码求乘积:比如递归、多重循环
  4. 多个规模求加法:比如方法有两个参数控制两个循环的次数,此时两者复杂度相加。

你可能感兴趣的:(数据结构与算法)