草庐IT

【数据结构】第一站:复杂度

青色_忘川 2023-04-05 原文

目录

一、算法效率

1.如何衡量一个算法的好坏

2.算法的复杂度

二、时间复杂度

1.时间复杂度概念

2.大O渐进表示法

3.关于时间复杂度的一些计算

三、空间复杂度

四、常见复杂度对比

五、两道经典的题目

1.消失的数字

2.轮转数组


一、算法效率

1.如何衡量一个算法的好坏

对于一个算法,我们想要衡量他的效率

我们有两个方向可以去衡量,时间和空间

但是要注意,这里的时间并不是绝对的时间。因为对于同一个算法,可能会由于硬件设施的不同而导致运行时间的不同。因此不能简单的将绝对时间作为算法效率的衡量标准。对于时间而言,我们衡量的是算法的大概的执行次数

2.算法的复杂度

算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。

二、时间复杂度

1.时间复杂度概念

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

如下面的例子

// 请计算一下Func1中++count语句总共执行了多少次?
#include<stdio.h>
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);
}

对于这个函数而言,我们不难得出他的运行次数为f(N)=N²+2N+10

但是这个过于繁琐了。我们不需要那么精确,我们抓大头,当n很大的时候,影响最大的一项就是我们最需要关注的一项。即最高次

所以这个运行次数我们简记为N²

为此我们也引出了大O渐进表示法,这个函数的大O渐进表示法为O(N²)

2.大O渐进表示法

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

如上题的大O渐进表示法结果为O(N²)

3.关于时间复杂度的一些计算

例一:

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

显然精确的次数为2N+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);
}

精确次数为M+N次

所以大O渐进表示形式为O(M+N),如果M>>N,那么可以近似为O(M),反之近似为O(N)

例三:

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

精确次数为100次

他的大O渐进表示为O(1),注意这里的1并不是一次,而是常数次

例四:

// 计算strchr的时间复杂度?
const char* strchr(const char* str, int character);

这个其实是一个库函数,他的作用是从字符串中找出对应的字符,他的实现是通过依次遍历来完成的

既然是通过遍历来寻找,那么我们就发现,他有三种特殊情况:最好、最坏和平均

他的最好次数为:O(1),即只需要一次就找到了

最坏情况是:O(N),也就是最后一次才找到

平均情况为:N/2次,即O(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;
	}
}

这个也是一种冒牌排序算法,但是要注意这个冒泡排序与我们之前写的有一点不同,我们加了一个break来进行控制,这样的话

对于最好的情况,也就是顺序的情况,他的次数为O(N),因为他有一个exchange来进行控制,如果他是顺序,那么第一次循环的时候,就会由于exchange没有被赋为1,导致外层循环直接被跳出,仅仅只有内层循环执行了N-1次,所以他的时间复杂度为O(N)

最坏情况:也就是彻底的逆序,那么就需要执行N-1 +N-2 +.....+1,即O(N²)

所以他的时间复杂度为O(N²)

但是假如说没有了这个exchange来控制的话,那么无论最好还是最坏都是O(N²)

例六:

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

这是一个二分查找

最好情况当然是O(1)了,一次就找到了

最坏情况就是O(logN)次,原因是,二分查找是每次缩小一半去查找的,最后的一个区间长度是1。因此我们逆着来思考,1*2*2....*2=N。就是我们的方程,每查找一次就乘以2,即方程又可化为2^X=N,所以X=log(2,N),意思是以2 为底

为了方便简写,我们一般默认以2为底时可以省略这个底数

例七:

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

这道题从栈帧的角度去理解,他开辟了多少次函数的栈帧,显然调用F(N)就需要调用F(N-1).......一直调用到F(0),所以时间复杂度为O(N)

例八:

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

如上图所示,他是一个不断递归调用的过程,那么就需要画出他的图解。第一层是2^0次方,第二层是2^1次方次......,当然后面的圆圈处又一些会提前结束,但是我们可以忽略不记。近似认为是补全的。根据等比数列求和可解得时间复杂度为O(2^N)

三、空间复杂度

空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度。空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定

例一:

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

对于这个冒泡排序,他所额外占用的变量数是3个,所以空间复杂度为O(1)

例二:

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

显然空间复杂度为O(N)

例三:

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

 

 如上图所示,是这个函数的栈帧图,显然是O(N)

例四:

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

对于这个斐波那契算法的空间复杂度,其实很容易陷入误区,误以为是O(2^N),其实不然。他的空间复杂度是O(N)

这是因为当函数递归到最后一层的时候,又会进行销毁这一层的栈帧,将空间还给操作系统,然后操作系统又将空间给了下一个栈帧。他的栈帧调用图如下所示

 他始终只有这O(N)的空间在反复利用

四、常见复杂度对比

如下是一些常见的时间复杂度

 

 

五、两道经典的题目

1.消失的数字

题目链接:力扣

法1:排序加二分查找

排序加二分查找

这种方法的时间复杂度其实是不满足条件的

法2:异或

将整个数组进行异或

然后将1到n与这个数组进行异或,最终的结果就是缺失的数字



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

法3:等差数列公式

先将整个数组求和,然后将从1~n进行求和

两个和相减即可

2.轮转数组

题目链接:力扣

法1:依次移动每个元素

先写一个可以轮转一次的函数,然后依次轮转k次

时间复杂度为O(K*N)

最坏情况需要轮转N-1次,时间复杂度为O(N²)

当然这种方法的时间复杂度太大了,对于这道题而言是无法通过的

法2:三次逆置

先逆置前n-k个数据

然后逆置后k个数据

最后整体逆置

void reverse(int* a, int begin, int end)
{
    while (begin < end)
    {
        int tmp = a[begin];
        a[begin] = a[end];
        a[end] = tmp;

        begin++;
        end--;
    }
}
void rotate(int* nums, int numsSize, int k) {
    if (k >= numsSize)
    {
        k = k % numsSize;
    }
    reverse(nums, 0, numsSize - 1 - k);
    reverse(nums, numsSize - k, numsSize - 1);
    reverse(nums, 0, numsSize - 1);
}

他的时间复杂度是O(N)

空间复杂度是O(1)

法3:以空间换时间

这种方法就是将后k个数据拷贝到一个新的数组中

然后将前n-k个数据拷贝到新数组的后面

最后在将新数组的数据整体拷贝到新的数组中

void rotate(int* nums, int numsSize, int k){
    k=k%numsSize;
    int* a=(int*)malloc(sizeof(int)*numsSize);
    memcpy(a,nums+numsSize-k,k*sizeof(int));
    memcpy(a+k,nums,(numsSize-k)*sizeof(int));
    memcpy(nums,a,numsSize*sizeof(int));
    free(a);
}

时间复杂度和空间复杂度都是O(N)


好了本期内容就到这里

如果对你有帮助的话,不要忘记点赞加收藏哦!!!

有关【数据结构】第一站:复杂度的更多相关文章

  1. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

    我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

  2. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  3. ruby - Ruby 有 `Pair` 数据类型吗? - 2

    有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳

  4. ruby - 是否有用于序列化和反序列化各种格式的对象层次结构的模式? - 2

    给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最

  5. ruby - 我如何添加二进制数据来遏制 POST - 2

    我正在尝试使用Curbgem执行以下POST以解析云curl-XPOST\-H"X-Parse-Application-Id:PARSE_APP_ID"\-H"X-Parse-REST-API-Key:PARSE_API_KEY"\-H"Content-Type:image/jpeg"\--data-binary'@myPicture.jpg'\https://api.parse.com/1/files/pic.jpg用这个:curl=Curl::Easy.new("https://api.parse.com/1/files/lion.jpg")curl.multipart_form_

  6. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  7. FOHEART H1数据手套驱动Optitrack光学动捕双手运动(Unity3D) - 2

    本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01  客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02  数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit

  8. 使用canal同步MySQL数据到ES - 2

    文章目录一、概述简介原理模块二、配置Mysql使用版本环境要求1.操作系统2.mysql要求三、配置canal-server离线下载在线下载上传解压修改配置单机配置集群配置分库分表配置1.修改全局配置2.实例配置垂直分库水平分库3.修改group-instance.xml4.启动监听四、配置canal-adapter1修改启动配置2配置映射文件3启动ES数据同步查询所有订阅同步数据同步开关启动4.验证五、配置canal-admin一、概述简介canal是Alibaba旗下的一款开源项目,Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。Git地址:https://github.co

  9. ruby-on-rails - 创建 ruby​​ 数据库时惰性符号绑定(bind)失败 - 2

    我正在尝试在Rails上安装ruby​​,到目前为止一切都已安装,但是当我尝试使用rakedb:create创建数据库时,我收到一个奇怪的错误:dyld:lazysymbolbindingfailed:Symbolnotfound:_mysql_get_client_infoReferencedfrom:/Library/Ruby/Gems/1.8/gems/mysql2-0.3.11/lib/mysql2/mysql2.bundleExpectedin:flatnamespacedyld:Symbolnotfound:_mysql_get_client_infoReferencedf

  10. STM32读取串口传感器数据(颗粒物传感器,主动上传) - 2

    文章目录1.开发板选择*用到的资源2.串口通信(个人理解)3.代码分析(注释比较详细)1.主函数2.串口1配置3.串口2配置以及中断函数4.注意问题5.源码链接1.开发板选择我用的是STM32F103RCT6的板子,不过代码大概在F103系列的板子上都可以运行,我试过在野火103的霸道板上也可以,主要看一下串口对应的引脚一不一样就行了,不一样的就更改一下。*用到的资源keil5软件这里用到了两个串口资源,采集数据一个,串口通信一个,板子对应引脚如下:串口1,TX:PA9,RX:PA10串口2,TX:PA2,RX:PA32.串口通信(个人理解)我就从串口采集传感器数据这个过程说一下我自己的理解,

随机推荐