【数据结构】时间复杂度详解

news2024/10/5 15:23:13

首先我们要知道学习数据结构时都会讨论到算法,数据结构中的问题多数都有算法解决,两者是你中有我,我中有你的关系,所以在数据结构中的学习中算法也是必不可少的。

为方便阅读,以下为本片目录

目录

1.算法效率

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

1.2算法的复杂度

2.时间复杂度

 2.1 时间复杂度的概念

应用题


1.算法效率

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

我们用我们所学过的函数的递归中的一道题来引出今天所要分享的内容

如下是关于斐波那契数列用函数递归的方法实现

long long Fib(int N)
{
    if(N < 3)
        return 1;

    return Fib(N-1) + Fib(N-2);
}

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

1.2算法的复杂度

算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。我们今天要研究的是时间复杂度方面的问题

假如我们将冒泡排序分别在一台i3的cpu的电脑上,内存只有1g,和一台i9的cpu的电脑上,内存有16个g的电脑,很显然后者的运行时间更短,运行速度也更快,所以影响到时间复杂度不只是有代码本身,还有运行代码的硬件环境有关。

2.时间复杂度

 2.1 时间复杂度的概念


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

我们接下来一一举例说明。

实例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);
}

我们要要计算运行了多少次,不妨将次数设为N

我们观察到第一个for循环是嵌套的形式,所以不难看出一共运行了N*N次;

我们再看第二个for循环,不难看出在第二个表达式中运行了2*N次

再观察最后一个表达式,不难看出while循环执行了十次;

所以他的时间复杂度函数式:F(N)=N*N+2*N+10

但是在实际问题中我们关注关于时间复杂度的问题重点并不是看它算了多少次,而是将这些次数分成等级,我们关注的是他在时间复杂度中的等级

就像我们所说的亿万富翁,千万富翁,百万富翁一样,我们根本不需要知道他们的总资产有多少,而是根据这些级别就可以知道这些人的地位。所以在时间复杂度中我们不用关心它具体的运算次数,而是看这些复杂度中划分的等级。

所以在这里我们用大O表示渐进表示法,对他们进行估算。

那么上面的时间复杂度函数式F(N)=N*N+2*N+10我们对他估算时要看哪一项呢?我们带值进去看看

N = 10 F(N) = 130
N = 100 F(N) = 10210
N = 1000 F(N) = 1002010

我们发现N*N这一项对整个公式的结果影响最大,当N越大时对最后的结果最小,如果N无限大时,后面的数基本上都可以忽略不计,所以我们在估算时不妨只看最大项。

所以我们将这个代码的时间复杂度函数式可以表达为:O(N^2)


实例2

int main()
{
	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);
	}
	return 0;
}

可以再想想这样的时间复杂度表达式是什么呢?

是的,答案是O(M+N)

在这个表达式中有两个变量分别是M和N,这两个变量我们都要注意到,我们不可能只在乎N的值,而不在乎M的值,两个变量之间是没有联系的,这里的时间复杂度是O(M+N)。

实例3

void Func4(int N)
	{
		int count = 0;
		for (int k = 0; k < 100; ++k)
		{
			++count;
		}
		printf("%d\n", count);
	}

可以继续猜测一下这里的时间复杂度表达式是什么

结果是O(1);而不是O(100)

在这里我们需要注意的是给了一个参数N,但是在函数体中没有用到,那为什么时间复杂度是O(1)呢?

首先我们要清楚我们所要估算的是时间复杂度,O(1)不代表只计算一次,而是代表计算的是常数次,也代表他的计算时间是一个固定值;所以在上面的for循环中,表达式2的上限只要是一个确定的常数,那他的时间复杂度就是O(1),不管他放了多大的数字,这里的时间复杂度都是O(1)。

那为什么呢?答案是因为我们现在电脑所使用的CPU非常强大,运算能力非常强,眨眼的功夫就可以计算几十亿次,在CPU眼里,我们所写的常数次的运算都是渣渣,所以只要我们写常数运算,他的时间复杂读都是O(1).

实例4

再来看下面的这个例子

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);
	}

通过上面的几个简单的实例来观察这个实例,我们可以把后面是个循环省略掉,那就只剩for循环中的2*N了,那他的时间复杂度就是2*N吗?

答案是,他的时间复杂度还是O(N);

因为2*N中N前面的系数也可以省略掉。我们想象一下,当N无限大的时候,N和2N甚至是5N、100N时前面的系数都是没有意义的,N已经是无限大了,所以我们在估算他的时间复杂度时,还是记作O(N);

实例5

接下来我们加一点难度 

计算strchr的时间复杂度

const char * strchr ( const char * str, int character );

其实也非常的简单,它大致的写法如下

while (*str)
	{
		if (*str = character)
			return str;
		else
			++str;
	}

这串代码的意思就是想要在一串字符串中找到我们想要的字符,也就是要遍历字符串。

关于寻找字符这件事是一件不确定的事,我们想要的字符可能出现在字符串的开头,也可能出现在字符串的结尾,这样寻找字符就变成了一件碰运气的事,这样看来他的时间复杂度也是不能够确定的,我们将它分为三种情况,最好、最坏和平均年情况。

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

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

也就是说可能是常数次就可以找到我们想要的字符,也有可能在字符串的末尾才能找到,

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

因为我们也无法确定在什么时候找到,所以我们要更加保守的计算,要有底线思维,这样可以降低我们的预期,让我们做最坏的打算。

实例6

难度继续提升

我们不妨来看看之前学过的冒泡排序的时间复杂度

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个数,第一个数要比较N-1次才能确定他的位置;第二个数就是N-2次才能确定他的位置,以此类推N-3、N-4、………… 3 、2、1我们会发现比较的次数是一个等差数列,等差数列就有公式:首相加末项乘以项数除以2,这样想我们的公式就是N*(N-1)/2。

通过上面的公式我们会发现最高阶的项还是N*N,也就是N^2,除以2可以看作乘以1/2,前面说到的系数也可以忽略不计,所以时间复杂度就是O(N^2)

实例7

我们不妨继续加上一点难度,来观察一下二分查找的时间复杂度是多少

int BinarySearch(int* a, int n, int x)
	{
		assert(a);
		int begin = 0;
		int end = n - 1;
		// [begin, end]:begin和end是左闭右闭区间,因此有=号
		while (begin <= end)
		{
			int mid = begin + ((end - begin) >> 1);
			if (a[mid] < x)
				begin = mid + 1;
			else if (a[mid] > x)
				end = mid - 1;
			else
				return mid;
		}
		return -1;
	}

首先我们要明白二分查找是怎么实现的。

二分查找是不断地缩小区间来查找目标,要算时间复杂度依然要最坏的打算。

第一次查找不到时会将范围缩小至一半,继续查找;如果第二次查找不到,继续讲范围缩小一半,继续查找,就这样以此类推,每次将查找到范围除以2

每找一次就除以2,我们不妨理解为找了多少次,就除以多少次2

那假设找了X次,就除了X次2,我们反过来看就是2^X=N,

X=log2  N

所以二分查找的时间复杂度就是O(log2  N),希望大家能够理解。

了解完二分查找的时间复杂度后我们不妨再和之前的暴力查找进行对比。

我们上面说提到的顺序查找的时间复杂度是O(N),而二分查找的时间复杂度是O(log2  N),二者再时间上的差异还是很大的,我们可以给N带入具体的数值。

加入我们有一百万个数据交给暴力查找和二分查找,暴力查找要计算一百万次,而二分查找仅仅计算20次即可;

我们通过计算器就可以看到二分查找在计算20次时已经查找了一百万多次的数据,我们以后还需要学很多的数据结构和算法,他们都有不同的使用场景和环境,希望大家可以理解以上的实例。

实例8

我么继续为大家分享几个例子

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

我们继续观察上方的代码时间复杂度是多少呢?

需要注意的是这个代码中没有了循环的使用,取而代之的是函数递归,所以时间复杂度是O(N)。

我们会发现如果是调用函数的话,就不难发现F(N-1)就会调用F(N-2),F(N-2)又会调用F(N-3),就这样以此类推,直到调用到最后的F(0),最后得出结果。

这样的每次调用就会发现调用N次就可以结束,递归不能只考虑当前函数,还需要考虑函数中的子函数,所以他的时间复杂度就是O(N);

实例9

了解上述内容之后我们不妨再加深一点难度

long long Fac(size_t N)
	{
		if (0 == N)
			return 1;

        for(size_t i=0;i<N;i++)
    {
            //.....
    }
		return Fac(N - 1) * N;
	}

我们就在刚刚的代码中加上一个for循环,这样还能发现时间复杂度吗?

在这里我们每调用一次函数就要将调用的时间加起来,因为所消耗的时间都是相加的,也可以理解为一个等差数列;

我们再来观察for循环中的N,和下面的函数i递归是一样的,N是不断变化的,所以每次循环的次数我们都要加起来,同样也可以看作一个等差数列,所以我们上面所说的等差数列的时间复杂度就是O(N^2);

应用题

了解完以上实例后我们不妨做一道题再来了解一下时间复杂度

面试题 17.04. 消失的数字 - 力扣(LeetCode)

以上是题目链接

首先我们得要确定解题思路:

第一种方法:首先是排序,依次查找,如果下一个数不是上一个数+1的值,那么上一个数+1就是消失的数字

在这里使用qsort来将这个数组排序,qsort的时间复杂度就是O(N*log2  N);

寻找也需要花费时间,那寻找数字的时间复杂度就是O(N)

我们将其合并起来总共花费的时间就是O(N*log2  N+N)

但是我们需要注意的是可以将后面的+N省略掉,对整个式子影响最大的还是前面的那一项。

第二种方法:可以通过两个数相互异或的方法,相异为1,相同为0,两个相同的数字异或就无了,所以我们可以设消失的数字为x,并使x=0;因为0和任何数相异或都为任何数,然后只需要用循环来控制这个全部的遍历的异或,用循环控制就可以使这个时间复杂度为O(N);

int missingNumber(int* nums, int numsSize){
    int x=0;
    for(int i=0;i<numsSize;++i)
    {
        x^=nums[i];
    }

    for(int i=0;i<numsSize+1;++i)
    {
        x^=i;
    }
    return x;

}

 那我们就选择用异或的方式来解决问题,相信大家能够理解。

以上就是本片要分享的关于时间复杂度的内容,希望大家有所收获,如果对你有帮助不妨三连支持一下,感谢您的阅读。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/413353.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaScript的学习理解

文章目录一、JavaScript 对象二、JavaScript 函数三、JavaScript 作用域总结一、JavaScript 对象 JavaScript 对象是拥有属性和方法的数据。 真实生活中的对象&#xff0c;属性和方法 在 JavaScript中&#xff0c;几乎所有的事物都是对象。 在 JavaScript 中&#xff0c;对象…

Spring5学习笔记01

一、课程介绍 Spring是什么呢&#xff1f; 它是一个轻量级的、开源的JavaEE框架&#xff0c;它的出现是为了解决企业繁琐的开发包括复杂代码&#xff0c;它可以用很优雅、很简洁的方式进行实现&#xff0c;也就是说它为了简化企业开发而生&#xff0c;而它在目前的企业中应用…

大规模MySQL运维陷阱之基于MyCat的伪分布式架构

引子 分布式数据库&#xff0c;已经进入了全面快速发展阶段&#xff0c;这种发展&#xff0c;是与时俱进的&#xff0c;与人的需求是分不开的&#xff0c;因为现在信息时代的高速发展&#xff0c;导致数据量和交易量越来越大。这种现象首先导致的就是存储瓶颈&#xff0c;因为…

(排序10)归并排序的外排序应用(文件排序)

TIPS 在一些文件操作函数当中&#xff0c;fputc与fgetc这两个函数都是针对字符的&#xff0c;如果说你需要往文件里面去放入整形啊等等&#xff0c;不是字符的类型&#xff0c;这时候就用fprintf&#xff0c;fscanf在参数里面数据类型控制一下就可以。但是话说回来&#xff0c…

自动化测试怎么学?这绝对是全网最系统的教程

目录 1、什么是自动化测试 2、自动化测试的发展前景怎么样 3、自动化测试难不难&#xff1f; 4、目前市场上自动化测试岗位的薪资是多少&#xff1f; 5、自动化测试学习方法好渠道 6、自动化测试怎么学&#xff1f; 学习基础知识 选择自动化测试框架 开始编写测试脚本 …

用HTTP proxy module配置一个反向代理服务器

反向代理与正向代理 摘抄&#xff1a;https://cloud.tencent.com/developer/article/1418457 正向代理 正向代理&#xff08;forward proxy&#xff09;&#xff1a;是一个位于客户端和目标服务器之间的服务器(代理服务器)&#xff0c;为了从目标服务器取得内容&#xff0c;…

“数实融合 元力觉醒”,苏州市元宇宙生态大会圆满召开!

为贯彻落实《苏州市培育元宇宙产业创新发展指导意见》&#xff0c;抢抓数字经济发展新机遇&#xff0c;加速培育与元宇宙发展相关的技术底座&#xff0c;“数实融合 元力觉醒——苏州市软件行业协会元宇宙专委会成立大会暨元宇宙生态大会”于4月14日成功举办。 苏州和数智能软件…

五金件装备不良、视觉检测零件是否缺失硬件方案

【检测目的】 检测不良品 【检测要求】 检测速度&#xff1a;13S一个 【拍摄效果图一】&#xff08;正面&#xff09; 【拍摄效果图二】正面 【拍摄效果图三】正面 【拍摄效果图四】&#xff08;正面&#xff09; 【拍摄效果图五】&#xff08;正面&#xff09; 【拍摄效果图…

如何写好付费专栏之开宗明义篇

大家好,我是herosunly。985院校硕士毕业,现担任算法研究员一职,热衷于机器学习算法研究与应用。曾获得阿里云天池比赛第一名,CCF比赛第二名,科大讯飞比赛第三名。拥有多项发明专利。对机器学习和深度学习拥有自己独到的见解。 本文主要介绍了写好付费专栏的开宗明义篇,希…

电脑上删除的文件可以恢复吗 如何恢复电脑上删除的文件

电脑早已走进千家万户&#xff0c;成为我们不可或缺的家庭设备&#xff0c;我们用电脑来学习、工作&#xff0c;处理各种数据。在使用电脑处理数据时&#xff0c;可能会失误操作&#xff0c;删除重要文件。那么&#xff0c;电脑上删除的文件可以恢复吗&#xff0c;如何恢复电脑…

Python学习笔记--函数进阶

&#xff08;一&#xff09; 函数多返回值 按照返回值的顺序&#xff0c;写对顺序的多个变量接收即可变量之间用逗号隔开支持不同类型的数据return def test_return():return 1,2x,y test_return() print(x) print(y)&#xff08;二&#xff09; 函数的多种传参方式 函数参数…

MySQL批量更新的常用实践

MySQL批量更新的常用实践 批量更新一般在批处理系统或者定时任务中比较常见&#xff0c;常见的诉求就是对表中多条数据进行更新&#xff08;待更新的值是不一样的&#xff0c;这个区别于update … where in(…)&#xff09; 1.利用case … when … 方式批量更新 特点&#x…

5年碌碌无为,我终于从功能测试转到了自动化测试,薪资暴涨8K......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 自动化测试现已悄然…

JavaEE企业级应用开发教程——第十二章 Spring MVC数据绑定和相应(黑马程序员第二版)(SSM)

第十二章 Spring MVC数据绑定和相应 12.1 数据绑定 在 Spring MVC 中&#xff0c;当接收到客户端的请求时&#xff0c;会根据请求参数和请求头等信息&#xff0c;将参数以特定的方式转换并绑定到处理器的形参中&#xff0c;这个过程称为数据绑定。数据绑定的流程大致如下&…

Golang每日一练(leetDay0035) 二叉树专题(4)

目录 103. 二叉树的锯齿形层序遍历 Binary Tree Zigzag Level Order Traversal &#x1f31f;&#x1f31f; 104. 二叉树的最大深度 Maximum Depth of Binary-tree] &#x1f31f; 105. 从前序与中序遍历序列构造二叉树 Construct-binary-tree-from-preorder-and-inorder-…

MySQL8.0的安装和配置

&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了 博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳,欢迎大佬指点!人生格言&#xff1a;当你的才华撑不起你的野心的时候,你就应该静下心来学习! 欢迎志同道合的朋友一起加油喔&#x1f9be;&am…

结合实际谈谈个人对代码优化的感想以及java优化

前言 本来想写一篇结合在实际工作中&#xff0c;自己去优化java代码的文章&#xff0c;用于记录便于复习提升自己的&#xff1b;但是在回想起自己在实际工作中诸多因素导致存在的问题&#xff08;仅针对我个人&#xff09;&#xff0c;个人总结以及去证实了&#xff0c;所悟&am…

16. unity粒子特效---旋转 + 花瓣飞舞案例

1. 旋转模块&#xff08;Rotation over Lifetime&#xff09; 在主模块中也可以设置粒子的旋转角度&#xff0c;通过参数Start Rotation&#xff0c;不过这个参数设置的是粒子刚生成时的角度&#xff0c;后面不会发生变化。 使用旋转模块可以通过参数Angular Velocity进行设置…

十一、删除市场活动

功能需求 ①用户在市场活动主页面,选择要删除的市场活动,点击"删除"按钮,弹出确认窗口; ②用户点击"确定"按钮,完成删除市场活动的功能. ③*每次至少删除一条市场活动 ④*可以批量删除市场活动 ⑤*删除成功之后,刷新市场活动列表,显示第一页数据,保持…

如何规划自己的大一生活

大家好&#xff0c;我是帅地&#xff0c;在帅地的训练营里&#xff0c;有不少大一打二大学员&#xff0c;不少学员在大一就会数据结构&#xff0c;算法等学了&#xff0c;还参加了一些实验室项目&#xff0c;这主要得益于他们规划等早。 帅地在接下来的时间里&#xff0c;会写…