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

前言

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

算法效率的度量

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

时间复杂度

定义

一个语句的频度是指该语句在算法中被重复执行的次数。算法中所有语句的频度之和记为【数据结构】第一章——绪论(3)_数据结构_02。它是该算法问题规模【数据结构】第一章——绪论(3)_数据结构_03的函数,时间复杂度主要分析【数据结构】第一章——绪论(3)_数据结构_02的数量级。

理解

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

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

//问题规模
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次的程序要快很多很多; 同理,执行【数据结构】第一章——绪论(3)_数据结构_03次的程序与执行【数据结构】第一章——绪论(3)_时间复杂度_06次的程序肯定要快;

时间复杂度的计算

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

算法中基本运算(最深层循环内的语句)的频度与【数据结构】第一章——绪论(3)_数据结构_02同数量级,因此通常采用算法中基本运算的平度【数据结构】第一章——绪论(3)_时间复杂度_08来分析算法的时间复杂度。因此,算法的时间复杂度记为:

【数据结构】第一章——绪论(3)_时间复杂度_09

式中,【数据结构】第一章——绪论(3)_时间复杂度_10的含义是【数据结构】第一章——绪论(3)_数据结构_02的数量级,其严格的数学定义是:若【数据结构】第一章——绪论(3)_数据结构_02【数据结构】第一章——绪论(3)_时间复杂度_08是定义在正整数集合上的两个函数,则存在正整数【数据结构】第一章——绪论(3)_时间复杂度_14【数据结构】第一章——绪论(3)_数据结构_15,使得当【数据结构】第一章——绪论(3)_时间复杂度_16时,都满足【数据结构】第一章——绪论(3)_数据结构_17

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

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

乘法规则:

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

这两个规则我们通过下面的代码来给大家分别介绍是什么意思;

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

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

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

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

  • 接下来我们来分析循环内的部分
  • 在这个程序的循环部分,我们可以看到有三层循环,从最外层到最里层,每层循环执行的次数都为【数据结构】第一章——绪论(3)_数据结构_03次,也就是说每一层循环的数量级都是【数据结构】第一章——绪论(3)_时间复杂度_25
  • 对于第二层循环它的时间复杂度应该是【数据结构】第一章——绪论(3)_数据结构_26,这里我们可以用到乘法规则

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

  • 对于第三层循环它的时间复杂度应该是【数据结构】第一章——绪论(3)_数据结构_28,我们根据乘法规则可以得到【数据结构】第一章——绪论(3)_数据结构_22;

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

对于一个代码来说,我们判断时间复杂度时取的是【数据结构】第一章——绪论(3)_时间复杂度_08中随着【数据结构】第一章——绪论(3)_数据结构_03增长最快的项,将其系数置为1作为时间复杂度的度量。 例如:【数据结构】第一章——绪论(3)_数据结构_32的时间复杂度为【数据结构】第一章——绪论(3)_时间复杂度_33

现在我们就能得到整个程序的时间复杂度为【数据结构】第一章——绪论(3)_数据结构_22

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

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

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

  • 当我们输入【数据结构】第一章——绪论(3)_时间复杂度_37时,那最深层循环的执行次数应该是【数据结构】第一章——绪论(3)_数据结构_38次;
  • 当我们输入【数据结构】第一章——绪论(3)_时间复杂度_39时,那最深层循环的执行次数应该是【数据结构】第一章——绪论(3)_数据结构_21次;
  • 当我们输入【数据结构】第一章——绪论(3)_数据结构_41时,那最深层循环的执行次数应该是【数据结构】第一章——绪论(3)_数据结构_42次;

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

  • 最坏时间复杂度:指的是在最坏的情况下,算法的时间复杂度。就比如在这个代码中当【数据结构】第一章——绪论(3)_时间复杂度_39时,此时的时间复杂度为【数据结构】第一章——绪论(3)_数据结构_22
  • 平均时间复杂度:指的是所有可能输入实例在等概率出现的情况下,算法期望运行时间。就比如在这个代码中当【数据结构】第一章——绪论(3)_时间复杂度_37时,此时的时间复杂度为【数据结构】第一章——绪论(3)_数据结构_46
  • 最好时间复杂度:指的是在最好的情况下,算法的时间复杂度。就比如我希望不打印循环内的任何内容时,我的输入满足【数据结构】第一章——绪论(3)_数据结构_41时,此时的时间复杂度为【数据结构】第一章——绪论(3)_时间复杂度_48;当我希望每句话打印一次时,我的输入满足【数据结构】第一章——绪论(3)_时间复杂度_49时,此时的时间复杂度也为【数据结构】第一章——绪论(3)_时间复杂度_48

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

一般我们考虑的都是在最坏的情况下的时间复杂度,这样是为了保证算法的运行时间不会比它更长 - 对于一个好的算法我们的要求是高效率,也就是运行时间越少越好,那对于不同的时间复杂度,我们应该如何比较大小呢?下面我们就来介绍一下常见的时间复杂度的大小关系;

常见的渐近时间复杂度

【数据结构】第一章——绪论(3)_时间复杂度_51

这个关系我们应该如何去记呢?这里我借用王道视频中咸鱼学长传授的口诀——常对幂指阶 这个口诀的意思就是:

【数据结构】第一章——绪论(3)_时间复杂度_52

时间复杂度的相关内容到这里咱们就介绍完了,接下来咱们趁热打铁,做几道习题巩固一下知识点;

习题巩固

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

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

【数据结构】第一章——绪论(3)_数据结构_53 【数据结构】第一章——绪论(3)_数据结构_54 【数据结构】第一章——绪论(3)_数据结构_55 【数据结构】第一章——绪论(3)_时间复杂度_56

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

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

【数据结构】第一章——绪论(3)_数据结构_53 【数据结构】第一章——绪论(3)_时间复杂度_58 【数据结构】第一章——绪论(3)_时间复杂度_59 【数据结构】第一章——绪论(3)_时间复杂度_60

3.在下列程序段中,【数据结构】第一章——绪论(3)_数据结构_03为正整数,则最后一行语句的频度在最坏情况下是()。

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

【数据结构】第一章——绪论(3)_数据结构_53 【数据结构】第一章——绪论(3)_时间复杂度_58 【数据结构】第一章——绪论(3)_数据结构_64 【数据结构】第一章——绪论(3)_时间复杂度_65

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

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

【数据结构】第一章——绪论(3)_时间复杂度_66 【数据结构】第一章——绪论(3)_数据结构_67 【数据结构】第一章——绪论(3)_数据结构_68 【数据结构】第一章——绪论(3)_数据结构_69

5.设【数据结构】第一章——绪论(3)_数据结构_03是描述问题规模的非负整数,下面的程序片段的时间复杂度是()

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

【数据结构】第一章——绪论(3)_数据结构_71 【数据结构】第一章——绪论(3)_时间复杂度_72 【数据结构】第一章——绪论(3)_数据结构_55 【数据结构】第一章——绪论(3)_时间复杂度_65

6.求整数【数据结构】第一章——绪论(3)_时间复杂度_75的阶乘算法如下,其时间复杂度为()

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

【数据结构】第一章——绪论(3)_数据结构_71 【数据结构】第一章——绪论(3)_时间复杂度_72 【数据结构】第一章——绪论(3)_数据结构_55 【数据结构】第一章——绪论(3)_时间复杂度_65

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

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

【数据结构】第一章——绪论(3)_数据结构_71 【数据结构】第一章——绪论(3)_时间复杂度_72 【数据结构】第一章——绪论(3)_数据结构_55 【数据结构】第一章——绪论(3)_时间复杂度_65

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

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

【数据结构】第一章——绪论(3)_时间复杂度_84 【数据结构】第一章——绪论(3)_数据结构_85 【数据结构】第一章——绪论(3)_数据结构_86 【数据结构】第一章——绪论(3)_数据结构_87

9.设【数据结构】第一章——绪论(3)_数据结构_03是描述问题规模的非负整数,以下程序段的时间复杂度为()

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

【数据结构】第一章——绪论(3)_时间复杂度_84 【数据结构】第一章——绪论(3)_数据结构_85 【数据结构】第一章——绪论(3)_数据结构_86 【数据结构】第一章——绪论(3)_时间复杂度_65

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

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

【数据结构】第一章——绪论(3)_时间复杂度_84 【数据结构】第一章——绪论(3)_时间复杂度_72 【数据结构】第一章——绪论(3)_数据结构_95 【数据结构】第一章——绪论(3)_时间复杂度_65

结语

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

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

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