【时间复杂度和空间复杂度】

简介: 1.时间复杂度时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个数学函数,它定量描述了该算法的额外运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为法的时间复杂度。即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。

1.时间复杂度

时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个数学函数,它定量描述了该算法的额外运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为法的时间复杂度。

即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。

总的来说,时间复杂度就是大致计算代码运行的次数。

举一些例子分析:

1.嵌套的时间复杂度计算

// 请计算一下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;
}
printf("%d\n",count);
}

实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这

里我们使用大O的渐进表示法。

推导大O阶方法:

1、用常数1取代运行时间中的所有加法常数。

2、在修改后的运行次数函数中,只保留最高阶项。

3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。

使用大O的渐进表示法以后,Func1的时间复杂度为:O(N^2)。

总的来说,1.就是使用O(1)来代表常数量级的运行次数

2.如果一段代码的运行次数是F(N) = N + 2,那么就保留最高阶项,时间复杂度为O(N)。

3.如果一段代码的运行次数是F(N) = 2*N^2,那么就去掉系数,时间复杂度为O(N^2)。

通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。

另外有些算法的时间复杂度存在最好、平均和最坏情况:

最坏情况:任意输入规模的最大运行次数(上界)

平均情况:任意输入规模的期望运行次数

最好情况:任意输入规模的最小运行次数(下界)

例如:在一个长度为N数组中搜索一个数据x

最好情况:1次找到

最坏情况:N次找到

平均情况:N/2次找到

在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)。

常见的时间复杂度计算:

  1. 双重循环时间复杂度计算
// 计算Func2的时间复杂度?
void Func2(int N)
{
int count = 0;
for (int k = 0; k < 2 * N ; ++ k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}

可以看出,运行次数为F(N) = 2*N +10 , 根据大O的表示法,时间复杂度为O(N)。

// 计算Func3的时间复杂度?
void Func3(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);
}

先计算运行次数为F(N) = M*N,那么时间复杂度为O(N)或者O(M),取决于M和N的大小:

  1. 当M>>N 时,时间复杂度为O(M)
  2. 当N>>M 时,时间复杂度为O(N)
  3. 当M和N差不多大时,时间复杂度为O(M)或者O(N)

2.常数循环的时间复杂度

// 计算Func4的时间复杂度?
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++ k)
{
++count;
}
printf("%d\n", count);
}

运行次数为F(N) = 100,所以时间复杂度为O(1).

注意:时间复杂度为O(1)并不代码代码只循环一次,而是常数次。

  1. strchar的时间复杂度。

strchar是从一个长度为N的字符串中查找一个字符的函数.


928c5c68192245409bb9f484d9b611d0.png

该函数的核心部分如上,假设从hello world中查找。


698f4fcaaf034c94b394df4d8edf164c.png

所以做悲观预期,时间复杂度为O(N)。

5.冒泡排序时间复杂度

// 计算BubbleSort的时间复杂度?
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;
}
}

对于一个冒泡排序,假设有10个数需要排序,第一次要排9次,第二次排8次,第三次排7次...

假设有n个数要排序,第一次排n-1次,第二次排n-2次,第三次排n-3次...

直到排到剩下一个数,就不需要排序了。

所以运行次数为F(N) = (N-1) +(N-2) + (N-3) +...+1,这是一个等差数列求和,所以可以认为,运行次数为F(N) = (N-1)*N/2,所以时间复杂度为O(N^2)。

6.二分查找的时间复杂度

// 计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
assert(a);
int begin = 0;
int end = n-1;
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;

求时间复杂度不能只看循环的次数,需要结合算法的实际来看。

假设有100个数字,查找最坏的情况,查找一次,除以2,查找一次,除以2,最后,就只剩下一个数字。 也就是 100/2/2/2/2/2/2 =1 。 所以查找次数7次。

假设有n个数字,要查找x次才到1,那么就是 2^x = n , 所以x = log以2为底n的对数。

所以二分查找的时间复杂度为O(log以2为底n的对数)

7.递归算法的时间复杂度

计算方法:递归次数*每次递归调用的次数

  1. 阶乘递归时间复杂度
// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
if(0 == N)
return 1;
return Fac(N-1)*N;
}

递归次数为N-1,每次调用一次,所以时间复杂度为O(N)。

2.斐波那契数列时间复杂度

// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
}

画出斐波那契的计算图:

27d813dcb55d411c9692a5affda445d8.png

计算次数是 2^0* 2^1* 2^2* ...* 2^N-2* 2^N-1 - X,这是一个等比数列,F(N) =2^N -1 -X ,

时间复杂度为O(2^N)。

2.空间复杂度

空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度 。

空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。

空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。

注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因

此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。

总的来说,空间复杂度是计算变量个数的。

1.计算冒泡排序的空间复杂度

// 计算BubbleSort的空间复杂度?
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;
}
}

对一个冒泡排序来说,空间复杂度是看该排序中的变量的个数,在外部循环中,创建了一个变量n,内部循环创建了一个变量i,但是,在出了内部循环后,i就销毁了, 再次进去才又创建了变量i,重新创建的变量i是占用之前创建的变量的空间的。 同理,exchange变量也是如此,总体来说,程序相当于只创建了3个变量,所以空间复杂度为O(1).


2.计算斐波那契额数列的空间复杂度

// 计算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;
}

e8baff62c158420a89b398d1c34fcd42.png

对于变量的创建和销毁来说,先创建Fib(N),再有Fib(N-1),再有Fib(N-2),一直往下走,最后创建Fib(2),Fib(1)。随后销毁之前创建的N个变量,再去创建Fib(N-1),到Fib(N-3),一直往下走,之后每一次创建的变量均小于第一次创建的N个变量个数,所以空间复杂度是O(N).


3.阶乘递归的空间复杂度:

// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{
if(N == 0)
return 1;
return Fac(N-1)*N;
}

与斐波那契额数列同理,阶乘递归的空间复杂度是O(N).

相关文章
|
存储 人工智能 缓存
空间复杂度介绍
空间复杂度介绍
130 0
|
1月前
|
机器学习/深度学习 存储 算法
一篇文章理解时间复杂度和空间复杂度
一篇文章理解时间复杂度和空间复杂度
30 0
|
5月前
|
算法 程序员 存储
时间复杂度与空间复杂度详解
时间复杂度与空间复杂度详解
|
6月前
|
算法
了解时间复杂度和空间复杂度
在学习数据结构前,我们需要了解时间复杂度和空间复杂度的概念,这能够帮助我们了解数据结构。 算法效率分为时间效率和空间效率
37 1
|
6月前
|
机器学习/深度学习 存储 算法
时间复杂度和空间复杂度
时间复杂度和空间复杂度
|
机器学习/深度学习 算法
时间复杂度和空间复杂度详解
时间复杂度和空间复杂度详解
258 0
|
6月前
|
机器学习/深度学习 算法 Windows
时间复杂度与空间复杂度
如何理解时间复杂度与空间复杂度
|
6月前
|
机器学习/深度学习 算法 搜索推荐
2.时间复杂度与空间复杂度
2.时间复杂度与空间复杂度
47 0
|
算法
【时间复杂度和空间复杂度】
【时间复杂度和空间复杂度】
58 0
|
存储 机器学习/深度学习 并行计算
空间复杂度
空间复杂度(Space Complexity)是用于描述算法在执行过程中所需占用空间大小的量度。空间复杂度通常与算法的输入规模成正比,表示算法在处理不同规模数据时所需的空间资源。空间复杂度可以帮助我们评估算法的效率,以及在实际应用中可能产生的内存消耗。
87 0