「错位算法时空」,让你彻底学会「时间」与「空间」复杂度

Posted 飞向星的客机

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了「错位算法时空」,让你彻底学会「时间」与「空间」复杂度相关的知识,希望对你有一定的参考价值。

前言

本篇文章将带领大家深入了解我们 算法的时间复杂度和空间复杂度!

数据结构与算法 的重要性相信不用多说了吧,那么入门之前,必不可少的就是学习我们的 时间复杂度和空间复杂度!

使用不同算法,解决同一个问题,效率可能相差非常大。

为了对算法的好坏进行评价,我们引入 “算法复杂度” 的概念。

Let’s get it!



数据结构前言

平时经常在网上看到 数据结构,那么它到底是什么呢?

🎃 什么是数据结构

数据结构(Data Structure):是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。

🎃 什么是算法?

算法(Algorithm):就是定义良好的计算过程,他取一个或一组的值为输入,并产生出一个或一组值作为输出。

 

简单来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果。

🎃 数据结构和算法的重要性

这里给大家看一下知乎上是怎么回答的

学好算法对一个程序员来说是必须的吗?

 

数据结构与算法对一个程序员来说的重要性?

不言而喻,数据结构与算法 真的非常非常非常非常重要!!!

🎃 如何学好数据结构和算法?

1、死磕代码:光看怎么写,肯定得多去刷题网站上面刷题。(最好从早刷到晚😄)

 

推荐两个人人皆知的刷题网站:力扣 (LeetCode)牛客网

 

2、注意画图和思考:很多思路光靠想是不行的,有时候思想抛锚的话,就得需要你画图了。

 

推荐常用的画图工具:ProcessOndraw.io

🎃 书籍及资料推荐

数据结构学习得差不多了,推荐大家都去把 《剑指offer》《程序员代码面试指南》 上的题做一遍

算法效率

🧐思考:如何衡量一个算法的好坏呢?

📃代码示例:斐波那契数列

long long Fib(int N)

	if (N < 3)
		return 1;

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

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

所以这就引出了我们算法的复杂度👇

🌳 算法的复杂度

算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。

因此衡量一个算法的好坏,一般是从时间空间两个维度来衡量的,即时间复杂度空间复杂度

算法效率分析分为两种:

 

第一种是时间效率

 

第二种是空间效率

 

时间效率被称为时间复杂度,而空间效率被称作空间复杂度

 

时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。

 

在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。

 

但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。

时间复杂度

🐱‍🐉 时间复杂度的定义

在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间
 
一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。

 

但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。

 

一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。

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

📃先来看一段代码示例:

// 请计算一下Func1中++count语句总共执行了多少次?
void Func1(int N)

	int count = 0;
    
    //代码一
    //两层循环嵌套外循环执行N次,内循环执行N次,整体计算就是N*N的执行次数
	for (int i = 0; i < N; ++i)
	
		for (int j = 0; j < N; ++j)
		
			++count;
		
	
    
    //代码二
	//2 * N的执行次数
	for (int k = 0; k < 2 * N; ++k)
	
		++count;
	
	
    //代码三
    //常数项10
	int M = 10;
	while (M--)
	
		++count;
	
	printf("%d\\n", count);

🧐分析:

Func1 执行的基本操作次数 :

 

在代码一中++count执行了 N ∗ N N*N NN

 

在代码二中++count执行了N次

 

在代码三中++count执行了10次

 

所以总共执行次数为 ( N 2 + N + 10 ) (N^2+N+10) N2+N+10次。

 

我们得到一个函数关系: F ( N ) = N 2 + N + 10 F(N)=N^2+N+10 F(N)=N2+N+10

🤔函数关系分析:

上面已经推出了我们的一个函数公式: F ( N ) = N 2 + N + 10 F(N)=N^2+N+10 F(N)=N2+N+10

当N = 10,F(N) = 130

当N = 100,F(N) = 10210

当N = 1000,F(N) = 1002010

 

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

由上面的示例引出了我们的大O渐进表示法👇

🐱‍🐉 大O的渐进表示法

时间复杂度和空间复杂度一般都使用大O的渐进表示法进行表示,大O的渐进表示法规则如下:

1、所有常数都用常数1表示。

 

2、只保留最高阶项。 O ( n 2 + 2 ) O(n^2+2) O(n2+2),保留最高阶项后,成为 O ( n 2 ) O(n^2) O(n2)

 

3、如果最高阶项存在且不是1,则去除与这个项的系数,得到的结果就是大O阶。 O ( 4 n 2 ) O(4n^2) O(4n2),省去最高阶项的系数后,成为 O ( n 2 ) O(n^2) O(n2)

🤔思考:那么我们上面的Func1函数怎么用大O表示呢?

是不是: O ( N 2 ) + N + 10 O(N^2)+N+10 O(N2)+N+10 呢?

那就大错特错了!!!

使用大O的渐进表示法,要去掉那些对结果影响不大的项简洁明了的表示出了执行次数

 

所以正确的应该是: O ( N 2 ) O(N^2) O(N2)

🌰举几个栗子:

1、可以忽略加法常数: O ( 2 n + 3 ) = O ( 2 n ) O(2n + 3) = O(2n) O(2n+3)=O(2n)

 

2、与最高次项相乘的常数可忽略: O ( 2 n 2 ) = O ( n 2 ) O(2n^2) = O(n^2) O(2n2)=O(n2)

 

3、最高次项的指数大的,函数随着 n 的增长,结果也会变得增长得更快: O ( n 3 ) > O ( n 2 ) O(n^3) > O(n^2) O(n3)>O(n2)

 

4、判断一个算法的(时间)效率时,函数中常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数

O ( 2 n 2 ) = O ( n 2 + 3 n + 1 ) O(2n^2) = O(n^2+3n+1) O(2n2)=O(n2+3n+1) O ( n 3 ) > O ( n 2 ) O(n^3) > O(n^2) O(n3)>O(n2)

😎理解了吗?

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

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

 

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

 

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

🌰举个栗子:

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

 

最好情况:1次找到

 

最坏情况:N次找到

 

平均情况:N/2次找到

 

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

❗❓重点:时间复杂度做的是悲观预期,所以时间复杂度看的是最坏的情况。

🐱‍🐉 时间复杂度的案例分析

📃案例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);

🤔分析:

第一个循环执行了 2 ∗ N 2*N 2N次,第二个循环执行了10次。 总共执行了 2 ∗ N + 10 2 * N+10 2N+10次。

 

因为最高阶项是: 2 ∗ N 2 * N 2N,所以时间复杂度是 O ( N ) O(N) O(N)

📃案例2

计算Func3的时间复杂度?

void Func3(int N, int M)

	int count = 0;
    
    //执行M次
	for (int k = 0; k < M; ++k)
	
		++count;
	
    
    //执行N次
	for (int k = 0; k < N; ++k)
	
		++count;
	
	printf("%d\\n", count);

🤔分析:

第一个循环执行了M次,第二个循环执行了N次。 总共执行了$ M+N$次。

 

最高阶项是M和N,所以时间复杂度是 O ( M + N ) O(M+N) O(M+N)

 

假设:

 

M大于N --> O ( M ) O(M) O(M)

 

N大于M --> O ( N ) O(N) O(N)

 

M和N一样大 --> O ( M ) / O ( N ) O(M) / O(N) O(M)/O(N

 

还是取影响最大的那一项,如果并没有说明M和N的大小关系,那么时间复杂度就是 O ( M + N ) O(M + 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);

🤔分析:

这个循环执行了100次, 说明总共执行了100次。

 

最高阶项是100,执行次数为常数次,所以时间复杂度是 O ( 1 ) O(1) O(1)

📃案例4

计算strchr的时间复杂度?

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

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

提示:strchr是一个在字符串中查找某个字符的算法

🤔分析:

时间复杂度: O ( N ) O(N) O(N)

 

在一个字符串中查找一个字符,肯定要变量这个字符串,所以会利用循环,遍历长度次,由于长度是未知的,所以最高阶项N。

 

最好情况:一次就找到了, O ( 1 ) O(1) O(1)

 

平均情况: O ( N / 2 ) O(N / 2) O(N/2) , 忽略系数项是 O ( N ) O(N) O(N)

 

最坏情况:遍历到最后才找到或者字符串中压根就没有, O ( N ) O(N) O(N)

📃案例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;
	


🤔分析:

精确的时间复杂度: O

以上是关于「错位算法时空」,让你彻底学会「时间」与「空间」复杂度的主要内容,如果未能解决你的问题,请参考以下文章

算法几分钟时间让你彻底学会—时间复杂度

算法 | 递归+缓存值=动态规划?

保姆级教学!彻底学会时间复杂度和空间复杂度

保姆级教学!彻底学会时间复杂度和空间复杂度

十分钟让你彻底学会Python函数式

递归的实质是能够把一个大问题分解第一