【数据结构】第一章——绪论(3)

封面

前言

大家好,很高兴又和大家见面啦!!!在上一篇中我们介绍了算法以及算法的五个特性和五个目标,今天我们将开始重点介绍算法的高效率和低存储量需求。

算法效率的度量

算法效率的度量是通过时间复杂度和空间复杂度来描述的。
时间复杂度对应的就是高效率;空间复杂度对应的就是低存储量需求。
那时间复杂度和空间复杂度又是什么呢?下面我们就来分别介绍一下;

时间复杂度

定义

一个语句的频度是指该语句在算法中被重复执行的次数。算法中所有语句的频度之和记为 T ( n ) T(n) T(n)。它是该算法问题规模 n n n的函数,时间复杂度主要分析 T ( n ) T(n) T(n)的数量级。

理解

大家看到这个定义时可能一脸懵,又是问题规模、又是数量级的,这些都是什么呀?
我刚开始看到这两个内容时也是一头雾水,经过后面的学习,我对这两个内容也有了一个初步的理解:

问题规模指的是一个程序执行的次数。

//问题规模
int main()
{
	printf("hello world\n");//执行次数为一次,问题规模为1
	for (int i = 0; i < 20; i++)
	{
		printf("你好\n");//执行次数为20次,问题规模为20
	}
	for (int i = 0; i < 3000; i++)
	{
		printf("I Love You\n");//执行次数3000次,问题规模为3000
	}
	return 0;
}

从这个代码中我们可以看到,每一句代码执行的次数都不相同,它们的执行次数也就代表着这句代码的问题规模;

数量级指的就是执行次数的单位。

//数量级
int main()
{
	for (int i = 0; i < 300; i++)
	{
		printf("hello\n");//执行次数为300,它的数量级就是百位级
	}
	for (int i = 0; i < 30000; i++)
	{
		printf("hello\n");//执行次数为30000,它的数量级就是万位级
	}
	int n = 0;
	scanf("%d", &n);
	for (int i = 0; i < n; i++)
	{
		printf("hello\n");//执行次数为30000,它的数量级就是n级
	}
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < n; i++)
		{
			printf("hello\n");//执行次数为n*n,它的数量级就是n*n级
		}
	}
	return 0;
}

在这个代码中我们可以看到,数量级也是代表的执行次数,但是它更多的是指执行次数的单位,比如一个程序要执行5次和一个程序要执行9次,这并不能说执行5次的就比执行9次的快多少;
一个程序执行1000次和一个程序执行9000次这也不能说执行1000次的比执行9000次的快多少;
但是执行5次的程序与执行1000次的程序想比,那肯定是执行5次的程序要快很多很多;
同理,执行 n n n次的程序与执行 n 2 n^2 n2次的程序肯定要快;

时间复杂度的计算

我们在了解了什么是问题规模和什么是数量级之后,下面我们来探讨一下这个时间复杂度是如何计算的。

算法中基本运算(最深层循环内的语句)的频度与 T ( n ) T(n) T(n)同数量级,因此通常采用算法中基本运算的平度 f ( n ) f(n) f(n)来分析算法的时间复杂度。因此,算法的时间复杂度记为:
T ( n ) = O ( f ( n ) ) T(n)=O(f(n)) T(n)=O(f(n))
式中, O O O的含义是 T ( n ) T(n) T(n)的数量级,其严格的数学定义是:若 T ( n ) T(n) T(n) f ( n ) f(n) f(n)是定义在正整数集合上的两个函数,则存在正整数 C C C n 0 n_0 n0,使得当 n > = n 0 n>=n_0 n>=n0时,都满足 0 < = T ( n ) < = C f ( n ) 0<=T(n)<=Cf(n) 0<=T(n)<=Cf(n)

那我们又应该如何来分析算法的时间复杂度呢?我们在分析一个程序的时间复杂度我们需要遵循两个规则:

加法规则:

T ( n ) = T 1 ( n ) + T 2 ( n ) = O ( f ( n ) ) + O ( g ( n ) ) = O ( m a x ( f ( n ) , g ( n ) ) ) T(n)=T_1(n)+T_2(n)=O(f(n))+O(g(n))=O(max(f(n),g(n))) T(n)=T1(n)+T2(n)=O(f(n))+O(g(n))=O(max(f(n),g(n)))

乘法规则:

T ( n ) = T 1 ( n ) × T 2 ( n ) = O ( f ( n ) ) × O ( g ( n ) ) = O ( f ( n ) × g ( n ) ) T(n)=T_1(n)×T_2(n)=O(f(n))×O(g(n))=O(f(n)×g(n)) T(n)=T1(n)×T2(n)=O(f(n))×O(g(n))=O(f(n)×g(n))
这两个规则我们通过下面的代码来给大家分别介绍是什么意思;

//时间复杂度的计算
int main()
{
	printf("hello\n");//执行次数为1次,问题规模为1,数量级为O(1)——也就是常数级
	int n = 0;//执行次数为1次,问题规模为1,数量级为O(1)——也就是常数级
	scanf("%d", &n);//执行次数为1次,问题规模为1,数量级为O(1)——也就是常数级
	for (int i = 0; i < n; i++)
	{
		printf("你好\n");//执行次数为n次,问题规模为n,数量级为O(n)
		for (int j = 0; j < n; j++)
		{
			printf("nice to meet you\n");//执行次数为n*n次,问题规模为n*n,数量级位O(n*n)
			for (int z = 0; z < n; z++)
			{
				printf("good bye\n");//执行次数为n*n*n次,问题规模为n*n*n,数量级为O(n*n*n)
			}
		}
	}
	return 0;
}

通过前面的介绍我们知道了算法中基本运算(最深层循环内的语句)的频度与 T ( n ) T(n) T(n)同数量级。
在这个程序中我们可以看到printf(“good bye\n”);这一句代码是最深层的语句,它需要执行的次数为 n 3 n^3 n3次,也就是说这个程序的时间复杂度 T ( n ) = O ( n 3 ) T(n)=O(n^3) T(n)=O(n3)
像这样一说是不是就感觉好简单,我只要搞明白最深层的循环的执行次数是不是就可以了。那下面问题来了,这个 n 3 n^3 n3是怎么来的呢?接下来我们可以来逐层分析;

  • 首先我们来分析循环外的部分

根据代码的注释我们可知道循环外的代码在这个程序中每次运行时,它们都只执行一次,也就是说它们的执行次数为一个确定的常数,这也就是我们所说的常数级

  • 接下来我们来分析循环内的部分
  • 在这个程序的循环部分,我们可以看到有三层循环,从最外层到最里层,每层循环执行的次数都为 n n n次,也就是说每一层循环的数量级都是 O ( n ) O(n) O(n)
  • 对于第二层循环它的时间复杂度应该是 T 1 ( n ) × T 2 ( n ) T_1(n)×T_2(n) T1(n)×T2(n),这里我们可以用到乘法规则
    T ( n ) = T 1 ( n ) × T 2 ( n ) = O ( f ( n ) ) × O ( g ( n ) ) = O ( f ( n ) × g ( n ) ) = O ( n × n ) = O ( n 2 ) T(n)=T_1(n)×T_2(n)=O(f(n))×O(g(n))=O(f(n)×g(n))=O(n×n)=O(n^2) T(n)=T1(n)×T2(n)=O(f(n))×O(g(n))=O(f(n)×g(n))=O(n×n)=O(n2)
  • 对于第三层循环它的时间复杂度应该是 T 1 ( n ) × T 2 ( n ) × T 3 ( n ) T_1(n)×T_2(n)×T_3(n) T1(n)×T2(n)×T3(n),我们根据乘法规则可以得到 T ( n ) = O ( n 3 ) T(n)=O(n^3) T(n)=O(n3);

现在我们对每一句代码的时间复杂度都分析完了,那整个程序的时间复杂度我们是不是只需要把它们每一句的时间复杂度相加就可以了,这样我们很容易就能得到: T ( n ) = T 1 ( n ) + T 2 ( n ) + T 3 ( n ) + T 4 ( n ) = 3 O ( 1 ) + O ( n ) + O ( n 2 ) + O ( n 3 ) T(n)=T_1(n)+T_2(n)+T_3(n)+T_4(n)=3O(1)+O(n)+O(n^2)+O(n^3) T(n)=T1(n)+T2(n)+T3(n)+T4(n)=3O(1)+O(n)+O(n2)+O(n3)

对于一个代码来说,我们判断时间复杂度时取的是 f ( n ) f(n) f(n)中随着 n n n增长最快的项,将其系数置为1作为时间复杂度的度量。
例如: f ( n ) = a n 3 + b n 2 + c n + d f(n) = an^3 + bn^2 +cn+d f(n)=an3+bn2+cn+d的时间复杂度为 O ( n 3 ) O(n^3) O(n3)

现在我们就能得到整个程序的时间复杂度为 T ( n ) = O ( n 3 ) T(n)=O(n^3) T(n)=O(n3)

相信大家看到这里对时间复杂度应该是有了一个初步的认识了,那下面我们再来看一个代码:

//时间复杂度的计算
int main()
{
	printf("hello\n");//执行次数为1次,问题规模为1,数量级为O(1)——也就是常数级
	int n = 0;//执行次数为1次,问题规模为1,数量级为O(1)——也就是常数级
	int m = 0;//执行次数为1次,问题规模为1,数量级为O(1)——也就是常数级
	scanf("%d%d", &n,&m);//执行次数为1次,问题规模为1,数量级为O(1)——也就是常数级
	for (int i = m; i < n; i++)
	{
		printf("你好\n");
		for (int j = m; j < n; j++)
		{
			printf("nice to meet you\n");
			for (int z = m; z < n; z++)
			{
				printf("good bye\n");
			}
		}
	}
	return 0;
}

对于这个代码而言,我们又应该如何来判断它的时间复杂度呢?此时程序最深层的循环执行次数还是不是 n 3 n^3 n3呢?
我相信有朋友很快就意识到了,此时的执行次数不仅与问题规模 n n n有关,而且还与我们输入的数据有关。
此时就会出现3种情况:

  • 当我们输入 m < n m<n m<n时,那最深层循环的执行次数应该是 ( n − m ) 3 (n-m)^3 (nm)3次;
  • 当我们输入 m = 0 m=0 m=0时,那最深层循环的执行次数应该是 n 3 n^3 n3次;
  • 当我们输入 m > n m>n m>n时,那最深层循环的执行次数应该是 0 0 0次;

对于时间复杂度来说,它也有三种情况:

  • 最坏时间复杂度:指的是在最坏的情况下,算法的时间复杂度。就比如在这个代码中当 m = 0 m=0 m=0时,此时的时间复杂度为 T ( n ) = O ( n 3 ) T(n)=O(n^3) T(n)=O(n3)
  • 平均时间复杂度:指的是所有可能输入实例在等概率出现的情况下,算法期望运行时间。就比如在这个代码中当 m < n m<n m<n时,此时的时间复杂度为 T ( n ) = O ( ( n − m ) 3 ) T(n)=O((n-m)^3) T(n)=O((nm)3)
  • 最好时间复杂度:指的是在最好的情况下,算法的时间复杂度。就比如我希望不打印循环内的任何内容时,我的输入满足 m > n m>n m>n时,此时的时间复杂度为 T ( n ) = O ( 1 ) T(n)=O(1) T(n)=O(1);当我希望每句话打印一次时,我的输入满足 m = n − 1 m=n-1 m=n1时,此时的时间复杂度也为 T ( n ) = O ( 1 ) T(n)=O(1) T(n)=O(1)

对于最好时间复杂度是我们需要的最理想的情况,但是我们在分析一个程序的时间复杂度时肯定是不能按最理想的状态来分析;

一般我们考虑的都是在最坏的情况下的时间复杂度,这样是为了保证算法的运行时间不会比它更长

对于一个好的算法我们的要求是高效率,也就是运行时间越少越好,那对于不同的时间复杂度,我们应该如何比较大小呢?下面我们就来介绍一下常见的时间复杂度的大小关系;

常见的渐近时间复杂度

O ( 1 ) < O ( l o g 2 n ) < O ( n ) < O ( n l o g 2 n ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) O(1)<O(log_2 n)<O(n)<O(nlog_2 n)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n) O(1)<O(log2n)<O(n)<O(nlog2n)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)
这个关系我们应该如何去记呢?这里我借用王道视频中咸鱼学长传授的口诀——常对幂指阶
这个口诀的意思就是:
常数级的时间复杂度 < 对数级的时间复杂度 < 幂数级的时间复杂度 < 指数级的时间复杂度 < 阶乘级的时间复杂度 常数级的时间复杂度<对数级的时间复杂度<幂数级的时间复杂度<指数级的时间复杂度<阶乘级的时间复杂度 常数级的时间复杂度<对数级的时间复杂度<幂数级的时间复杂度<指数级的时间复杂度<阶乘级的时间复杂度
时间复杂度的相关内容到这里咱们就介绍完了,接下来咱们趁热打铁,做几道习题巩固一下知识点;

习题巩固

1.以下算法的时间复杂度为()

`void fun(int n)
{
	int i = 1;
	while (i <= n)
	{
		i = i * 2;
	}
}`

A . O ( n ) A.O(n) A.O(n)
B . O ( n 2 ) B.O(n^2) B.O(n2)
C . O ( n l o g 2 n ) C.O(nlog_2n) C.O(nlog2n)
D . O ( l o g 2 n ) D.O(log_2n) D.O(log2n)

2.以下算法的时间复杂度为()

void fun(int n)
{
	int i = 0;
	while (i * i * i <= n)
	{
		i++;
	}
}

A . O ( n ) A.O(n) A.O(n)
B . O ( n l o g n ) B.O(nlogn) B.O(nlogn)
C . O ( n 1 / 3 ) C.O(n^{1/3}) C.O(n1/3)
D . O ( n 1 / 2 ) D.O(n^{1/2}) D.O(n1/2)

3.在下列程序段中, n n n为正整数,则最后一行语句的频度在最坏情况下是()。

for (i = n - 1; i > 1; i--)
{
	for (j = 1; j < i; j++)
	{
		if (A[j] > A[j + 1])
		{
			A[j]与A[j + 1]对换;//求最坏情况下的语句频度
		}
	}
}

A . O ( n ) A.O(n) A.O(n)
B . O ( n l o g n ) B.O(nlogn) B.O(nlogn)
C . O ( n 3 ) C.O(n^3) C.O(n3)
D . O ( n 2 ) D.O(n^2) D.O(n2)

4.以下算法中最后一行语句的执行次数为()

	for (i = 1; i <= n; i++)
	{
		for (j = 1; j <= 2 * i; j++)
		{
			m++;
		}
	}`

A . n ( n + 1 ) A.n(n+1) A.n(n+1)
B . n B.n B.n
C . n + 1 C.n+1 C.n+1
D . n 2 D.n^2 D.n2

5.设 n n n是描述问题规模的非负整数,下面的程序片段的时间复杂度是()

	x = 2;
	while (x < n / 2)
	{
		x = 2 * x;
	}

A . O ( l o g 2 n ) A.O(log_2n) A.O(log2n)
B . O ( n ) B.O(n) B.O(n)
C . O ( n l o g 2 n ) C.O(nlog_2n) C.O(nlog2n)
D . O ( n 2 ) D.O(n^2) D.O(n2)

6.求整数 n ( n > = 0 ) n(n>=0) n(n>=0)的阶乘算法如下,其时间复杂度为()

int fact(int n)
{
	if (n <= 1)
	{
		return 1;
	}
	return n * fact(n - 1);
}

A . O ( l o g 2 n ) A.O(log_2n) A.O(log2n)
B . O ( n ) B.O(n) B.O(n)
C . O ( n l o g 2 n ) C.O(nlog_2n) C.O(nlog2n)
D . O ( n 2 ) D.O(n^2) D.O(n2)

7.以下程序段的时间复杂度为()

	count = 0;
	for (k = 1; k <= n; k *= 2)
	{
		for (j = 1; j <= n; j++)
		{
			count++;
		}
	}

A . O ( l o g 2 n ) A.O(log_2n) A.O(log2n)
B . O ( n ) B.O(n) B.O(n)
C . O ( n l o g 2 n ) C.O(nlog_2n) C.O(nlog2n)
D . O ( n 2 ) D.O(n^2) D.O(n2)

8.下列函数的时间复杂度为()

int func(int n)
{
	int i = 0, sum = 0;
	while (sum < n)
	{
		sum += ++i;
	}
	return i;
}

A . O ( l o g n ) A.O(logn) A.O(logn)
B . O ( n 1 / 2 ) B.O(n^{1/2}) B.O(n1/2)
C . O ( n ) C.O(n) C.O(n)
D . O ( n l o g n ) D.O(nlogn) D.O(nlogn)

9.设 n n n是描述问题规模的非负整数,以下程序段的时间复杂度为()

x = 0;
while (n >= (x + 1) * (x + 1))
{
	x = x + 1;
}

A . O ( l o g n ) A.O(logn) A.O(logn)
B . O ( n 1 / 2 ) B.O(n^{1/2}) B.O(n1/2)
C . O ( n ) C.O(n) C.O(n)
D . O ( n 2 ) D.O(n^2) D.O(n2)

10.以下程序段的时间复杂度为()

int sum = 0;
for (int i = 1; i < n; i *= 2)
{
	for (int j = 0; j < i; j++)
	{
		sum++;
	}
}

A . O ( l o g n ) A.O(logn) A.O(logn)
B . O ( n ) B.O(n) B.O(n)
C . O ( n l o g n ) C.O(nlogn) C.O(nlogn)
D . O ( n 2 ) D.O(n^2) D.O(n2)

结语

在今天的篇章中我们重点介绍了时间复杂度的相关知识点;

  • 什么是时间复杂度?
  • 时间复杂度的判断规则
  • 常见的渐近时间复杂度——常对幂指阶

今天的内容分享就到这里,有兴趣的朋友可以在了解完时间复杂度的相关知识点后把这十道题自己练习一下,后面我会分享答案及其解析,各位记得关注哦!
最后感谢各位的翻阅,咱们下一篇再见!!!

【数据结构】第一章——绪论

【数据结构】第一章——绪论(1):【数据结构的基本概念】

  • 本章内容介绍了数据结构的基本概念和术语以及数据结构的三要素

【数据结构】第一章——绪论(2):【算法】

  • 本章介绍了算法的基本概念

【数据结构】第一章——绪论(4):【空间复杂度】

  • 本章详细介绍了算法的空间复杂度

【数据结构】第一章——习题演练:【时间复杂度的分析方法与步骤】

  • 本章详细介绍了算法的时间复杂度的分析方法与步骤