时间复杂度和空间复杂度
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了时间复杂度和空间复杂度相关的知识,希望对你有一定的参考价值。
写在前面
说实话,我已经学了不少的数据结构了,第一次接触时间和空间复杂度的时候感觉有些不好理解,这篇博客就搁置下来了.这些天感觉自己思路理清楚了.所以开始和大家分享.要是有什么错误的地方,还请大家指正.看完我们就会发现,计算复杂度,我们就是计算一个大概,我们平常遇到的问题复杂度不是O(1)就是O(N),其他的一般很少见.
算法效率
在谈复杂度之前,我们必须说一下什么是好的代码,好的代码本质就是耗费算力较少的代码,我们都知道斐波那契数列,下面让我们看看用递归方法来实现会怎么样
#include <stdio.h>
int val = 0;
int Fib(int n)
val++;
if (n < 3)
return 1;
return Fib(n - 1) + Fib(n - 2);
int main()
int n = 0;
while (1)
printf("请输入: ");
scanf("%d", &n);
Fib(n);
printf("递归执行的次数 : %d\\n", val);
val = 0;
return 0;
从上面我们就可以看到,当我们 n 的值增大,我们会发现执行的次数增加的难以想象.后面我们就会了解到这是以指数的形式增长,这就是一个效率极低的代码.
时间复杂度
在看到这个时间一词后,我首先想到的是便是一个程序执行所用的时间.起初我认为这就是时间复杂度的解释吧,可是现实给了我一巴掌.要是我们用时间来衡量一个程序的好坏.那么厉害的计算机一定吊打老式计算机,一个程序出现两种不一样的复杂度,这不是荒谬吗! 后来我查了一下定义.原来是程序的一些代码的执行次数,我们先来看下定义.
时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间.一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道.但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式.一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度
大O渐进法
我们计算时间复杂度和后面的空间复杂度都是使用大O渐进法,现在你可能还不太了解这是什么,后面你一看例子就可以明白了
// 请计算一下Func1中++count语句总共执行了多少次?
void Func1(int N)
int count = 0;
for (int i = 0; i < N; ++i)
for (int j = 0; j < N; ++j)
++count;
for (int k = 0; k < 2 * N; ++k)
++count;
int M = 10;
while (M--)
++count;
我们可以轻易算出++count语句语句执行的次数是N2 + 2 * N + 10, 这对于我们来说是在是太简单了,so easy!!!那么这个函数的时间复杂度是 O(N2) ??? 我写了什么?为何是这样的?下面我把大O渐进法的规则说一下.
大O渐进法规则
- 用常数1取代运行时间中的所有加法常数.
- 在修改后的运行次数函数中,只保留最高阶项.
- 如果最高阶项存在且不是1,则去除与这个项目相乘的常数.得到的结果就是大O阶
看了规则后,我们还是一头雾水,这究竟什么跟什么,我是看不懂.大家不要担心我这里每一条都给出解释.
用常数1取代运行时间中的所有加法常数.
我们可以看到 N2 +2 * N + 10 里面 的 2 和 10 都是常数,这里都给我变成1得到 N2 + 1 * N + 1
只保留最高阶项
从第一步得到的结果是N2 + N + 1,我们只保留最高项,我们发现 ==N2==是最高项,这里就保存N2.
如果最高阶项存在且不是1,则去除与这个项目相乘的常数
判断最高项的系数是不是 1 不是就给他变成1,假如最高项是 2N2 ,我们就把他变成 N2,这就是得到O(N2)
如何计算时间复杂度
我们已经初步了解了时间复杂度的算法,下面我们就用几个例子来巩固一下.不过这些例子都是我们经常看到的类型,大家最好把思想记住.我们这里看一下下面函数的时间复杂度.
//计算Func2的时间复杂度
//计算Func2的时间复杂度
void Func2(int N, int M)
int count = 0;
for (int k = 0; k < M; ++k)
++count;
for (int k = 0; k < N; ++k)
++count;
printf("%d\\n", count);
我们计算实际执行次数是 N + M,由于不知道N和M的关系,随意时间复杂度就是 O(N+M) .假如我们要是知道了M和N关系,比如说M<<N 那么时间复杂度就是O(N).顺便把下面的例子也过一下吧.
// 计算Func3的时间复杂度?
// 计算Func3的时间复杂度?
void Func3(int N)
int count = 0;
for (int k = 0; k < 100; ++k)
++count;
printf("%d\\n", count);
这个就更简单了,实际次数就是 100,那么时间复杂度就是 O(1),我们可能会有些疑惑,我们执行了1亿次是不是也是O(1),实际上是的,只要是常数次数 就是O(1)
二分查找的时间复杂度
这个才是重点,我们对二分查找很熟悉,这里我们简单的实现一下,采用左闭右开的写法,这里要和大家说一下,下面的代码你一定要会写啊,二分查找的思想很简单,但是我们需要控制住细节.
// 计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
assert(a);
int begin = 0;
int end = n; // 修改一下 注意 边界
while (begin < end)
int mid = begin + ((end - begin) >> 1);
if (a[mid] < x)
begin = mid + 1;
else if (a[mid] > x)
end = mid;
else
return mid;
return -1;
不过我们如何计算他的复杂度呢,对于这种我们可能有以下三种情况.
- 最好情况 只查找一次就找到了
- 平均情况 查找多次才找到
- 最坏情况 最后还没有找到
那么我们应该用那种情况来计算时间复杂度呢?我们可以想一下,如果我们把一个项目送给上司,对他说这个代码的时间复杂度是O(1),但是当上司测试代码时候,发现很长时间才跑出了结果,你说他会怎么做:smile:.我们应该考虑最坏情况,这也是大O渐进法所规定的.
我这里重点说一下二分查找的时间复杂度怎么计算.我把过程画在了一张图上,我计算的有些粗糙,不过意思都是一样的,但我们折叠了X次后不能才折叠了.就是 2X = N.对此取对数我们就可以得到 X= log2 (N) 所以时间复杂度就是 (log2 N).由于代码中log2 N 不好表示,所以我们默认log2 N用 (log N)表示.所以二分查找函数的时间复杂度就是 (log2 N).下面是我简单的画的一张图片.
递归函数的时间复杂度
对于递归的函数时间复杂度我们也计算函数递归的次数.计算递归函数的时间复杂度是一大难点,我们要是了解函数的栈帧可能会容易一些.不过不用太担心这些,笔试一般不会出现太难的问题.
// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
if(0 == N)
return 1;
return Fac(N-1) * N;
我们假设 N的初始值是10,后面会依次递归,直到N=0时,函数一共递归了N次,每一次递归函数的时间复杂度都是O(1).所以这里总共的时间复杂度是O(N).这里我无法用文字形象的表示出来,要是大家不理解可以在网上找些视频或者私信我.
我们需要分析一下下面的多路递归,一般这里还是比较难的.
// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
这个斐波那契数列的递归更加困难了.要是理解二叉树可能会更好理解一点.这里我们还使 N=10这样我们就理解了一点,每往下一层我们递归的次数 *2(一些细枝末节就不考虑了) .可以很轻易的知道我们有N层(实际小于N),那么时间复杂的就是O(2N), 这就是我们最恶心的复杂度,也是很坏的代码.但我们N = 20时,会递归 1,048,576次.后面越来越恐怖.
总结
这里我们需要简单的总结一下时间复杂度是如何计算的.
- 对于非递归函数,我们简单的分析
- 对于递归函数,如果每次函数都是O(1),我们看它的递归次数,如果不是,我们就看它们每次递归的累加和.
空间复杂度
空间复杂度的计算和时间复杂度的计算是一样了,都是使用大O渐进法,不过空间复杂度关注的是空间的大小.它的计算比时间复杂度简单多了.
空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度 .空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数. 注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定.
我们还是来用例子演示.为了解决这一问题,我们最多是的时候开辟了三个变量 end ,exchange ,i所以对于这个冒泡而言,我们开辟的空间是常数,所以这里是O(1).
void BubbleSort(int* a, int n)
assert(a);
for (size_t end = n; end > 0; --end)
int exchange = 0;
for (size_t i = 1; i < end; ++i)
if (a[i - 1] > a[i])
Swap(&a[i - 1], &a[i]);
exchange = 1;
if (exchange == 0)
break;
注意我们可能会对exchange 感觉到疑惑,它不是开辟了N次吗?是的,但是每一开辟的都是同一块空间,并且出了作用域就被销毁了.下面的例子可能会解决你的疑惑.
int main()
for (int i = 0; i < 2; i++)
int ret = 0;
printf("%p\\n", &ret);
return 0;
这里我们再计算一个.
// 计算Fibonacci的空间复杂度?
// 返回斐波那契数列的前n项
long long* Fibonacci(size_t n)
if (n == 0)
return NULL;
long long* fibArray = (long long*)malloc((n + 1) * sizeof(long long));
fibArray[0] = 0;
fibArray[1] = 1;
for (int i = 2; i <= n; ++i)
fibArray[i] = fibArray[i - 1] + fibArray[i - 2];
return fibArray;
这个空间复杂度是O(N),我们确实开辟了 N 个 long long类型的空间.
递归函数的空间复杂度
下面我们来看一个递归的
// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
if(N == 0)
return 1;
return Fac(N-1)*N;
这个我们虽然没有开辟变量的空间,但是我们开辟了N个函数栈帧,每个栈帧里面都是常数个所以空间复杂度是O(N).
这里我们需要卡看一个比较难的.
// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
要是按照我们和时间复杂度一样的想法,我们也按照递归的展开图来分析,这里我们得到的结构是O(2N).这里我们简单的分析下,这个想法对不对?我们知道栈不大,但是够用.
long long Fac(size_t n)
if (n == 0)
return 1;
return Fac(n - 1)*n;
也就是说,我们只是简单的递归了10000次,这里栈就会溢出,那么我们们前面的的结果是如何跑出来的?
这里我们就可以的知道了,我们空间复杂计算错了,关于空间复杂度的计算,我们需要这样想.
我们先来解释下面的图.我们知道当N = 3时候,这里还会递归,首先递归的就是N = 2,记住,等到N=2这个路递归完了之后,我们才会递归N = 1,也就是说我们N = 2这个函数栈帧我们已经释放了,后面N = 1的时候重复使用了这块空间.也就是说,在我们这个函数空间是被重复利用了,图上的x,y就=是共用了同一片空间.
我们这里测试一下,比较详细的需要我们看函数栈帧那里,可以找些博客,我之前写的也有.
void f1()
int x = 0;
printf("%p\\n", &x);
void f2()
int y = 0;
printf("%p\\n", &y);
int main()
f1();
f2();
return 0;
总结
空间复杂度的计算比较简单,这里我们重点关注一下递归的,这里记住一点,时间是一去不复返的,空间却是可以重复利用的.
以上是关于时间复杂度和空间复杂度的主要内容,如果未能解决你的问题,请参考以下文章