1.1什么是算法
我们在生活中时时刻刻都在使用着算法,只是我们自己不知道,比如说家里的电视机坏了,我们需要去维修或者进行更换,这是不是我们解决电视机坏掉的一个方法。大大小小的生活琐事,一道菜品的制作、一台电脑的组装指南等等都是算法,简单来说算法是解决问题的方法,而方法就存在着多种多样、千变万化,算法的设计过程是一个极其灵活且充满智慧的过程,同一个问题有多少人就会有多少种不同的解决方法。学会读懂和设计算法应是每个学计算机的人的基本功,也是基本要求。
1.1.1定义
算法不是问题的答案而是解决问题的操作步骤,在计算机当中,算法是针对特定问题的求解步骤的一系列精准描述,在程序当中指令是一个有限序列,所以算法需要满足以下特性:
(1)有穷性:任意合法的输入,算法都必须要能够在有穷的时间内完成描述。
(2)确定性:算法是形式化、机械化的操作步骤,其中的每一条指令都必须要有确定的含义,且同输入得到同输出。
(3)可行性:算法的每一条确定的指令都能通过已实现的基本操作的有限次数来完成。
在计算机当中算法的指令序列如果不满足其算法的特性,则不能真正意义上称为算法。
例1.1 设计一个算法来求解两个自然数的最大公约数。
解:最大公约数就是这两个自然数的所有公因子的乘积,我们只需要找到这两个自然数的所有公因子即可。例:12=2*2*3,24=2*2*2*3,则12和24的公因子有2、2、3,因此最大公约数为2*2*3=12。设两个自然数为m和n,算法可设计如下:
第一步:找出m的所有质因子。
第二步:找出n的所有质因子。
第三步:找出第一步和第二步所有的公因子。
第四步:将所有的公因子进行乘积即可得到m和n的最大公约数。
但是上述方法却不能称之为一个确定的算法,因为第一步和第二步没有明确定义怎样找出一个自然数的所有质因子,而且在计算机当中分解质因数问题是一个NP类问题(后续会进行讲解),目前没有行之有效的解决方法;第三步当中也没有明确如何在两个长度不相等的序列中找出相同所有相同的元素。因此,该问题的求解过程充满了不确定性和不可行性,不满足算法的特性。
1.1.2描述方法
算法设计人员在完成算法构思后,必须要清楚的将所设计的算法求解步骤记录下来,也就是描述算法。当然使用自然语言进行描述可以很容易进行书写、理解,但是缺点也很明显:一是容易出现二义性问题,不满足算法特性的确定性;二是语句较长,易导致算法冗余;三是自然语言的抽象级别较高,不容易转换为计算机语言程序,故而自然语言通常用来大致的描述算法的基本思想。以下就来介绍几种常见的算法描述方法:
流程图:使用一些ANSI规定的基本符号来表示算法或程序流程图,优点是直观易懂、控制流程随意,缺点是严谨性不高。目前一般用来描述程序设计语言的基本语法。
使用程序语言描述的算法可由计算机直接执行,进而要求了设计者掌握程序设计语言和编程环境,导致算法设计者拘泥于描述算法的细节,忽略了算法“好”和逻辑正确性。
伪代码:一种介于自然语言和程序设计语言之间,使用某一种程序设计语言的基本语法,通常操作指令结合自然语言来进行设计,需要注意的是伪代码并不是一种实际的编程语言,而是在表达能力上类似于编程语言,优点是极小化了算法不必要的技术细节,被称为“算法语言”或“第一语言”。
例1.2 使用伪代码描述欧几里得算法。
解:欧几里得算法用于求解自然数丢的最大公约数。起基本思想是利用辗转相除法将自然数直除到余数为零。例如,M=35,N=25,如下图所示求解步骤,当余数R=0时,除数N就是所求的最大公约数。
则算法如下 :
算法名称:欧几里得算法
输入:两个自然数M和N
输出:M和N的最大公约数
1.R = M mod N
2.循环直到R = 0:
M = N;
N = R;
R = M mod N;
3.输出N;
1.2什么是好算法
1.2.1评价算法的标准
(1)正确性:满足具体问题的需求,对于任意合法的输入都能得到正确的结果。
(2)健壮性:对于错误的输入算法应能力及时做出反应并处理,而不是置之不理或陷入瘫痪。
(3)可理解性:算法设计者要明白算法是为了人的阅读和理解,然后才是程序的实现,故而算法要易于理解、易于转化为程序。
(4)抽象分级:若算法的步骤太多,则需要使用抽象分级来表达算法基本思想,简而言之,算法的求解步骤太多,可以将某些步骤进行整合,作为一个较抽象的处理,然后利用其他算法描述这个较为抽象的处理。
(5)高效性:算法效率包括时间和空间,好的算法应具备较短的时间和占用尽可能少的辅助空间。
算法是计算机科学的基石,也是推动计算机技术革新的推动力,学习和研究算法训练可以帮助我们提供计算思维能力,而算法训练一般按照“问题-->想法-->算法-->程序”的一般过程进行训练,类似于一种思维体操运动,可以在潜移默化中提高计算思维能力。作为程序员,理应具备一定的算法功底,以及将算法转化程序的能力。
1.3如何设计算法
1.3.1基本数据结构
数据结构简单来说是相互之间存在一定关系的数据元素的结合。通常包含以下几种:
线性表:包含n个数据元素的有限序列,简称表,记作:L = (a1,a2,....an),其中,L表示一个线性表,ai称为数据元素,下角标i表示该元素在线性表中的位置或者是序号。简单来说,线性表就像糖葫芦串一样,一个接着一个。
栈:限定在一端进行插入或者删除的线性表,允许插入和删除的一端栈顶,另一端称为栈底,并且栈具有后进先出的特性。
队列:只允许在一端进行插入,称为队尾,另一端进行删除,称为队头,具有先进先出的特性。
树:是n个节点的有限集合。任意一棵空树需要满足如下条件:
(1)有且只有一个特定的称为根节点。
(2)当n>1时,除去根节点以外的其余节点被分成m个互不相交的有限集合,其中每个集合又是一棵树,称为根节点的子树。
图:由n个顶点的有限集合和顶点之间的集合组成,通常表示:G = (V,E),G表示图,V是顶点的集合,E是顶点之间的集合。需要注意的是,图中任意两个之间的边没有方向,则称为无向图,否则就是有向图。权是图中对边赋予的数量值,边上带权的图称为权图或者是网图。
1.3.2 重要问题类型
1.查找问题
查找:在一个数据集合中查找满足给定条件的记录。但是没有一种查找算法是对于任何情况都是合适的,需要算法设计者仔细进行思考,针对特定查找问题必须仔细地设计数据结构和算法,才能在各种操作的需求需求之间达到一个平衡。
2.排序问题
排序:将一个记录的无序序列调整为一个有序序列的过程。排序的主要目的为了进行快速查找,作为辅助步骤,供算法设计者更好的设计算法。
3.图问题
图问题:在计算机中有些图问题是复杂抽象的,例如TSP问题(货郎担问题、邮递员问题、售货员问题)经典问题(后续对其进行算法设计并讲解)。
4.组合问题
组合问题:一般都是最优解问题,简单来说就是寻找一个组合对象,使这个组合对象能够满足特定的约束条件并使得某个目标函数取得极值。组合问题在计算机领域当中属于最难解问题,一是随着问题规模的增大,组合对象的数量增长极快,即使是中等大小的实例,其组合问题的对象的数量也会达到数十亿的数量级,甚至更夸张,从而产生组合爆炸;二是对于绝大多数组合问题目前在计算机领域中尚未找到行之有效的方法。
5.数学问题
数学问题:在数学领域中涉及到许多的专业知识,一个计算机程序设计者要能够熟练设计程序,必须要有扎实的数学功底,之后的讲解仅涉及初等数学、矩阵、数学游戏等基础性大学学科数学知识。
6.几何问题
几何问题:随着计算机图形学的发展,像机器人、断层X摄像技术的研究探讨,激发了广大算法设计者强烈兴趣,之后只讲解两个几何问题:最近对问题和凸包问题。
1.3.3 算法设计的一般步骤
算法设计过程会随着实际问题的变化而千奇百怪,所以一般的设计过程可以在一定程度上辅助算法设计者进行算法设计。
1.分析问题
对于带求解的问题,首先需要了解求解的目标是什么,给出的已知信息。准确的理解算法的输入和输出,也就是明确算法的入口和出口,明确要求算法做什么。
2.选择设计算法技术
算法设计策略是算法设计的一般性方法,可以解决不同计算机领域的诸多问题,根据问题的具体要求选择合适的算法设计技术是算法设计者必备的功底。
3.设计并描述算法
构思好一个算法后,需要及时的描述记录算法,利用自然语言、流程图、伪代码,程序设计语言进行描述。
4.手动验算算法
算法的逻辑错误计算机是无法检测出来的,计算机只会运行程序代码并不会理解动机,从而找出程序的逻辑错误,此时就需要进行跟踪算法,像计算机一样用一个具体的输入实例进行验算,从而发现其中的逻辑错误。
5.分析算法的效率
即分析算法的时间和空间复杂度。
6.实现算法
算法设计者按照需要的程序语言将算法转化为计算机程序,并使其运行且没有错误。
1.3.4代码的优化技巧
优化代码之前必须要优化解题方案,优化代码需要在原功能不变的前提下,重写局部代码,使其逻辑严密,下面介绍几种优化技巧:
1.常量计算
在运行中值不变的数据或语句表达式,要尽量在常量说明语句中进行赋值,编译时分配存储单元并赋值,可以节省代码执行时间。
2.算数运算
需要注意的是,在计算机中各种算数运行的时间差异很大,按照以下原则可以提高代码效率:
原则一:乘除的运算速度比加减慢的多,尽量使用加减来代替乘除。
原则二:高次整幂采用降阶操作,可防止中间项的值过大或过小而造成溢出。
3.用位运算代替除法和取模运算
数据的位是可以操作的最小数据单位,理论上所有的位运算都可以完成计算机操作,合理的位运算可以提高程序运行的效率。
4.避免重复计算
对于重复计算的结果值,可以采用中间值进行存储,虽然多了一个中间值,但是在实际编译是编译器也会为中间值分配存储单元,不会增加开销。
5.有利于编译优化
这里需要学习编译原理才能更好的了解编译优化的进行,简单来讲需要按照程序技术规定的方式进行书写代码,可以提高编译效率。
6.优化逻辑运算
大多数程序语言的在进行逻辑运算表达式的值时都会有短路功能,所谓短路就是指自左向右计算,一旦得到了表达式的值就会跳出表达式的计算,合理的逻辑优化运算可以避免和消除冗余。
7.合理的条件表达式排序
一般按照表达式成立的概率来安排分支语句,即把执行概率较大的语句放在最前面。
8.改善循环结构
一般来讲,程序执行的时间主要耗费在循环结构上,提高循环结构的执行效率会产生累积效应。
原则一:不滥用循环执行循环语句是,有赋初值、条件判断和增值等开销,原则上能用表达式来实现的功能尽量不采用循环语句。例:
fro (i = 0; i < 3; ++i)
sum += a[i];
可改为:
sum += a[0] + a[1] + a[2];
原则二:合理安排嵌套循环。在不影响程序逻辑的前提下,将循环次数较多者作为内循环,例:
fro (i = 0; i <= 10; ++i) #执行11次
fro (j = 1; j <= 100; ++j) #执行10 * 101次
x++; #执行10 * 100次,共执行2021次
改为:
fro ( j = 1; j <= 100; ++j) #执行 101 次
fro (i = 1; i <= 10; ++i) #执行 100 *11 次
x++; #执行 100 * 10 次,共执行2021次
原则三:合并循环,避免冗余循环,使循环体尽可能一次执行多的工作。例:
for (max = a[0] , i = 1; i < n; ++i)
if (max < a[i])
max = a[i];
for (min = a[0] , i = 1; i < n; ++i)
if (min > a[i])
min = a[i];
改为:
for (max = a[0] , i = 1; i < n; ++i)
if (max < a[i])
max = a[i];
if (min > a[i])
min = a[i];
原则四:循环不变式外提。即将与循环变量无关的操作提到循环外面,可以提高代码运行效率。例:
for (i = 1; i < 100; ++i)
sum = x * y + a[i];
#改为
temp = x * y;
fro ( i = 1; i < 100; ++i)
sum = temp + a[i];
原则五:循环无开关。循环体中如果出现与循环变量无关的判断,则可以在循环外面进行判断。例:
fro (i = 0; i < 100; ++i)
if(x > 5)
c[i] = a[i] + b[i];
else
c[i] = a[i] - b[i];
#改为
if(x > 5)
fro (i = 0; i < 100; ++i)
c[i] = a[i] + b[i];
else
fro (i = 0; i < 100; ++i)
c[i] = a[i] - b[i];