[初阶数据结构】时间复杂度与空间复杂度

 

 前言

📚作者简介:爱编程的小马,正在学习C/C++,Linux及MySQL。

📚本文收录于初阶数据结构系列,本专栏主要是针对时间、空间复杂度,顺序表和链表、栈和队列、二叉树以及各类排序算法,持续更新!

📚相关专栏C++及Linux正在发展,敬请期待!

本文主要讲解时间复杂度以及空间复杂度,对大O渐进法会有比较详细的讲解,相信大家学完本文,会对复杂度有个很好的理解,那现在开始吧!

目录

 前言

1. 算法效率

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

1.2 算法的复杂度

2. 时间复杂度

2.1 时间复杂度的概念

2.2 大O的渐进表示法

 2.3 常见的时间复杂度计算举例

3. 空间复杂度 

4. 复杂度的OJ练习

4.1 消失的数字OJ链接:消失的数字 

4.2 轮转数组OJ链接 :轮转数组

总结


1. 算法效率

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

首先给大家看一段求斐波那契数列数:

int Feb(int n)
{
	if (n < 3)
		return 1;
	else
		return Feb(n - 1) + Feb(n - 2);
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Feb(n);
	printf("%d\n", ret);
	return 0;
}

斐波那契实现递归非常的简洁,但是简洁的代码一定好吗?简洁的代码执行效率就高吗?其实不是的,我可以告诉大家,这个递归的时间复杂度是O(2^n),比如我想求第50个斐波那契数,是很慢的。那我们如何衡量一个代码是好还是坏呢?

1.2 算法的复杂度

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

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

2. 时间复杂度

2.1 时间复杂度的概念

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

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

例如:
 

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", count);
}

Func1执行的基本操作次数:

F(N) = N^{_{2}}+2*N+10 

怎么样得到的这个函数表达是呢?挨个循环分析,首先第一个 i 和 j 控制的循环,是不是里外都是N次,求和就是N*N ,其次第二个k控制的循环,是不是2N次,最后是M控制的循环,是10次,时间是累计的,所以用加法加在一起就是上文代码的时间复杂度。

 实际上,我们不需要精确到这种程度,因为没有意义。只是需要大概的精确度,可以知道这个算法的时间复杂度是什么level就可以了。那么这就需要我们使用大O的渐进表示法

2.2 大O的渐进表示法

 大O符号:是用于描述函数渐进行为的数学符号。

推导大O阶的方法:

1、用常数1取代运行时间中的所有加法常数

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

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

上文函数算法如果使用大O阶的方法,那就是保留最高阶的函数,也就是时间复杂度为:               

F(N) = O(N)

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

例如:在长度为N的数组中搜索X

最好的情况,是不是第一个就找到X了,这个时候1次找到

平均情况,在中间找到X,这个时候是N/2次找到

最坏情况,遍历完一次数组,N次找到

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

 2.3 常见的时间复杂度计算举例

实例1:

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

计算Func2的时间复杂度,首先需要简单计算出数学表达式,K控制的循环有2N次,M控制的循环有10次,那时间复杂度函数就是:

F(N) = 2*N+10 

但是根据我们的大O表示法,F(N) = O(N) 

实例2:

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

计算Func3的时间复杂度,F(N) = M+N ,这个时候都不能省略,因为M对复杂度的影响和N一样,所以该算法的时间复杂度是 F(N) = O(M+N)

实例3:

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

计算Func4的时间复杂度,F(N) = 100 ,根据大O渐进法,常数都要置为1,所以该算法的时间复杂度是   F(N) = O(1)

实例4:

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

计算这个函数的时间复杂度? 找一个字符串,可能1次就找到了,也可能找了一半找到了,也可能找到最后一个找到了,也可能找不到了,上文我们提到,要考虑最坏的情况,所以N次查找,该函数的时间复杂度也就是   F(N) = O(N)

实例5:

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

这是一个升序的冒泡排序,BubbleSort的时间复杂度是多少?

先简单给大家介绍一下,在冒泡排序里有详细讲解,就是两两对比,你比我大我就往后移动,直到比完,最大的就到后面去了。第一次需要比n-1 ,第二次需要比n-2 次,直到最后1次

我们用等差数列求和来算一下时间复杂度

F(N) = \frac{N*(N-1))}{2}

那么根据大O渐进法,F(N) = O(N^2)

实例6:

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

这是二分查找法,那么如何求时间复杂度呢?实际上是这样的,就是我每查找到一次,就除以2对不对,那我查找X次找到了,是不是可以有这个表达式:

2^{n} = x   

 实际上,F(N)=\log_{2}x

所以,时间复杂度就是 f(N) = O(\log_{2}N)

实例7:

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

	return Fac(N - 1) * N;
}

这个Fac,我每调用一次函数,是不是都是常量个时间复杂度,那我调用了多少次函数?是N次,所以Fac的时间复杂度就是 F(n)= O(n)

实例8:

int Feb(int n)
{
	if (n < 3)
		return 1;
	else
		return Feb(n - 1) + Feb(n - 2);
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Feb(n);
	printf("%d\n", ret);
	return 0;
}

上文给大家埋下伏笔的这个代码,应该是如何求时间复杂度,和实例7一样,就是看函数的调用,画个图给大家看看:

实际上是等比数列的求和,为什么可以先求左边的,而不管右边开辟的函数,因为函数栈帧是可以重复利用的,实际上调用完就销毁了,留个下一个函数使用。所以斐波那契数列的时间复杂度表达式是:

F(N) =2 ^{_{}^{N-1}}-1(并没有经过二叉树的复杂计算,就是个估计值,等以后写了二叉树系列在和大家讲解详细的算法)

所以,斐波那契数的时间复杂度是:

F(N)=2^{N} 

3. 空间复杂度 

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

空间复杂度不是程序占用了多少字节的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟时间复杂度类似,也是用大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;
	}
}

如何计算呢?其实数组以及形式参数n已经是建立好的,不计入空间复杂度计算中。只有exchangde这个显式申请的额外空间需要计入。开辟了常数个额外空间,所以是O(1)

实例2:

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

实例3:

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

	return Fac(N - 1) * N;
}

可以看到,这里的额外空间主要就是函数的调用,那么其实函数栈帧的开辟是可以重复使用的,所以,这里的空间复杂度就是O(N) 

4. 复杂度的OJ练习

4.1 消失的数字OJ链接:消失的数字 

题目是这样的,数组nums包含从0n的所有整数,但其中缺了一个。 题目要求,要在O(N)时间内完成。那么我们最好想的是什么办法?先给数组排个序,排序了之后,让数组看看是不是后一个等于前一个+1,如果不是,那么上一个+1就是小时的数字,那么这个时间复杂度是多少呢?其实是

F(N)=O(n*logn),题意,那我再介绍两个方法。

1、异或法

原理:我把原数组中所有的数字进行异或,再讲0-n的所有整数进行异或,最后输出的就是消失的数字,为什么会这样呢?因为相同的就全部异或为0了,而保留下来的就是消失的数字,那这个时间复杂度是多少呢,实际上是O(2N-1)也就是O(N),看代码:

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

还有一种方法,对0-n的等差数列求和,求出来的值和缺失数字的数组挨个相减,即可得到消失的数字。看代码:

int missingNumber(int* nums, int numsSize)
{
    int sum = ((numsSize+1)*(numsSize))/2;
    for(int i = 0;i<numsSize;i++)
    {
        sum -= nums[i];
    }
    return sum;
}

4.2 轮转数组OJ链接 :轮转数组

比方说 1 2 3 4 5 6 7 ,旋转一次 7 1 2 3 4 5 6 ,旋转两次 , 6 7 1 2 3 4 5,可以看到,我们有一个方法,是不是每次把最后一个拿出来,所有数组往后移动,再插入到第一个,这个的时间复杂度是O(N^2),很慢。那么有没有比较快的方法,有,下面给大家介绍两个,第一,首先是很巧妙的方法,是翻转法。

1、翻转法:

我要移动3位  那我们就对前n-k个逆置 得到 4 3 2 1 5 6 7 再对k个之后的数字进行逆置,4 3 2 1 7 6 5  再整体逆置 得到 5 6 7 1 2 3 4 是不是很巧妙,我们来实现以下,看以下代码:

void reverse(int* nums,int left , int right)
{
    while(left<right)
    {
        int tmp = nums[left];
        nums[left]=nums[right];
        nums[right] = tmp;
        left++;
        right--;
    }
}
void rotate(int* nums, int numsSize, int k) 
{
    if(k>numsSize)
        k%=numsSize;
    reverse(nums,0,numsSize-k-1);
    reverse(nums,numsSize-k,numsSize-1);
    reverse(nums,0,numsSize-1);
}

还有一种方法,就是我们把前k个拷贝到新数组中去,再把后k个也拷贝到新数组中去,最后一起拷贝到数组中即可。

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

总结

1、时间复杂度和空间复杂度的概念

2、大O渐进法

3、大家一定要去做一下OJ题目,去看一看算法题应该怎么写。

如果这份博客对大家有帮助,希望各位给小马一个大大的点赞鼓励一下,如果喜欢,请收藏一下,谢谢大家!!!
制作不易,如果大家有什么疑问或给小马的意见,欢迎评论区留言。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

nuxt3使用记录六:禁用莫名其妙的Tailwind CSS(html文件大大减小)

发现这个问题是因为&#xff0c;今天我突然很好奇&#xff0c;我发现之前构建的自动产生的200.html和404.html足足290k&#xff0c;怎么这么大呢&#xff1f;不是很占用我带宽&#xff1f; 一个啥东西都没有的静态页面&#xff0c;凭啥这么大&#xff01;所以我就想着手动把他…

ICode国际青少年编程竞赛- Python-1级训练场-基础训练2

ICode国际青少年编程竞赛- Python-1级训练场-基础训练2 1、 a 4 # 变量a存储的数字是4 Dev.step(a) # 因为变量a的值是4&#xff0c;所以Dev.step(a)就相当于Dev.step(4)2、 a 1 # 变量a的值为1 for i in range(4):Dev.step(a)Dev.turnLeft()a a 1 # 变量a的值变为…

未来科技的前沿:深入探讨人工智能的进展、机器学习技术和未来趋势

文章目录 一、人工智能的定义和概述1. 人工智能的基本概念2. 人工智能的发展历史 二、技术深入&#xff1a;机器学习、深度学习和神经网络1. 机器学习2. 深度学习3. 神经网络 三、人工智能的主要目标和功能1. 自动化和效率提升2. 决策支持和风险管理3. 个性化服务和预测未来 本…

DHCPv4_CLIENT_ALLOCATING_01: 在其本地物理子网上广播DHCPDISCOVER消息

测试目的&#xff1a; 确保客户端能够在其本地物理子网上广播DHCPDISCOVER消息。 描述&#xff1a; 该测试用例旨在验证DHCP客户端是否能够正确地在其本地物理子网上广播DHCPDISCOVER消息&#xff0c;以便进行IP地址的自动分配。 测试拓扑&#xff1a; 测试步骤&#xff1a…

机器学习:深入解析SVM的核心概念【三、核函数】

核函数 **问题一&#xff1a;为什么说是有限维就一定存在高维空间可分呢&#xff1f;**原始空间与特征空间为什么映射到高维空间可以实现可分核函数的作用 **问题二&#xff1a;最终怎么得到函数**从对偶问题到决策函数的步骤&#xff1a;结论 **问题三&#xff1a;为什么说特征…

【Proteus】LED呼吸灯 直流电机调速

1.LED呼吸灯 #include <REGX51.H> sbit LEDP2^0; void delay(unsigned int t) {while(t--); } void main() {unsigned char time,i;while(1){for(time0;time<100;time){for(i0;i<20;i){LED0;delay(time);LED1;delay(100-time);}}for(time100;time>0;time--){fo…

003 redis分布式锁 jedis分布式锁 Redisson分布式锁 分段锁

文章目录 Redis分布式锁原理1.使用set的命令时&#xff0c;同时设置过期时间2.使用lua脚本&#xff0c;将加锁的命令放在lua脚本中原子性的执行 Jedis分布式锁实现pom.xmlRedisCommandLock.javaRedisCommandLockTest.java 锁过期问题1乐观锁方式&#xff0c;增加版本号(增加版本…

GPT-1

GPT 系列是 OpenAI 的一系列预训练模型&#xff0c;GPT 的全称是 Generative Pre-Trained Transformer&#xff0c;顾名思义&#xff0c;GPT 的目标是通过 Transformer&#xff0c;使用预训练技术得到通用的语言模型。目前已经公布论文的有 GPT-1、GPT-2、GPT-3。 最近非常火的…

腾讯云ubuntu新建用户后,命令行只显示$

这是因为&#xff0c;新建用户命令行解释器默认是sh&#xff0c;需要手动切换为bash&#xff0c;bash可以认为是sh的加强版本。 所以我们只需要将&#xff0c;shell切换为bash就好了。 切换到root 修改配置文件 vim/etc/bash 将sh修改为bash

AcWing 3194:最大的矩形 ← 笛卡尔树

【题目来源】https://www.acwing.com/problem/content/3197/【题目描述】 在横轴上放了 n 个相邻的矩形&#xff0c;每个矩形的宽度是 1&#xff0c;而第 i&#xff08;1≤i≤n&#xff09;个矩形的高度是 hi。 这 n 个矩形构成了一个直方图。 例如&#xff0c;下图中六个矩形的…

类和对象【四】运算符重载

文章目录 运算符重载的概念运算符重载&#xff08;函数&#xff09;返回值类型&#xff1a;任意类型函数名&#xff1a;operator已有操作符 运算符重载&#xff08;函数&#xff09;的特点和注意点3个比较特殊的运算符重载赋值运算符&#xff08;&#xff09;重载返回值类型和返…

Linux CentOS7部署ASP.NET Core应用程序,并配置Nginx反向代理服务器和Supervisor守护服务

前言&#xff1a; 本篇文章主要讲解的是如何在Linux CentOS7操作系统搭建.NET Core运行环境并发布ASP.NET Core应用程序&#xff0c;以及配置Nginx反向代理服务器。因为公司的项目一直都是托管在Window服务器IIS上&#xff0c;对于Linux服务器上托管.NET Core项目十分好奇。因为…

简单学生信息管理系统

简单&#xff0c;单表&#xff1b; https://download.csdn.net/download/bcbobo21cn/89251742

【QT学习】12.UDP协议,广播,组播

一。Udp详细解释 UDP&#xff08;User Datagram Protocol&#xff09;是一种无连接的传输层协议&#xff0c;它提供了一种简单的、不可靠的数据传输服务。与TCP相比&#xff0c;UDP不提供可靠性、流量控制、拥塞控制和错误恢复等功能&#xff0c;但由于其简单性和低开销&#x…

Java | Leetcode Java题解之第64题最小路径和

题目&#xff1a; 题解&#xff1a; class Solution {public int minPathSum(int[][] grid) {if (grid null || grid.length 0 || grid[0].length 0) {return 0;}int rows grid.length, columns grid[0].length;int[][] dp new int[rows][columns];dp[0][0] grid[0][0]…

《罪与罚》读后感

陀思妥耶夫斯基和列夫托尔斯泰是公认的俄国文学黄金时代的两座高峰&#xff0c;分别代表着俄国文学的“深度”和“广度”。列夫托尔斯泰的鸿篇巨著《复活》《安娜卡列尼娜》等等都已经拜读过&#xff0c;但陀思妥耶夫斯基的作品却一本也没有看过&#xff0c;实在是有点遗憾。这…

LabVIEW换智能仿真三相电能表研制

LabVIEW换智能仿真三相电能表研制 在当前电力工业飞速发展的背景下&#xff0c;确保电能计量的准确性与公正性变得尤为重要。本文提出了一种基于LabVIEW和单片机技术&#xff0c;具有灵活状态切换功能的智能仿真三相电能表&#xff0c;旨在通过技术创新提高电能计量人员的培训…

Flutter笔记:谈Material状态属性-为什么FlatButton等旧版按钮就废弃了

Flutter笔记 谈Material状态属性-为什么FlatButton等旧版按钮就废弃了 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this artic…

MySQL-逻辑架构

1、MySQL服务器处理客户端请求 MySQL是典型的C/S架构&#xff0c;服务端程序使用 mysqld。实现效果&#xff1a;客户端进程像服务端发送&#xff08;SQL语句&#xff09;&#xff0c;服务器进程处理后再像客户端进程发送 处理结果。 2、connectors 指不同语言中与SQL的交互…

Vue3+ts(day05:ref、props、生命周期、hook)

学习源码可以看我的个人前端学习笔记 (github.com):qdxzw/frontlearningNotes 觉得有帮助的同学&#xff0c;可以点心心支持一下哈&#xff08;笔记是根据b站上学习的尚硅谷的前端视频【张天禹老师】&#xff0c;记录一下学习笔记&#xff0c;用于自己复盘&#xff0c;有需要学…