数据结构与算法(C语言版)P1---算法效率

2023-09-15 18:59:42

算法的效率:算法的时间复杂度和空间复杂度

【本节目标】

  • 1.算法效率
  • 2.时间复杂度
  • 3.空间复杂度
  • 4.常见时间复杂度以及复杂oj练习

1、算法效率

1.1、如何衡量一个算法是的好坏

如何衡量一个算法的好坏呢?比如斐波那契数列:

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

斐波那契数列的递归实现方式非常简洁,但简洁一定好吗?那该如何衡量其好与坏呢?

1.2、算法的复杂度

算法在编写可执行程序后,运行时需要耗费时间资源和空间(内存)资源。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量,即时间复杂度和空间复杂度。

时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎,但是经过计算机行业的迅速发展,计算机的存储容量已经达到的很高的程度。所以我们如今已经不再需要特别关注一个算法的空间复杂度。

2、时间复杂度

2.1、时间复杂度和概念

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

算法中的基本操作的执行次数,为算法的时间复杂度。

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

那下面举个例子:

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

我们可以得出:时间复杂度的函数式:F(N) = N*N+2*N+10。

那这个时候我们赋予N具体的值:

  • N=10时,F(N)=130
  • N=100时,F(N)=10210
  • N=1000时,F(N)=1002010

我们想一下,当N越大,其实F(N)的值,就和__N*N__这个式子关系越大,后面的__2*N+10__所带来的值对整体的F(N)的值影响不大。

实际中我们计算时间复杂度时,我们其实并不是要计算精确的执行次数,而是只需要大概执行次数,那么这里我们使用__大O的渐进表示法。__

2.2、大O的渐进表示法

大O符号(Big O notation):用于描述函数的进行行为的数学符号。

推导大O阶方法:

1、用常数1取代运行时间中的所有加法常数。(只要有常数项就用1去取代)

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

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

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

  • N=10,F(N)=100。
  • N=100,F(N)=10000。
  • N=1000,F(N)=1000000。

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

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

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

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

  • 最坏情况:N次找到。
  • 平均情况:N/2找到。
  • 最好情况:1次找到。

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

2.3、常见时间复杂度计算练习

下面我们多看几个案例,来练习一下大O阶:

2.3.1、单层循环时间复杂度计算

案例1:

//计算Func2的时间复杂度
void Func2(int N)
{
	int count = 0;

	for (int k = 0; k < 2 * N; ++k)
	{
		++count;
	}

	int M = 0;
	while (M--)
	{
		++count;
	}
	printf("%d\n", count);
}

大O阶表示法:O(N) = N

分析:F(N) = 2*N+10

随着N的数值越来越大,+10的这一项对F(N)的值影响不大,所以忽略。

然后高阶项是2*N,N的系数不是1,所以把N前面的系数省略。

所以用大O阶法表示就是O(N) = N

2.3.2、嵌套循环时间复杂度计算

//请计算一下Func1中++count语句总共运行了多少次?
void Func1(int N)
{
	int count = 0;
	for (int i = 0; i < N; ++i)
	{
		for (int j = 0; j < N; ++j)
		{
			++count;
		}
	}
}

大O阶表示法:O(N) = N^2

2.3.3、双重循环时间复杂度计算

案例2:

//计算Func3的时间复杂度
void Func3(int N,int M)
{
	int count = 0;
	for (int i = 0; i < N; ++i)
	{
		++count;
	}

	for (int i = 0; i < M; ++i)
	{
		++count;
	}
 printf("%d\n",count);
}

大O阶表示法:分情况

分析:

1、如果没有说明M和N的大小关系:

  • O(M+N)

2、如果说明了M和N的大小关系:

  • M远远大于N:O(M)
  • N远远大于M:O(N)
  • M差不多相等于N:O(M)或O(N)

2.3.4、常数循环的时间复杂度

案例3:

//计算Func4的时间复杂度
void Func4(int N,int M)
{
	int count = 0;
	for (int k = 0; k < 100; ++k)
	{
		++count;
	}
	printf("%d\n", count);
}
大O阶表示法:O(N) = O(1)

分析:由上面的大O阶规则:__用常数1取代运行时间中的所有加法常数。(只要有常数项就用1去取代)__来说。

此大O阶表示:O(1)。

【扩展】:在做题时,题目会要求:把这个题的时间复杂度优化到O(1)。那这意思并不是只能运算一次。而是说需要运算常数次。

2.3.5、strchar的时间复杂度

案例4:

//计算strchar的时间复杂度
//【补充】strchar是个库函数,用于查找,搜索相匹配的字符串
const char* strchar (const char* str,int character);

比如现在有个字符串:"hello world"

大O阶表示法:分情况

分如下几种情况:

  • 假设查找的是h,大O阶表示法:O(1)。
  • 假设查找的是w,大O阶表示法:O(N/2)。
  • 假设查找的是d,大O阶表示法:O(N)。

那在实际情况中,当一个算法随着输入的不同,时间复杂度不同,时间复杂度一律做悲观预期处理,看最坏的情况,所以上面的大O阶表示法就是O(N)。

2.3.6、冒泡排序的时间复杂度的计算

案例5:

冒泡排序的核心思想是:相邻两元素之间进行比较。

如果有N个元素,需要比较N-1次,第一次,比较N-1次,第二次比较N-2次…最后一次比较1次即可。

所以是个等差数列,[项数*(a1+an)] / 2,所以就等于[n*(n-1)]/2。

所以O(N) = N^2。

2.3.7、二分查找时间复杂度的计算

在学习C语言中,学习了二分查找算法,它的底层数学知识就是log2 N。

比如有8=2**3个数据,进行二分查找。

转化为数学知识就是:log2 8 = 3。所以说悲观期望,二分查找最多执行3次。

所以说使用大O阶法表示,也分三种情况:

  • 最坏情况:O(log2 N)次找到。
  • 平均情况:O((log2 N)/2)找到。
  • 最好情况:O(1)次找到。

所以综上,使用悲观期望,二分查找的时间复杂度大O阶法表示为O(log2 N)。

这里补充一点,二分查找是个很nb的算法,数据越多它越nb,但是二分查找有个缺陷就是,只能针对有序数列进行查找,如果想要使用二分,前面是需要先排序,但是排序是很消耗性能的。

所以说以后要学:

  • 树—>二叉树—>搜索二叉树—>平衡二叉树—>AVL Tree/RB Tree。

  • 哈希表。

  • B树系列。

2.3.8、阶乘的时间复杂度

实例7:

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

分析:Fac一共调用了N次。

所以大O阶表示法:O(N)。

2.3.9、斐波那契的时间复杂度

实例8:

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

大O阶表示法:O(N) = 2^N

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

  • 递归次数:每一次只执行一次Fib,所以是O(1)。
  • 每次递归调用的次数:计算Fib(N),需要调用Fib(N-1)+FIB(N-2)。就类似的这个过程。

如下图分析:

在这里插入图片描述

可以发现这每一行的规律,它们是等比数列,然后减去省略的一部分x。

所以Fib(N) = 20+21+22+…+2(N-1)-x。

等比数列之和为:a1(1-q^n)/(1-q)

所以20+21+22+…+2(N-1) = (2^N) - 1。

又因为当N无限大时,减去x相当于没减。

所以最终Fib(N) = (2^N) - 1。

那使用大O阶表示为:O(N) = 2^N。

3、空间复杂度计算

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

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

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

注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数运行时显示申请的额外空间来确定。

3.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;
	}
}

//空间复杂度:O(1)。

分析:这里就看额外创建几个变量即可。

  • end变量是额外创建的。
  • i变量是额外创建的(i在一套冒泡排序完之后,++i,i还是占用同样的空间,所以整体下来i始终是一个变量)。
  • exchange变量是额外创建的。

所以N=3是常数,所以冒泡排序的空间复杂度用大O阶表示就是:O(1)。

3.2、计算斐波那契第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项个数的数组,所以需要计算N次。

  • fibArray计算N次。
  • 变量i也是额外创建的。

当N越来越大时,变量i可以忽略不计了。

所以此大O阶表示为:O(N)。

3.3、阶乘的空间复杂度计算

long long Fac(size_t N)
{
	if (N == 1)
		return 1;
	return Fac(N - 1) * N;
}

//空间复杂度:O(N)

分析:比如N=4,那就是4*3*2*1。

N=4时需要额外的空间。

N=3时需要额外的空间。

N=2时需要额外的空间。

N=1时需要额外的空间。

一共是N个栈帧的创建。

所以大O阶表示:O(N)。

3.4、斐波那契数列的空间复杂度计算

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

大O阶表示法:O(N)

斐波那契数列按理说时间复杂度和空间复杂度是一样的,都应该是O(N) = 2^N。

但是这里为什么空间复杂度时O(N)呢?如果空间复杂度是2^N,那么会栈溢出。

这是因为:

  • 空间是可以重复利用,不累计的。
  • 时间是一去不复返,累计的。

那这里空间是如何重复利用呢?如下图所示:

在这里插入图片描述

我们将具体的斐波那契数列执行过程细分来看其实是这样的:

在这里插入图片描述

这样其它的会重复使用这个空间,这里使用了N个空间,所以最多建立N个栈帧。

所以说我们也可以感知到了斐波那契数列的空间复杂度就是O(N)。

4、常见的复杂度对比

5201314O(1)常数阶
3n+4O(N)线性阶
3n^2+4n+5O(N^2)平方阶
3log(2)n+4O(longn)对数阶
2n+3nlog(2)n+14O(nlogn)nlogn阶
n3+2n2+4n+6O(N^3)立方阶
2^nO(2^N)指数阶
更多推荐

如何下载安装 WampServer 并结合 cpolar 内网穿透,轻松实现对本地服务的公网访问

文章目录前言1.WampServer下载安装2.WampServer启动3.安装cpolar内网穿透3.1注册账号3.2下载cpolar客户端3.3登录cpolarwebui管理界面3.4创建公网地址4.固定公网地址访问前言Wamp是一个Windows系统下的Apache+PHP+Mysql集成安装环境,是一组常用来搭

【操作系统笔记】并发安全问题

用户态抢占和内核态抢占内核中可以执行以下几种程序:①当前运行的进程:陷阱程序(系统调用)和故障程序(pagefault),进程运行在内核态的时候,其实就是在执行进程在用户态触发的异常对应的异常处理程序②中断处理程序③内核线程用户态线程抢占的调度时机检查当前线程是否需要被抢占的时机点(检查点):时钟中断发生,在时钟中断处

Mybatis的mapper接口实现原理

目录1概述2动态代理和反射对象3源码分析4总结1概述为啥mybatis的mapper只有接口没有实现类,但它却能工作?说起mybatis,大伙应该都用过,有些人甚至底层源码都看过了。在mybatis中,mapper接口是没有实现类的,取而代之的是一个xml文件。也就是说我们调用mapper接口,其实是使用了mapper

代理IP和Socks5代理:跨界电商与爬虫的智能引擎

跨界电商,作为全球市场的一部分,对数据的需求越来越大。同时,随着互联网的发展,爬虫技术也在不断演进,成为了跨界电商的关键工具之一。然而,随之而来的是网站的反爬虫机制和网络安全风险。在这种情况下,代理IP和Socks5代理应运而生,为企业提供了数据采集的解决方案和网络安全的保护。本文将深入研究代理IP和Socks5代理在

视频去LOGO的方法,AI自动完美地去除视频LOGO

喜欢做影视剧剪辑的朋友,可能会遇到下载的影视剧本身存在字幕、台标的情况,这些和新的剪辑主题不相符的原片元素,都会影响我们最终的成片效果。不过也无需烦恼哦,我们可以利用AI视频处理工具,自动去除视频中的logo或其它物体。只要利用好工具,想要去除视频中的logo是一件很简单的事情,AI抠图工具,只需导入需要处理的视频文件

ClickHouse面向列的数据库管理系统(原理简略理解)

目录官网什么是Clickhouse什么是OLAP面向列的数据库与面向行的数据库特点为什么面向列的数据库在OLAP场景中工作得更好为什么ClickHouse这么快真实的处理分析查询OLAP场景的关键属性引擎作用ClickHouse引擎输入/输出CPU官网https://clickhouse.com/什么是Clickhou

Kotlin | 在for、forEach循环中正确的使用break、continue

文章目录for循环中使用break、continueLabel标签forEach中如何退出循环资料Kotlin有三种结构化跳转表达式:return:默认从最直接包围它的函数或者匿名函数返回。break:终止最直接包围它的循环。continue:继续下一次最直接包围它的循环。for循环中使用break、continuef

智能指针介绍(C++)

前言关于智能指针大家或多或少都有听说过,因为在C++中没有GC,所以存在很多内存泄露的风险,所以基于RAII思想设计出了,智能指针,智能指针经过了很多个版本的迭代,从刚开始在C++98中推出了auto_ptr,但是auto_ptr不好用,它的设计存在重大缺陷,又因为C++的官方库更新的很慢,所以在接下来的n年中,没有改

openEuler 亮相全球顶级开源盛会 OSSUMMIT 2023,持续推动智能化未来的实现

2023年9月19日,全球顶级开源峰会OSSUMMITEU2023在西班牙-毕尔巴鄂正式开场。openEuler作为钻石级别赞助参会。这是openEuler继去年正式亮相后的第二次全面参加该峰会。本次会议,openEuler带来Keynote及多场分论坛演讲,涵盖LinuxKernel、编译器、AI、多样性计算、软件供

基于 CPU 在docker 中部署PaddleOCR

1.拉取镜像dockerpullregistry.baidubce.com/paddlepaddle/paddle:2.4.0注:写该文章时,Paddle最新版本为2.5.1,但是在实际安装中会出现与PaddleHub2.3.1版本的冲突,故采用2.4.0版本2.构建并进入容器dockerrun--namepaddle

在Vue中实现组件间的通信(父子通信,非父子通信,通用通信)

在vue中实现组件间的通信文章目录在vue中实现组件间的通信1、组件通信1.1、不同的组件关系和组件通信方案分类1.2、组件通信的解决方案1.3、非父子通信-eventbus事件总线2、prop2.1、prop详解2.2、prop校验2.3、prop&data、单向数据流3、v-mdoel原理1、组件通信概念:组件通信

热文推荐