数据结构(2)—算法

(1)小白建议学习青岛大学王卓老师的数据结构视频,或者购买程杰老师的大话数据结构。

 

(2)邀请加入嵌入式社区,您可以在上面发布问题,博客链接,公众号分享,行业消息,招聘信息等。

目录

算法的概念

算法的定义

两种算法比较

算法与数据结构的关系

算法的特性

输入

输出

有穷性

确定性

可行性

算法设计要求

正确性

可读性

健壮性

高效性

算法效率度量

事后统计法

事前分析估算法

函数的渐近增长

算法的时间复杂度

最好,最坏和平均时间复杂度

 算法空间复杂度


算法的概念

算法的定义

算法就是为了解决某一类问题而规定的有限长的操作序列。

两种算法比较

(1)可能上面这个算法的定义很多人看不懂。没关系,看了下面这两个例子就明白了。

(2)看完下面这两个算法比较,我们发现,算法其实就是对一个问题的计算方式而已。比如算法1采用的是将100个数依次加起来,而算法2就是采用小学所学的,首项加尾项乘以项数除以二来计算。

(3)我们对比这两个算法,能够发现什么,第一个算法很花费时间。如果是人进行计算,还很有可能计算出现问题。但是第二个算法就有很高的优越性,计算速度快。算法2就是大佬高斯先生所创造。

(4)虽然说算法2比算法1有很高的优越性,但是不知道各位有没有发现一个问题。就是,假如我只要计算1+2怎么办呢?这个时候,如果还采用算法2,就显得非常愚蠢了,因为算法2需要加乘除三步操作,而算法1只需要一次加即可(注意,从数据结构的角度,算法1执行了5步,算法2执行了3步,后面会讲)。这表明,算法是没有通用的,没有绝对好的算法,就好像世界上没有治疗百病的药物一样。

/*
    计算1+2+3+...+100的两种算法
*/

/******算法1*******/
int i, sum = 0, n = 100;
for(i = 1; i <= n; i++)
{
	sum = sum + i;
}
printf("%d", sum);	

/******算法2*******/
int sum = 0, n = 100;
sum = (1 + n) * n / 2;
printf("%d", sum);

算法与数据结构的关系

我们很多时候,听到数据结构与算法,将数据结构和算法放在一起说。他们直接有什么联系呢?

其实只有通过对特定算法的使用,才能真正清楚理解数据结构的作用。在涉及到算法运算的时候,总是要联系到算法处理的对象和结果的数据,而这些数据如何存储与操作就需要设计到数据结果的内容了。

算法的特性

算法具有五大基本特性:输入输出有穷性确定性可行性

输入

一个算法有零个多个输入。

/*
    有多个输入的算法
*/
int Algorithm(int sum, int n)
{
	sum = (1 + n) * n / 2;
	return sum;
}


int main()
{
	printf("%d\n", Algorithm(0, 100));
	return 0;
}

/*
    零个输入的算法
*/
int Algorithm()
{
	int sum = 0, n = 100;
	sum = (1 + n) * n / 2;
	return sum;
}


int main()
{
	printf("%d\n", Algorithm());
	return 0;
}

输出

 (1)算法至少有一个多个输出。

(2)算法没有输出要它干嘛?输出方式有多种,比如return返回一个值,比如printf打印出数据,或者通过指针的方式,将数据进行更改。

/*
    return返回值的方式输出
*/
int Algorithm()
{
	int sum = 0, n = 100;
	sum = (1 + n) * n / 2;
	return sum;
}


int main()
{
	printf("%d\n", Algorithm());
	return 0;
}


/*
    printf的方式输出
*/
void Algorithm()
{
	int sum = 0, n = 100;
	sum = (1 + n) * n / 2;
	printf("%d\n", sum);
}


int main()
{
	Algorithm();
	return 0;
}

/*
    通过修改指针内数据形式输出
*/

void Algorithm(int* sum,int* n)
{
	*sum = (1 + *n) * (*n) / 2;
}


int main()
{
	int sum = 0, n = 100;
	Algorithm(&sum, &n);
	printf("%d\n",sum);
	return 0;
}

有穷性

(1)有穷性:算法执行必须是有限步骤之后结束,而且每一步都是必须在有限时间内完成

(2)一个算法如果进入了死循环,那么它就没有存在的意义。同理,假如一个数据,利用这个算法需要计算100年,人都没了,还算什么?所以这种需要计算过长时间的算法也是没有存在意义的

确定性

(1)确定性:算法的每一步都是有确定意义的,不会产生二义性,使算法的执行者或阅读者都能明确其含义及如何执行。

(2)如果一个算法出现了歧义,就会产生不一样的结果最终导致程序出问题,比如下面这一段算法。明明是同一个算数方式,而且符合操作符的优先级,但会产生不一样的结果。

/***   算法    ***/
c + --c;

假设c=1

/***   理解1   ***/
因为操作符--优先级在+之前,所以先--,再+
--c   此时c=0
0+0=0

/***   理解2   ***/
1+ --c      先将c放入算法
1+0=1       因为操作符--优先级在+之前

可行性

(1)可行性:算法的每一步都必须是可行的,能够通过已经实现的基本操作运算执行有限次来完成。

(2)因为当前存在一些极其复杂的算法,受限于编程方法、工具和大脑限制了这个工作。不过这个属于理论研究的范围,不属于我们当前需要讨论的问题

算法设计要求

我们判断一个算法的时候,不仅仅只是通过计算速度这一个方面来判断。还需要从其他几个角度来思考,总结起来就是四点:正确性可读性健壮性高效性

正确性

(1)正确性:是指算法应该具有输入,输出和加工处理没有歧义,能正确反映问题的需求,能够得到问题的正确答案。

(2)算法的正确通常有很大的区别,大体可以分为以下四类:

        1,算法程序没有语法错误

        2,算法程序对合法的输入数据能够产生满足要求的输出结果

        3,算法程序对非法的输入数据能够产生满足要求的输出结果

        4,算法程序对于精心选择的,甚至刁难的测试数据都有满足要求的输出结果

可读性

(1)可读性:一个优秀的算法程序设计,首先要便于他人理解。可读性强的算法有助于人们对于算法的理解,而难懂的算法容易隐藏漏洞,难于调试与修改。

(2)虽然我常常能够在某音上看到很多人说,程序写的羞涩难懂能够让老板以为自己是大佬。而程序写的太易懂,老板很容易以为自己没有竞争力,容易被替代。虽然这么说有那么一点点道理,但是我还是建议代码写的易懂一些,如果出了问题自己也好处理。

健壮性

(1)健壮性:当输入的数据非法时,好的算法能够适当的做出处理,而不是产生莫名奇妙的输出结果。

(2)像我专栏OpenMV的教程里面,在做颜色识别的时候,需要输入RBG的最大和最小阈值。当你的最大和最小阈值写反了,也能在算法里面进行交换,输出正确的结果。

高效性

(1)高效性:高效性包含时间与空间两个方面。

(2)时间高效是指算法设计合理,执行效率高,可以用时间复杂度来度量。

(3)空间高效是指算法占用存储容量合理,可以用空间复杂度来度量。

(4)以我数据结构(1)前言中的链式存储结构部分例子说明,虽然链式存储结构进行插队这一操作方便了许多,但是它需要腾出空间进行记录号码牌的顺序。时间和空间上要做抉择了,这也就是我们常说了,利用空间换取时间

算法效率度量

事后统计法

(1)事后统计法:事后统计法就是利用设计好的程序和数据,利用计算机对不同算法程序进行编译比较,最后看哪一个程序跑最快。

(2)这种方法毫无疑问是很麻烦的。

第一花费时间。你为了找出一个好的算法,需要编写出很多套不同的算法程序,最终选取其中一个,比较消耗时间。

第二测试数据麻烦。如果需要测试算法,想必需要很多数据才能看到他们的差异。就拿我上面说的1+2+..+100哪个为例子,如果只需要计算1+2,无法看出两个算法的差异。必须要比较多的数据,比如100个。但是有一些算法,可能他们的差异并不只有100个数据才能看出,他们会需要1000个,1W个甚至更多。这一就导致测试数据比较麻烦。

第三依赖硬件和编译环境。相同的程序放在不同的计算机上面跑可能会因为硬件的差异,导致运算速度不同。因为编译器的不同,它最后的执行速度也不同。就算是同一台机器,由于CPU的使用率和内存占用情况不同,也会造成细微差别

事前分析估算法

(1)事前分析估计法:在计算机程序编译前,依据统计的方法对算法进行估计。

(2)一个算法的运行时间大致可以等于计算机执行一种简单的操作(如赋值、比较,移动)所需的时间与算法中进行的简单操作次数乘积。

算法运行时间=\sum每条语句的执行次数(又称为语句频度)X该语句执行一次所需的时间。

(3) 我们看之前上面所说的两种算法。如果是n=2,也就是只需要计算1+2,如果是让人来计算,显然算法1比较方便。但是对于计算机而言,因为每一条语句所执行的时间是由机器所异,即使是相同的计算机也会因为CPU占用率而导致不同,所以我们会将每一条语句默认执行时间是相同的。这也解释了,为什么我上面说,从数据结构的角度,算法1执行了5步,算法2执行了3步。

/***   算法1   ***/
int i, sum = 0, n = 100;	/* 执行1次 */
for(i = 1; i <= n; i++)	/* 执行了n+1次,为什么后面需要+1,是因为当i=101的时候,需要判断101<=100这一步,所以需要+1 */
{
	sum = sum + i;			/* 执行n次 */
}
printf("%d", sum);			/* 执行1次 */


/***   算法2   ***/
int sum = 0,n = 100;		/* 执行一次 */
sum = (1 + n) * n / 2;		/* 执行一次 */
printf("%d", sum);			/* 执行一次 */

函数的渐近增长

(1)现在我们知道了,比较算法的好坏采用事前分析估计方法比较好。那么看一下下面这个代码,看看它的算法运行时间是多少。

(2)

1,首先for (i = 1; i <= n; i++) 为什么是n+1次上面已经解释,不多说了。

2,for (j = 1; j <= n; j++) 因为是在for (i = 1; i <= n; i++)里面,所以它会被执行n次,然后自身又有n+1次,所以最终是n*(n+1)次。

3,c[i][j] = 0;是因为在两个for语句里面,所以是n*n次。

4,for (k = 0; k < n; k++)因为在两个for语句里面,所以前面需要乘n*n。最后因为自身有一个n+1次,所以最终是n*n*(n+1)。

5,因为c[i][j] = c[i][j] + a[i][j] * b[k][j];在三个for语句里面,所以是n*n*n次。

6,最终结果就是,执行时间为2n^{3}+3n^{2}+2n+1

(3)当n=1的时候,执行时间为8。n=10的时候,执行时间为2321。n=100,执行时间为2030201。我们会发现,随着n的增加,最小项的权重越大,其他项可以逐渐被忽略。所以,与我们只会保留最高项!我们将2n^{3}3n^{2}+2n+1比较,会发现随着n的增大,前者会远大于后者。此时我们将n^{3}3n^{2}+2n+1做比较,会发现,依旧随着n的增大,前者会远大于后者。于是我们可以得出结论,与最高项相乘的常数并不重要

	for (i = 1; i <= n; i++)         //n+1
	{
		for (j = 1; j <= n; j++)     //n*(n+1)
		{
			c[i][j] = 0;             //n*n
			for (k = 0; k < n; k++)  //n*n*(n+1)
				c[i][j] = c[i][j] + a[i][j] * b[k][j];//n*n*n
		}

算法的时间复杂度

(1)若有某个辅助函数f(n),使得当n趋近无穷大时,\frac{T(n)}{f(n)}的极限值不为0的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n))为算法的渐近时间复杂度,简称时间复杂度。其中f(n)是问题规模n的某个函数。

(2)想必绝大多数人看到上面这段话是一脸懵的。没错,我也是绝大多数人。我举个例子讲一下我简单理解。

首先,T(n)是我们的算法耗费时间,例如上面的代码T(n)为2n^{3}+3n^{2}+2n+1。那么我假设f(n)为n^{3}\lim_{n->\varpi }\frac{T(n)}{f(n)}=\lim_{n->\varpi }\frac{2n^{3}+3n^{2}+2n+1}{n^{3}}=2,2为常数,所以T(n)的时间复杂度为O(n^{3})。我们将这种利用O()体现算法时间复杂度的记法,称为大O计法

(3)现在我们知道了大O计法了,那么有多数种呢?这些时间复杂度耗费的时间大小关系如何呢?如下

 

这些阶有一些非官方术语

最好,最坏和平均时间复杂度

(1)将算法在最好情况下的时间复杂度为最好时间复杂度,即算法计算量可能达到的最小值;

(2)将算法在最坏情况下的时间复杂度为最坏时间复杂度,即算法计算量可能达到的最大值;

(3)将算法在所有可能情况下,按照输入实例以等概率出现时,算法计算的加权平均值称为平均时间复杂度

 算法空间复杂度

(1)与时间复杂度类似,我们采用渐近空间复杂度。一般情况下,一个程序在机器上执行时,除了需要存储程序本身的指令、常数、变量和输入数据外,还需要存储对数据操作的存储单元。若输入数据所占空间只取决于问题本身,和算法无关,这样只需要分析该算法在实现时所需的辅助单元即可。若算法执行时所需的辅助空间相对于输入数据量而言是个常数,则称此算法为原地工作,空间复杂度为O(1)。

(2)看完上面这段话,相信百分之九十九的人是不明白的。我们只需要知道,空间复杂度与辅助单元有关。那么辅助单元又是啥呢?看下面两个代码。

第一个代码,辅助单元是t,与问题规模n无关,是一个常量,所以空间复杂度为O(1)。

第二个代码,辅助单元是数组b[n],所以空间复杂度为O(n)。

(3)一般情况下,我们是不会考虑空间复杂度的

/***   将一维数组a中的n个数逆序存放到原数组中。   ***/
    //空间复杂度为O(1)
	for (i = 0; i < n / 2; i++)
	{
		t = a[i];
		a[i] = a[n - i - 1];
		a[n - i - 1] = t;
	}

    //空间复杂度为O(n)
	for (i = 0; i < n; i++)
		b[i] = a[n - i - 1];
	for (i = 0; i < n; i++)
		a[i] = b[i];

 

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风正豪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值