浅谈时间、空间复杂度

浅谈时间、空间复杂度

/*2020/8/11*/

一、时间复杂度:

时间复杂度的定义:一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度,记为T(n)。
一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f (n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。

名词解释:
n:问题的规模,重复执行的次数
T(n):一段程序运行,各种操作代码所执行的总次数
f(n): 存在的某个函数,使得T(n)/f(n)=非零常数, 那么f(n)称为T(n)的同数量级函数
O:大O符号,一种符号,表示渐进于无穷的行为

 

关于时间复杂度(自己的理解)

	时间复杂度的意义在于可以在不运行的情况下知道哪个算法花费的时间多,哪个算法花费的时间少。
	这对于优化代码,评价算法的优劣是很有用的。

1.什么是时间复杂度

	在计算机科学中,时间复杂性,又称时间复杂度,算法的时间复杂度是一个函数,它定性描述该算法的运行时间。
	这是一个代表算法输入值的字符串的长度的函数。(以上来自百度百科)
	百度百科的看着有点抽象,我个人的理解是:根据与代码语句的执行次数而成正比变化的函数。假设每条语句执行消耗的时间一致,
	那么执行次数越多,消耗的时间自然就多,而时间复杂度自然就高;

2.时间复杂度的写法(格式)

	时间复杂度一般用大写O表示;
	常数级时间复杂度记为:O(1);
	线性级:O(n),
	平方级:O(n²)
	等;
	基本就是O()括号里放对应的n的表达式;
	我对此的理解是:n是可以决定代码执行次数的某种数(变量),f(n)就是执行次数相对与n的变化趋势,O(f(n))就是时间复杂度;

3.计算时间复杂度

	时间复杂度只计算量级最大的那条代码语句。
	这个我觉得还是可以理解的,低量级的时间复杂度的增长趋势相对与高量级的时间复杂度来说,是可以忽略不计的,
	自然就可以省略不计算,反正也不是精确的去计算代码的执行时间。
	举个例子:

按数量级递增排列,常见的时间复杂度有:

常数阶O(1),对数阶O(log2n),线性阶O(n),线性对数阶O(nlog2n),平方阶O(n^2),立方阶O(n^3),…,k次方阶O(n^k),指数阶O(2^n)

总的来说时间复杂度是总运算次数表达式中受n的变化影响最大的那一项(不含系数)

例如:

(1)sum = n*(n+1)/2;    //执行一次时间复杂度为O(1)
(2)   for(i=1;i<=n;i++)   //循环了n*n次,当然是O(n^2)
            for(j=1;j<=n;j++)
                 s++;
(3)   for(i=1;i<=n;i++)//循环了(n+n-1+n-2+...+1)≈(n^2)/2,因为时间复杂度是不考虑系数的,所以也是O(n^2)
            for(j=i;j<=n;j++)
      
(4)   for(i=1;i<=n;i++)//循环了(1+2+3+...+n)≈(n^2)/2,当然也是O(n^2)
            for(j=1;j<=i;j++)
                 s++;           s++;
(4)   i=1;k=0;
      while(i<=n-1)
        {
           k+=10*i;
            i++;
         }//循环了n-1≈n次,所以是O(n)
int i = 1, n = 100;
while(i < n){
    i = i * 2;
}
//设执行次数为x. 2^x = n 即x = log2n
//时间复杂度O(log2n)

 

最坏时间复杂度和平均时间复杂度
 最坏情况下的时间复杂度称最坏时间复杂度。一般不特别说明,讨论的时间复杂度均是最坏情况下的时间复杂度。
 这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的上界,这就保证了算法的运行时间不会比任何更长。
 平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下,算法的期望运行时间。设每种情况的出现的概率为pi,平均时间复杂度则为sum(pi*f(n))。

例如快速排序

void Quicksort(int *n,int left,int right)
{
	int m = right;
	if (left >= right)
		return;
	int s = n[left];
	while (left != right)//判断左右索引,相遇时就结束循环
	{
		while (n[right] >= s && left < right)
			right--;
		n[left] = n[right];

		while (n[left] <= s && left < right)
			left++;
		n[right] = n[left];
	}
	n[right] = s;
	Quicksort(n, 0, left - 1);//递归调用
	Quicksort(n, left + 1, m);
}

最差时间复杂度O(n^2), 平均的时间复杂度是O(n*log2n);

 

二、空间复杂度:

1、空间复杂度的定义

一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。程序执行时所需存储空间包括以下两部分。

(1)固定部分。这部分空间的大小与输入/输出的数据的个数多少、数值无关。主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间。这部分属于静态空间。

(2)可变空间,这部分空间的主要包括动态分配的空间,以及递归栈所需的空间等。这部分的空间大小与算法有关。一个算法所需的存储空间用f(n)表示。S(n)=O(f(n))其中n为问题的规模,S(n)表示空间复杂度。一般的递归算法就要有o(n)的空间复杂度了,简单说就是递归集算时通常是反复调用同一个方法,递归n次,就需要n个空间

2、空间复杂度的计算

递归算法的空间复杂度=递归深度N*每次递归所要的辅助空间

对于单线程来说,递归有运行时堆栈,求的是递归最深的那 一次压栈所耗费的空间的个数,因为递归最深的那一次所耗费的
 空间足以容纳它所有递归过程。


3例题计算

1,常数级

a=0;
b=0;
cout<<a<<b<<endl;

它的空间复杂度O(n)=O(1);

2,线性级

int fun(n)
{
	int k=10;
	if(n==k)
	{return n;}
	else{
	return fun(++n);
	}
}

递归实现,调用fun函数,每次都创建1个变量k。调用n次,空间复杂度O(n*1)=O(n)。

3,循环型
1)在循环里定义

for(int i=0;i<n;i++)
{
	int temp=i;
}

变量的内存分配发生在定义的时候,因为temp的定义是循环里边,所以是n*O(1)
2)在循环外定义

int temp=0;
for(int i=0;i<n;i++)
{
	temp=i;
}

temp定义在循环外边,所以是1*O(1)

总结


当一个算法的空间复杂度为一个常量,即不随被处理数据量n的大小而改变时,可表示为O(1);
当一个算法的空间复杂度与以2为底的n的对数成正比时,可表示为O(log2n);
当一个算法的空间复杂度与n成线性比例关系时,可表示为O(n).
若形参为数组,则只需要为它分配一个存储由实参传送来的一个地址指针的空间,即一个机器字长空间;
若形参为引用方式,则也只需要为其分配存储一个地址的空间,用它来存储对应实参变量的地址,以便由系统自动引用实参变量。

 

 

 

三、分析排序算法时间空间复杂度:

1、冒泡排序

void bubblingsort(int *n, int size)
{
	for (int i = 1; i < size; i++)
	{
		for (int j = 0; j < size-i; j++)
		{
			if (n[j+1]>n[j])
			{
				int t = n[j];
				n[j] = n[j+1];
				n[j+1] = t;
			}
		}

	}

}
/*
时间复杂度
外循环和内循环以及判断和交换元素的时间开销。
最优的情况也就是开始就已经排序好序了,那么就可以不用交换元素了,由于外层循环为n,内层所需要循环比较的次数为(n-1)、(n-2)…1由等差数列求和得时间花销为:[ n(n-1) ] / 2;所以最优的情况时间复杂度为:O( n^2 )。
最差的情况也就是开始的时候元素是逆序的,那么每一次排序都要交换两个元素,则时间花销为:[ 3n(n-1) ] / 2;(其中比上面最优的情况所花的时间就是在于交换元素的三个步骤);所以最差的情况下时间复杂度为:O( n^2 );

空间复杂度
冒泡排序的辅助变量空间仅仅是一个临时变量,并且不会随着排序规模的扩大而进行改变,所以空间复杂度为O(1)。
*/

 

2、选择排序

void choosesort(int *n,int size)
{
	int min;
	for (int i=0; i < size; i++)
	{
		min = i;
		for (int j = i + 1; j < size; j++)
		{
			if (n[j] < n[min])
				min = j;
		}
		if (i != min)
		{
			int t = n[i];
			n[i] = n[min];
			n[min] = t;
		}
	}
}
/*
时间复杂度:   
选择排序的复杂度分析。第一次内循环比较N - 1次,然后是N-2次,N-3次,……,最后一次内循环比较1次。共比较的次数是
(N - 1) + (N - 2) + … + 1,求等差数列和,得(N−1+1)∗N/2=N*N/2
舍去最高项系数,其时间复杂度为 O(N^2)。
虽然选择排序和冒泡排序的时间复杂度一样,但实际上,选择排序进行的交换操作很少,最多会发生 N - 1次交换。
而冒泡排序最坏的情况下要发生N^2 /2交换操作。从这个意义上讲,交换排序的性能略优于冒泡排序。而且,交换排序比冒泡排序的思想更加直观。

空间复杂度:  
最优的情况下(已经有顺序)复杂度为:O(0) ;最差的情况下(全部元素都要重新排序)复杂度为:O(n);平均的时间复杂度:O(1)
*/

3、插入排序

void insertsort(int *n, int size)
{
	for (int i = 1; i < size; i++)
	{
		int t = n[i];
		int j;
		for ( j = i - 1; j >= 0; j--)
		{
			if (t < n[j])
			{
				n[j+1] = n[j];
			}
			else
			{
				break;
			}
		}
		n[j+1] = t;
	}


}
/*
时间复杂度:
插入排序的时间复杂度分析。在最坏的情况下,数组完全逆序,插入第2个元素时要考察前1个元素,插入第3个元素是,要考虑前2个元素,…,插入第N个元素,要考虑前N−1
个元素。因此,最坏情况下的比较次数是 1+2+3+…+(N−1)1+2+3+…+(N−1),等差数列求和,结果为
N*N/2,所以最坏情况下的复杂度为 O(N^2)
最好情况下,数组已经是有序的,每插入一个元素,只需要考查前一个元素,因此最好情况下,插入排序的时间复杂度为O(N)。
空间复杂度:
算法的空间复杂度很清楚:计算中只用了两个简单变量,用于辅助定位和完成序列元素的位置转移。因此算法的空间复杂度是O(1),与序列大小无关
*/

4、快速排序

void Quicksort(int *n,int left,int right)
{
	int m = right;
	if (left >= right)
		return;
	int s = n[left];
	while (left != right)//判断左右索引,相遇时就结束循环
	{
		while (n[right] >= s && left < right)
			right--;
		n[left] = n[right];

		while (n[left] <= s && left < right)
			left++;
		n[right] = n[left];
	}
	n[right] = s;
	Quicksort(n, 0, left - 1);//递归调用
	Quicksort(n, left + 1, m);
}
/*
空间复杂度:logn
主要是由于递归造成的栈空间的使用,
最好的情况下其树的深度为:log2(n)
空间复杂度为 O(logn)
而最坏的情况下:需要n-1次调用,每2个数都需要交换,此时退化为冒泡排序
空间复杂度为 O(n)
平均时间复杂度为:O(logn)

时间复杂度:O(nlogn)
由于快速排序用到了递归调用,因此计算其时间复杂度也需要用到递归算法计算
递归算法的时间复杂度公式:T[n] = aT[n/b] + f(n)
**

最优情况下时间复杂度
快速排序最优的情况就是每一次取到的元素都刚好平分整个数组

此时的时间复杂度公式则为:T[n] = 2T[n/2] + f(n);T[n/2]为平分后的子数组的时间复杂度,f[n] 为平分这个数组时所花的时间
第一次递归:
T[n] = 2T[n/2] + n;
第二次递归:
令 n = n/2
= 2^2 T[ n/ (2^2) ] + 2n
第三次递归:
令:n = n/(2^2)
= 2^3 T[ n/ (2^3) ] + 3n
…
第m次递归:
令:n = n/( 2^(m-1) )
= 2^m T[1] + mn

当最后平分的不能再平分时,也就是说把公式一直往下跌倒,到最后得到T[1]时,说明这个公式已经迭代完了(T[1]是常量了)。
得到:T[n/ (2^m) ] = T[1] ===>> n = 2^m ====>> m = logn;
T[n] = 2^m T[1] + mn ;其中m = logn;
T[n] = 2^(logn)T[1]+lognn = n +nlogn
又因为当n >= 2时:nlogn >= n (也就是logn > 1),所以取后面的 nlogn;

综上所述:快速排序最优的情况下时间复杂度为:O( nlogn )

最差情况下时间复杂度
最差的情况就是每一次取到的元素就是数组中最小/最大的,这种情况其实就是冒泡排序了(每一次都排好一个元素的顺序)
此时的时间复杂度为:T[n] = n * (n-1) = n^2 + n;
综上所述:快速排序最差的情况下时间复杂度为:O( n^2 )

平均时间复杂度
快速排序的平均时间复杂度也是:O(nlogn)
*/

5、总结四种排序的时间、空间复杂度

四种基本排序的时间空间复杂度
名称最差时间复杂度平均时间复杂度稳定性空间复杂度
冒泡排序O(n^2)O(n^2)稳定O(1)
选择排序O(n^2)O(n^2)稳定O(1)
插入排序O(n^2)O(n^2)稳定O(1)
快速排序O(n^2)O(n*log2n)不稳定O(log2n)~O(n)

对于一个算法来说,空间复杂度和时间复杂度往往是相互影响的。当追求一个较好的时间复杂度时,可能会使空间复杂度的性能变差,即可能导致占用较多的存储空间;反之,当追求一个较好的空间复杂度时,可能会使时间复杂度的性能变差,即可能导致占用较长的运行时间。现在内存对我们来讲没那么重要所以有时我们可以用空间来换取时间以达到目的。

 

 

 

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值