资讯专栏INFORMATION COLUMN

时间复杂度计算及空间复杂度计算

tanglijun / 1494人阅读

摘要:时间效率被称为时间复杂度,而空间效率被称作空间复杂度。时间复杂度时间复杂度的概念时间复杂度的定义在计算机科学中,算法的时间复杂度是一个函数数学中带有未知表达式的函数,它定量描述了该算法的运行时间。


1.算法效率

算法效率分析分为两种:第一种是时间效率,第二种是空间效率。时间效率被称为时间复杂度,而空间效率被称作空间复杂度。 时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算法所需要的额外空间,在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。

2.时间复杂度

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

3.空间复杂度

空间复杂度是对一个算法在运行过程中临时占用存储空间大小量度 。空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。

4.大O渐进表示法

  • 大O符号(Big O notation):是用于描述函数渐进行为的数学符号。推导大O阶方法:
  • 1、用常数1取代运行时间中的所有加法常数。
  • 2、在修改后的运行次数函数中,只保留最高阶项。
  • 3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。

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

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

  • 最好情况:任意输入规模的最小运行次数(下界) 例如:在一个长度为N数组中搜索一个数据x 最好情况:1次找到 最坏情况:N次找到

  • 平均情况:N/2次找到 在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)

5.常见时间复杂度

计算一下这个算法的时间复杂度

// 请计算一下Func1基本操作执行了多少次?void Func1(int N) {int count = 0;//两层循环嵌套外循环执行n次,内循环执行n次,整体计算就是N*N的执行次数for (int i = 0; i < N ; ++ i) {	 for (int j = 0; j < N ; ++ j)	 {		 ++count;	 }}//2 * N的执行次数for (int k = 0; k < 2 * N ; ++ k){	 ++count;}//常数项10int M = 10;while (M--) { 	++count;  } printf("%d/n", count);}

精确的时间复杂度是N ^ 2 + 2 * N + 10
大O的渐进表示法时间复杂度是O(N ^ 2)
分析:
1、两层循环嵌套外循环执行n次,内循环执行n次,整体计算就是N*N 的执行次数
2、2 * N的执行次数
3、常数项10
根据前面的大o渐进表示法规则,所以最后只保留那项对执行次数影响最大的那一项,时间复杂度就是O(N ^ 2)

计算一下这个算法的时间复杂度

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

精确的算法执行次数是2 * n + 10
采用大O渐进表示法规则后时间复杂度是O(N)
如果最高阶项存在且不是1,则去除与这个项目相乘的常数,那保留的就会是O(N)

计算一下这个算法的时间复杂度

// 计算Func3的时间复杂度?void Func3(int N, int M){	//执行M次	int count = 0;	for (int k = 0; k < M; ++ k) 	{		++count; 	}	//执行N次	for (int k = 0; k < N ; ++ k) 	{		++count; 	}	printf("%d/n", count);}

时间复杂度:O(M + N)
假设:
M远大于N --> O(M)
N远大于M --> O(N)
M和N一样大 --> O(M) / O(N)
还是取影响最大的那一项,如果并没有说明M和N的大小关系,那么时间复杂度就是O(M + N)

计算一下这个算法的时间复杂度

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

时间复杂度:O(1)
循环执行的次数是常数次,常数取1

计算一下这个算法的时间复杂度

// 计算strchr的时间复杂度?const char * strchr ( const char * str, int character ){	while(*str)	{		if(*str == character)			return *str;		else			str++;	}}

时间复杂度:O(N)
最坏情况:遍历到最后才找到或者字符串中压根就没有,O(N)
平均情况:O(N / 2) , 忽略系数项是O(N)
最好情况:一次就找到了,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; 	} }

精确的时间复杂度:O(N * (N-1) / 2)
冒泡排序的思想是:假设给你N个元素,让你排升序,对N个元素排升序,只需要执行N - 1趟,两两相比较,每趟交换都会将最大的那个元素换到最后面去,这样子一来最大的数就都集中在后面,不就相当于每排序一趟后就少比较一个元素嘛,因为后面已经是最大的了不需要再交换。
通过公式展开后采用大O渐进表示法规则后,时间复杂度:O(N ^ 2)

计算一下这个算法的时间复杂度

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

先来理解二分查找的思想:
二分查找是通过下标来搜索对应的元素值,前提是这个数组是有序的,通过确定中间位置划分左右区间,如果val小于mid那么目标值排除了出现在右边的情况,继续再左边区间查找,确定中间下标,通过中间下标划分左右区间,直到找到,否则一直重复规则直到循环结束,理解了二分查找的思想我们能想到的几种情况

最坏情况:O(log N)

每次确定中间下标,划分左右区间的时候都会除以2,那么
N / 2
N / 4
N / 8
.
.
.
1 //等于1的时候找到了
当找到了,要想知道它的执行次数的时候,通过它的展开就能知道
1 * 2 * 2 * 2 * 2 = N
2 ^ x = N
x = log N (以2为底,N的对数)

平均情况:不考虑
最好情况:O(1)

计算一下这个算法的时间复杂度

// 计算阶乘递归Factorial的时间复杂度?long long Factorial(size_t N) {	return N < 2 ? N : Factorial(N-1)*N; }

时间复杂度:O(N)
这个算法,递归的开始条件是当N >= 2,它的结束条件是N < 2,当条件成立的时候会一直递归下去,直达条件为假递归终止才会将结果返回,递归的深度决定的是算法的时间复杂度,
N - 1
N - 2
N - 3
.
.
2
1 //递归结束,递归的层数是N

计算一下这个算法的时间复杂度

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

时间复杂度:O(2 ^ N)
经典的递归分治算法,读者可以将它看成一个完全二叉树,在它递归层层展开的时候开辟的栈帧看作是一个结点,每创建一个结点都是一次函数调用,那么在脑海中想象出来的场景是这样的,可以看到它的调用次数是 Fib(N) = 2 ^0 + 2 ^1 + 2 ^ 2 + … + 2 ^(n -1) - x
时间复杂度:O(2 ^ N)

计算一下这个算法的空间复杂度

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

空间复杂度:O(1),常数个额外的变量

计算一下这个算法的空间复杂度

// 计算Fibonacci的空间复杂度?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),malloc了N+1个long long大小的空间

计算一下这个算法的空间复杂度

long long Factorial(size_t N) {	 return N < 2 ? N : Factorial(N-1)*N; }

空间复杂度O(N),之前说时间复杂度的时候讲这个例子是每调用一次这个函数就会开辟一次栈帧,调用了N次

long long Fib(size_t N){	if(N < 3)		return	else		return Fib(N - 1) + Fib(N - 2); }

空间复杂度是O(N)
再之前讲时间复杂度的时候用这个例子,说的是将它看成一个完全二叉树,在它递归层层展开的时候开辟的栈帧看作是一个结点,每创建一个结点都是一次函数调用,当然计算时间复杂度符合这个特性,因为时间是不可复用的,而如果以空间复杂度的角度来计算的话,结果却并不是这样子,因为空间是可以复用的,每次递归调用,当Fib(N-1)递归完了,就会将内存归还给操作系统,进而才递归Fib(N - 2),所以空间复杂度是O(N)

常见复杂度对比

oj练习

17.04. 消失的数字
链接: link.

题目描述:
数组nums包含从0到n的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在O(n)时间内完成吗?

  • 思路一:快速排序 时间复杂度是O(N * log N),并不符合题目要求
  • 思路一:0 ~N个数字全部加起来再减去数组中的所有数字加起来,得到的结果就是缺失的那个数,时间复杂度O(N)
  • 思路三:内存映射,将数组元素值出现的次数放在对应的一个数组下标位置,时间复杂度是O(N)
  • 思路四:异或,将m与0 ~ numsize个数字和0 ~ numsize数组元素异或,最后再与数组长度异或,时间复杂度是O(N)
//实现思路四:int missingNumber(int* nums, int numsSize){    int m = 0;    int i = 0;    while(i < numsSize)    {        m ^= i;        m ^= nums[i++];    }        m ^= numsSize;    return m;}

旋转数组
链接: link.
题目描述:

给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数

实现思路:
数组元素整体逆置,【0,numsSize - 1】
左区间逆置,【0,k - 1】
右区间逆置,【k,numsize - 1】

//逆置数组void reverse(int* nums1,int *nums2){   while(nums1 < nums2)   {       int tmp = *nums1;       *nums1 = *nums2;       *nums2 = tmp;       ++nums1;       --nums2;   }   }void rotate(int* nums, int numsSize, int k){    if(k >= numsSize)    k %= numsSize;        reverse(nums, nums + numsSize - 1);    reverse(nums,nums + k - 1);    reverse(nums + k,nums + numsSize - 1);   }

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/122302.html

相关文章

  • 揭开神秘的面纱,这数据结构与算法的背后,究竟在隐藏着什么?

    摘要:如何合理地组织数据高效地处理数据这就是数据结构主要研究的问题。数据结构无外乎关系数据的逻辑结构和存储结构然后后面接触到算法分析包括时间复杂度和空间复杂度了解它的特性和评价标准。 ...

    hot_pot_Leo 评论0 收藏0
  • OpenCV4机器学习(七):KNN 原理实现

    摘要:前言本专栏主要结合,来实现一些基本的图像处理操作经典的机器学习算法比如决策树贝叶斯分类器等,以及常用的深度学习算法。现在设定,并计算到待测样本最近的个样本有个苹果和个梨子,距离分别为。 ...

    Mr_zhang 评论0 收藏0
  • 数据结构与算法的重温之旅(三)——数组

    摘要:整个程序在存入是数据大于数组长度的时候才会发生数组的删除操作。数组作为非常基础的数据结构,通过下标随机访问数组元素又是其非常基础的编程操作,效率的优化就要尽可能做到极致。本系列所有文章的代码都是用JavaScript实现,之所以用JavaScript实现是因为它可以直接在浏览器宿主中运行代码,即在浏览器中按f12打开控制台,选择console按钮,在下面空白的文本框把本例的代码黏贴上去回车即...

    jsliang 评论0 收藏0
  • 什么是粒计算?和云计算有什么关系?

    摘要:粒计算中存在个基本问题粒化和基于粒化的计算。其实,粒计算比云计算的概念出现得还早。云计算和粒计算之间的关系云计算提供的服务也是个性化的,是多粒度和可变粒度的提供的是细粒度服务。 云计算真正作为一个新兴技术得到IT界认可是在2007年左右,经过这十年的普及和发展,云计算早已走进千万个数据中心,成为IT世界里炙手...

    saucxs 评论0 收藏0
  • 什么是粒计算?和云计算有什么关系?

    摘要:粒计算中存在个基本问题粒化和基于粒化的计算。其实,粒计算比云计算的概念出现得还早。云计算和粒计算之间的关系云计算提供的服务也是个性化的,是多粒度和可变粒度的提供的是细粒度服务。 云计算真正作为一个新兴技术得到IT界认可是在2007年左右,经过这十年的普及和发展,云计算早已走进千万个数据中心,成为IT世界里炙手可热的技术门类,并可以在未来的一段时间内继续获得长足发展。云计算固然好,但也有不少的...

    RobinTang 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<