【数据结构】时间复杂度及其计算

封面

前言

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

算法效率的度量

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

时间复杂度

定义

一个语句的频度是指该语句在算法中被重复执行的次数。算法中所有语句的频度之和记为 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):【空间复杂度】

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

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

  • 本章详细介绍了算法的时间复杂度的分析方法与步骤
  • 7
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
数据结构——用C描述答案 习题解答(唐策善版)(其他版本在上面) 第一章 绪论(参考答案) 1.3 (1) O(n) (2) (2) O(n) (3) (3) O(n) (4) (4) O(n1/2) (5) (5) 执行程序段的过程中,x,y值变化如下: 循环次数 x y 0(初始) 91 100 1 92 100 2 93 100 …… …… …… 9 100 100 10 101 100 11 91 99 12 92 100 …… …… …… 20 101 99 21 91 98 …… …… …… 30 101 98 31 91 97 到y=0时,要执行10*100次,可记为O(10*y)=O(n) 1.5 2100 , (2/3)n , log2n , n1/2 , n3/2 , (3/2)n , nlog2n , 2 n , n! , n n 第二章 线性表(参考答案) 在以下习题解答中,假定使用如下类型定义: (1)顺序存储结构: #define MAXSIZE 1024 typedef int ElemType;// 实际上,ElemType可以是任意类型 typedef struct { ElemType data[MAXSIZE]; int last; // last表示终端结点在向量中的位置 }sequenlist; (2)链式存储结构(单链表) typedef struct node {ElemType data; struct node *next; }linklist; (3)链式存储结构(双链表) typedef struct node {ElemType data; struct node *prior,*next; }dlinklist; (4)静态链表 typedef struct {ElemType data; int next; }node; node sa[MAXSIZE]; 2.1 头指针:指向链表的指针。因为对链表的所有操均需从头指针开始,即头指针具有标识链表的作用,所以链表的名字往往用头指针来标识。如:链表的头指针是la,往往简称为“链表la”。 头结点:为了链表操作统一,在链表第一元素结点(称为首元结点,或首结点)之前增加的一个结点,该结点称为头结点,其数据域不无实际意义(当然,也可以存储链表长度,这只是副产品),其指针域指向头结点。这样在插入和删除中头结点不变。 开始结点:即上面所讲第一个元素的结点。 2.2 只设尾指针的单循环链表,从尾指针出发能访问链表上的任何结点。 2•3 void insert(ElemType A[],int elenum,ElemType x) // 向量A目前有elenum个元素,且递增有序,本算法将x插入到向量A中,并保持向量的递增有序。 { int i=0,j; while (i<elenum && A[i]<=x) i++; // 查找插入位置 for (j= elenum-1;j>=i;j--) A[j+1]=A[j];// 向后移动元素 A[i]=x; // 插入元素 } // 算法结束
对于您提到的《数据结构》一书,根据引用\[1\]和引用\[2\]的内容,这是一本关于数据结构的教材,作者是严蔚敏。该书的第一章是绪论,介绍了数据结构的基本概念和术语,以及算法算法分析的内容。该书还包含习题集,其中第一章的习题包括算法设计题。此外,根据引用\[3\]的内容,书中还介绍了顺序栈的存储结构。至于是否有C语言版的第一版,我无法确定,因为引用的内容中没有提到具体的版本信息。如果您需要获取该书的C语言版第一版,建议您查阅相关的图书出版信息或者在网上搜索相关资源。 #### 引用[.reference_title] - *1* [【数据结构】01-绪论《数据结构 C语言版(严蔚敏、吴伟民)》](https://blog.csdn.net/TMT123421/article/details/84568290)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [数据结构C语言严蔚敏版(第二版)超详细笔记附带课后习题](https://blog.csdn.net/chongyang_/article/details/109544719)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值