度量算法的效率时就会提到时间复杂度和空间复杂度。
在谈时间复杂度之前我们先介绍一个O渐进表示法。
一、什么是O渐进表示法
一个算法语句总的执行次数总是关于问题规模N的某个函数,记为f(N),N称为问题的规模。语句总的执行次数记为T(N),当N不断变化是,T(N)也在变化,算法执行次数的增长速率和f(N)的增长速率相同。则有T(N)=O(f(N)),称O(f(N))为时间复杂度O的渐进表示法。
- 一般算法O(n)的计算方法:
- 先找次数
- O( 次数)
- 用常数1取代运行时间中的所有常数
- 保留最高阶
- 丢掉常数C
二、时间复杂度
一个算法语句总的执行次数总是关于问题规模N的某个函数,记为f(N),N称为问题的规模。语句总的执行次数记为T(N),当N不断变化是,T(N)也在变化,算法执行次数的增长速率和f(N)的增长速率相同。则有T(N)=O(f(N)),称O(f(N))为时间复杂度O的渐进表示法。
三、空间复杂度
函数中创建对象的个数关于问题规模函数表达式,一般情况下用O的渐进表示法表示。
我们下来计算一下常见程序的时间复杂度和空间复杂度
- 二分查找法
int binary_search(int arr[], int key, int sz) { int left = 0; int right = sz - 1; int mid = 0; while (left <= right) { mid = left + (right - left) / 2; if (arr[mid] == key) { return mid; } else if (arr[mid] < key) { left = mid + 1; } else { right = mid - 1; } } if (left <= right) return mid;O else return 0; }
二分查找法的基本思想是将n个元素的有序数组大致分成相等的两部分,每次取中间的数和要找的数进行比较。时间复杂度就是计算while的循环次数,总共有n个元素每一次折半那就是n,n/2,n/4.....n/2^k(接下来所要查找的元素个数),k就是while的循环次数,给n/2^k取整后大于等于1,即n/2^k=1,计算可得k(循环次数)=Log₂n。所以二分查找法的时间复杂度可以表示为:O(Log₂n)。在执行这个函数时,它只创建了三个变量left、right和mid,因此它的空间复杂度为O(1)。
- 斐波那契数列
- 递归法
int fib(int n) { if (n <3) return 1; else return fib(n - 1) + fib(n - 2); }
我们来求一下fib(5),要计算fib(5)就要计算fib(4)和fib(3),计算fib(4)又要计算fib(2)和fib(3)........它们之间存在一种相互依赖的关系,我们可以画出树状图来表示这种依赖关系。
现在我们来计算一下总共计算调用函数的次数,2³-1+2次,也就是2^(5-2)+1。以此类推可以得到当计算fib(n)是函数调用的次数为2^(n-2)+1,那么斐波那契数列的时间复杂度为O(2^n)。
在计算空间复杂度的时候要考虑到函数运行时栈帧,所以我们来画图仔细分析一下
在计算fib(n)的时候函数最大要开辟n-1个空间才足够使用,那么斐波那契数列递归法的时间复杂度为O(n-1)->O(n)。
由此可以看出递归的算法在计算时有好多重复计算的值导致程序的效率下降,时间复杂度较大。但是使用循环的时候会避免重复计算
- 循环法
int fib(int n) { int a = 0; int b = 1; int c = 1; while (n > 2) { a = b; b = c; c = a + b; n--; } return c; }
我们来计算一下这个程序的时间复杂度
当计算fib(5)的时候,这里只循环3次,当计算fib(n)的时候循环n-3次,那么使用循环的方法可以将时间复杂度优化到O(n)。因为创建了3个变量,所以它的空间复杂度为O(1)。
- 尾递归
- 尾递归定义:在程序中,执行的最后一条语句是对自己的调用,而且没有别的运算
- 尾递归的实现:尾递归是在编译器优化的条件下实现的
long fib(long first, long second, long N) { if (N < 3) return 1; if (N == 3) return first + second; return fib(second, first + second, N - 1); }
使用尾递归的方法也很大程度上减小了时间复杂度,这里计算fib(5)的时候,函数只需调用3次,当计算fib(n)的时候函数需要调用n-2次,所以它的时间复杂度为O(n)。尾递归的调用次数相较于递归减少了很多,而且避免了大量的重复计。我们来画图研究一下它的空间复杂度。
这里计算fib(1,1,5)的时候只需开辟(5-2)空间 就足够了,那么它的空间复杂度为O(n-2)->O(n)。
- 用递归法实现阶乘
int fac(int n) { if (n <= 0) return 1; else return n*fac(n - 1); }
在计算fac(5)的时候要调用函数6次,那当计算fac(n)的时候需要调用n+1次,那它的时间复杂度为O(n)。接下来研究一下它的空间复杂度:
计算fac(5)如上图所示,推广到n,那么空间复杂度为O(n+1)->O(n)。
- 求1+2+3+……+N之和
int Sum(int n) { if (n <= 1) return 1; else return n + Sum(n - 1); }
它的时间复杂度为O(n),空间复杂度为O(n)。