【C语言数据结构(基础篇)】第一站:时间复杂度与空间复杂度

news2025/1/11 17:59:03

目录

一、什么是时间复杂度和空间复杂度

1.算法效率

2.时间复杂度的概念

3.空间复杂度的概念

二、如何计算常见的时间复杂度

1.大O的渐进表示法

2.一些时间复杂度的例子

(1)例1

(2)例2

(3)例3

(4)例4

(5)例5

(6)例6

(7)例7

3.总结

三、常见空间复杂度的计算

1.例1

2.例2

3.例3

四、有复杂度要求的算法题练习

1.消失的数字

(1)思路一

(2)思路二

(3)思路三

 2.轮转数组

 (1)思路一

(2)思路二

(3)思路三

总结


一、什么是时间复杂度和空间复杂度

1.算法效率

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

2.时间复杂度的概念

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

3.空间复杂度的概念

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

二、如何计算常见的时间复杂度

1.大O的渐进表示法

我们看这样一段代码,并分析他的时间复杂度

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平方+2n+10,但是呢,我们一般说时间复杂度的时候不会这么精确,因为随着n的增大,这个表达式的结果中n平方项的影响最大

例如

N = 10         F(N) = 130

N = 100       F(N) = 10210

N = 1000     F(N) = 1002010

......

因此,时间复杂度是一个估算,是去看表达式中影响最大的一项 ,本题的时间复杂度为o(n^{2}

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

推导大O阶方法:

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

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

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

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

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

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

平均情况:任意输入规模的期望运行次数

最好情况:任意输入规模的最小运行次数(下界)

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

最好情况:1次找到

最坏情况:N次找到

平均情况:N/2次找到

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

2.一些时间复杂度的例子

(1)例1

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(n)

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

他的时间复杂度为o(M+N),但是如果题目说了M远大于N,那么时间复杂度为O(M)

(3)例3

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

他的时间复杂度为O(1),因为他的时间是固定,不会随着n的增大而改变。他是一个常数,他的时间不变

(4)例4

// 计算strchr的时间复杂度?
const char* strchr(const char* str, char character)
{
    while (*str != '\0')
    {
        if (*str == character)
            return str;

        ++str;
    }

    return NULL;
}

对于这段代码,我们发现,他的功能是查找一个字符,那么他就有能出现一次就好,这是最好的情况,也可能查找了n次才找到,这是最坏情况,他的平均情况为n/2,但是我们实际中只看最坏情况,所以他的时间复杂度为o(n)

(5)例5

// 计算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次比较,第二次是n-1,直到最后一趟是1

等差数列求和为后结果为(n+1)*n/2,所以他的时间复杂度为o(n^{2}

这里也说明一点,不是说一层循环就是n,两层循环就是o(n^{2}

(6)例6

// 计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
	assert(a);
	int begin = 0;
	int end = n;
	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;
}

 对于这段代码,我们可以发现这是一个二分查找算法,那么这个该如何思考呢?

他的最好情况是1

那么最坏情况呢?

我们想假设是一个n个元素的数组,他每一次都需要除以2,最终结果为1。

因此我们可以逆向思考,1*2*2....*2=n,这就是我们二分查找的过程,所以2^{x}=n,因此解得x=log^{_{2}^{n}},所以查找次数就是log^{_{2}^{n}},但是因为很多地方不方便写底数,所以算法的复杂度常常简写为logN,也就是O(logN)但是我们会发现网上很多书或资料会写成O(lgN),这种写法严格上来说是不对的,因为数学中这个是以10为底的,我们这个是以2为底的可以写成这样的。

(7)例7

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

这是一个求阶乘的递归算法。其实他的运算次数我们可以看出是n,因为他递归了n次,每次递归运算的是O(1),所以最终的时间复杂度就是O(N)

那么我们假如在里面又套了一层for循环

for(i=0;i<n;i++)
{
    
}

那么他的时间复杂度就是O(N^{2})

3.总结

以上就是我们常见的时间复杂度计算

常见的时间复杂度有   O(N^{2})O(N)O(logN)O(1)

下图是各个时间复杂度的比较

我们从这个中也能看出来,O(1)与O(logN)几乎是一样的,也就是说他们几乎处于同一个量级,他们两个就是最优的

三、常见空间复杂度的计算

我们在前面已经介绍过空间复杂度的概念了

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

也就是说,他只是计算变量的个数的。

1.例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;
	}
}

对于我们这个代码而言,他其实总共就只创建了三个变量,end,exchange,i,所以空间复杂度就是O(1)

在这里大家疑惑就是,不是有一个循环吗,这样应该创建了很多次空间啊,这里要说的就是时间是累计的,但是空间是不累计的,空间我们使用完以后,就会将他还回去,所以每一次都是那三个变量。因此就是三个。

而且函数的形参是一般不记作空间复杂度的计算的,但也可以计入,因为就算计入,他的量级也是最小的,不会影响原本的空间复杂度。

2.例2

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

}

对于这个代码,这是一个斐波那契数列的代码,他采用的是循环的方式来求解,我们可以看出,如果计入形参的话,他的变量个数为n+5=6个,所以空间复杂度为O(N)

3.例3

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

这是一个求阶乘的代码

我们知道,递归调用了N层,每次调用建立一个栈帧,每个栈帧使用了常数个空间O(1)

所以总的空间最大的消耗,空间复杂度就为O(N)

这里要注意,函数调用时候建立栈帧,返回时销毁栈帧。我们计算的是空间同时使用的最大消耗,而不能认为他最好是要销毁的,就认为他的空间复杂度是O(1),这与时间复杂度是类似的,计算的是最坏的情况。

四、有复杂度要求的算法题练习

1.消失的数字

题目链接:

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

 

题目描述:

(1)思路一

我们首先最先想到的办法当然就是先排序,然后一个一个找的,但是实际上这样是行不通的,我们在这里先给出一条结论,最快的排序也需要O(N*logN)的时间复杂度,显然不符合题目要求,所以排除此思路

(2)思路二

既然排序行不通,那么我们这样想,如果我们将1--N之间的数都加一块,然后将数组里面的数都加一块,然后一相减,是不是也可以啊,确实如此,这样一来时间复杂度也满足了我们的要求。这种思路较为简单,此处先不做代码展示了

(3)思路三

我们在操作符中学习过异或操作符,他的作用是,相同的二进制位进行异或,结果为0,0跟任何数异或,结果仍为该数,并且异或是满足交换律的

由此我们得到一种新的办法,让这个数组中的所有元素全部异或,放入一个数中,然后在让这个数与1~N之间的所有数进行异或,最终的结果就是不见的数字了,由此题目得到解决,我们给出代码

int missingNumber(int* nums, int numsSize){
    int i=0;
    int x=0;

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

运行结果为

 2.轮转数组

题目链接:189. 轮转数组 - 力扣(Leetcode)

题目描述:

 (1)思路一

这道题我们简单分析一下,我们发现,右旋一次其实就是将最后一个元素放到最前面那个元素,然后剩下的都依次往后挪动即可,因此我们可以按照这个思路先写一下代码

void rotate(int* nums, int numsSize, int k){
    while(k--)
    {
        int tmp=nums[numsSize-1];
        int right=numsSize-2;
        for(right=numsSize-2;right>=0;right--)
        {
            nums[right+1]=nums[right];
        }
        nums[0]=tmp;
    }
}

当我们运行的时候,我们很遗憾的发现,超出时间了,我们发现这个测试用例给的很大。

因此我们这个思路是行不通的,我们要另寻他法

我们这个代码的时间复杂度是O(N*K)

(2)思路二

我们可以以空间换时间,也就是说,我们在创建一个数组,后面的k项放到新数组的前面,然后将nums数组的前n-k项放到新数组后面,最后将新数组的值都拷贝到原来的数组中,这样做似乎也没有问题,而且时间复杂度降低了,变成了O(N),但是空间复杂度也变成看O(N),而且万一题目再给出那么大的测试用例,那么空间都不够用了怎么办?时间也超时了怎么办?。所以综合来看,这个方法似乎也不是很好。

(3)思路三

这个方法其实我们讲过一个字符串逆序问题,在这个问题中我们提到过,这里给出链接:C语言经典题目之字符串逆序_青色_忘川的博客-CSDN博客        

这里我们最后一个题目是,逆序一句话,但不逆序单词。我们可以联想到,我们这个题逆序前n-k个数组元素,然后逆序后面k个元素,最后逆序整个数组,注意次序不可颠倒,否则会出错的,问题也就迎刃而解,这道题就相当于前面那片文章中题目的弱化版

但是我们会发现其实会出现这种报错,像这种错误都是内存泄漏之类的,

 

他最终给的一个用例是一个元素的时候,我们会发现,我们中间出现了numsSize-k这种的语句,这明显导致数组越界了。因此会报错。所以我们进行修改

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=k%numsSize;
    }
    reverse(nums,numsSize-k,numsSize-1);
    reverse(nums,0,numsSize-k-1);
    reverse(nums,0,numsSize-1);

}

运行结果为


总结

本站主要讲解了时间复杂度与空间复杂度的概念,以及一些经典的题目,包括两个力扣题,如果对你有帮助,不要忘记点赞加收藏哦!!!

关注我,后面的文章将更加精彩

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

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

相关文章

【计算机视觉+自动驾驶】二、多任务深度学习网络并联式、级联式构建详细讲解(图像解释 超详细必看)

觉得有帮助麻烦点赞关注收藏~~~ 一、多任务网络的主要分类 目前建立的多任务网络可以分为两种方法&#xff0c;一种为并联多任务网络结构&#xff0c;另一种为级联多任务网络结构&#xff0c;两种网络构建方式分别如下图所示 并联式 级联式 并联网络结构大多为共享基础网络而…

ADI Blackfin DSP处理器-BF533的开发详解14:LED跑马灯(含源代码)

接口讲完了&#xff0c;下面写点应用程序&#xff0c;GPIO最典型的应用&#xff0c;LED跑马灯。 硬件准备 ADSP-EDU-BF533&#xff1a;BF533开发板 AD-HP530ICE&#xff1a;ADI DSP仿真器 软件准备 Visual DSP软件 硬件链接 *硬件实现原理 ADSP-EDU-BF533开发板上共设计了…

2005-2020年全国31省劳动力市场分割指数

2005-2020年全国31省劳动力市场分割指数 1、时间&#xff1a;2005-2020年 2、范围&#xff1a;包括全国31省&#xff0c; 3、数据内容&#xff1a;数据存在缺失&#xff0c;下载链接界面有数据预览&#xff0c;具体缺失情况参看链接内数据预览&#xff0c; 内含原始数据、A…

把废旧监控改无人机遥控车红外远程摄像头

像我们这些精打细算的业余玩家&#xff0c;淘个新宝贝都要掂量掂量。很羡慕能买到专用红外摄像头配无人机。可是手头不宽裕&#xff0c;只有一些旧零件。这都是废物再利用&#xff0c;所以说不要太追求性能了&#xff0c;自然让他工作就好&#xff0c;测试这条路线的可行性。 …

blneder 蜡笔

文章目录简介.打开蜡笔.基本操作.自由线.图形工具.图层.遮罩.画布.画布原点.![在这里插入图片描述](https://img-blog.csdnimg.cn/46cb7019e8ff41e6b391e056c616ce32.png)画布旋转.辅助.圆形.径向.平行.栅格.等距.编辑模式.顶部工具栏.选择.曲线编辑.左侧工具栏.快捷键.画笔深度…

值得一看的Linux内核—中断下半部之软中断

软中断 软中断&#xff08;softirq&#xff09;是中断处理程序在开启中断的情况下执行的部分&#xff0c;可以被硬中断抢占。 内核定义了一张软中断向量表&#xff0c;每种软中断有一个唯一的编号&#xff0c;对应一个softirq_action实例&#xff0c;softirq_action实例的成员…

b站黑马JavaScript的Node.js案例代码——考试成绩整理案例

目录 目标效果&#xff1a; 重点原理&#xff1a; 1.js中split方法——转换字符串为数组 2.js中forEach方法——遍历数组中每个对象 3.js数组操作中push方法——添加1/多个元素去数组的末尾 4.js数组操作中replace方法——在字符串中用一些字符替换另一些字符 5.js数组操…

ATtiny13与Proteus仿真-8位通用定时器/计数器与PWM仿真

8位通用定时器/计数器与PWM 1、8位通用定时器介绍 ATtiny13的8位通用定时器/计数器有两个独立的输出比较单元,并支持PWM。这意味着,可以通过8位通用定时器/计数器生产PWM信号。关于PWM的介绍,在这里就展开介绍,请参考相关资料。 ATtiny13的8位通用定时器/计数器具有如下…

备战一年,终于斩获腾讯T3,老子一定有美好的未来...

我就是那个从25岁躺平&#xff0c;30岁开始醒悟的“中年秃头大叔”&#xff0c;这人一到了中年&#xff0c;思考问题的方向确实不一样了。以前我不想结婚不想养育后代&#xff0c;天天公司摸鱼&#xff0c;总觉得自己赚钱自己花就挺好&#xff0c;25岁赚一万&#xff0c;30岁还…

[ 数据结构 ] 排序算法--------七大内排,看完还不会写来揍我

0 前言 1.1 排序分类 内部排序和外部排序,前者数据加载到内存,后者数据量大需借助外部文件. 内部排序包含: 插入排序:直接插入排序,希尔排序 选择排序:简单选择排序,堆排序 交换排序:冒泡排序,快速排序 归并排序 基数排序 1.2 复杂度 1)度量一个程序时间有两种方法,事后统…

tp3.2实现websocket

首先从单服务器实现开始 我的系统是centos系统&#xff0c;lnmp搭建的环境&#xff0c;php5.6 1&#xff1a;首先检查环境是否支持 curl -Ss http://www.workerman.net/check.php | php PHP Version > 5.3.3 [OK] Extension pcntl check [OK] Extension posix check [OK] 2.…

Metal每日分享,均值模糊滤镜效果

本案例的目的是理解如何用Metal实现均值模糊效果滤镜&#xff0c;均值模糊原理其实很简单通过多个纹理叠加&#xff0c;每个纹理偏移量设置不同达到一点重影效果来实现模糊; Demo HarbethDemo地址 实操代码 // 均值模糊效果滤镜 let filter C7MeanBlur.init(radius: 0.5)//…

清除浏览器缓存

清除浏览器的缓存知识调用前言引入具体操作知识调用 文章中可能用到的知识点前端学习&#xff1a;浏览器缓存方式有哪些&#xff08;http协议 websql indexDB cookie localstorage sessionstorage&#xff09;如何查看Chrome浏览器的页面缓存内容【详细教程】 前言引入 上期文…

基于汇编的.NET高级调试

一:背景 1. 简介 .NET 高级调试要想玩的好,看懂汇编是基本功,但看懂汇编和能写点汇编又完全是两回事,所以有时候看的多,总手痒痒想写一点,在 Windows 平台上搭建汇编环境不是那么容易,大多还是用微软的 MASM + DosBox 搭一个 8086 的环境,这玩意距今快 50 年了。 在…

Node.js Event Loop 处理的几大周期介绍

Node.js Event Loop 处理的几大周期如下图所示&#xff1a; Timer&#xff1a;通过 setTimeout() 或 setInterval() 安排的一切都将在这里处理。 IO 回调&#xff1a;这里将处理大部分回调。 由于 Node.js 中的所有用户态代码基本上都在回调中&#xff08;例如&#xff0c;对传…

深入理解机器学习——概率图模型(Probabilistic Graphical Model):马尔可夫随机场(Markov Random Field,MRF)

分类目录&#xff1a;《深入理解机器学习》总目录 马尔可夫随机场&#xff08;Markov Random Field&#xff0c;MRF&#xff09;是典型的马尔可夫网&#xff0c;这是一种著名的无向图模型&#xff0c;图中每个结点表示一个或一组变量&#xff0c;结点之间的边表示两个变量之间的…

Zookeper报错:Will not attempt to authenticate using SASL (unknown error)|防火墙的问题

先放一张debug成功的图吧~ 之前一直报这个错&#xff0c;不知道为什么&#xff0c;非常迷惑&#xff0c;然后试了多方法&#xff0c;就是防火墙的问题。我是Hadoop2.5和centos6&#xff0c;因此没法用systemtcl&#xff0c;就使用serive命令。 方式一&#xff1a;Linux命令来…

Kubernetes单主集群的部署(一)

目录 一、k8s单主架构集群的部署 1.操作系统初始化配置 2.部署 etcd 集群 3.部署docker引擎 4.部署 Master 组件 5.部署 Worker Node 组件 6.部署网络组件&#xff08;使用 flannel&#xff09; 一、k8s单主架构集群的部署 k8s集群master01&#xff1a;192.168.116.1…

纯手写2022年最新JVM调优实战手册,看完让你精通JVM调优

很多程序员不重视 JVM 内存调优&#xff0c;写出来的代码经常出现 OOM 等内存问题。而且&#xff0c;面试求职者中&#xff0c;很多求职者一旦遇到JVM 或者 JVM 调优方面的问题&#xff0c;往往不知如何回答&#xff0c;才能充分展现自己的能力。 jvm OOM问题实战分析 说说问题…

JS 原生面经从初级到高级【近1.5W字】

前言 是时候撸一波 JS 基础啦,撸熟了,银十速拿 offer; 本文不从传统的问答方式梳理,而是从知识维度梳理,以便形成知识网络; 包括函数,数组,对象,数据结构,算法,设计模式和 http. 1. 函数 1.1函数的3种定义方法 1.1.1 函数声明 //ES5 function getSum(){} function (){}//匿名…