算法
什么是算法
算法是求解问题的一系列计算步骤,用来将输入数据转换成输出结果 :
graph LR A[输入] B[算法] C[输出] A-->B B-->C
如果一个算法对其每一个输入实例,都能输出正确的结果并停止,则称它是正确的。
算法,满足的目标
- 正确性
- 可使用性
- 可读性
- 健壮性
- 高效率与低存储量需求
例子
-
【例1.1】以下算法用于在带头结点的单链表h中查找第一个值为x的结点,找到后返回其逻辑序号(从1计起),否则返回0。分析该算法存在的问题。
int findx(LNode *h;int x) { LNode *p=h->next; int i=0; while (p->data!=x) { i++; p=p->next; } return i; }
-
解:问题
-
当单链表中首结点值为x时,该算法返回0,此时应该返回逻辑序号1。
-
当单链表中不存在值为x的结点时,该算法执行出错,因为p为NULL时仍执行p=p->next。
-
所以该算法不满足正确性和健壮性。应改为:
int findx(LNode *h;int x) { LNode *p=h->next; int i=1; while (p!=NULL && p->data!=x) { i++; p=p->next; } if (p==NULL) return 0; else return i; }
-
-
算法具有以下五个重要特征
- 有限性
- 确定性
- 可行性
- 输入性
- 输出性
例1.2
//描述1
void exam1()
{ int n;
n=2;
while (n%2==0)
n=n+2;
printf("%d\n",n);
}
//描述2
void exam2()
{ int x,y;
y=0;
x=5/y;
printf("%d,%d\n",x,y);
}
- 这两段描述均不能满足算法的特征,试问它们违反了算法的哪些特征?
- 解:(1)是一个死循环,违反了算法的有限性特征。(2)出现除零错误,违反了算法的可行性特征。
算法的描述
以设计求1+2+…+n值的算法为例说明C/C++语言描述算法的一般形式,该算法如下:
返回值 形参列表
bool fun(int n,int s)
{
if (n<0) return false;
s=0;
for (int i=1;i<=n;i++)
s+=i;
return true;
}
-
用函数的返回值表示算法能否正确执行。
-
用形参表示算法的输入输出。
-
C语言中只有从实参到形参的单向值传递,若改变形参而对应的实参不会同步改变。
-
例如,设计以下主函数调用上面的fun函数:
void main() { int a=10,b=0; if (fun(a,b)) printf("%d\n",b); else printf("参数错误\n"); }
执行时发现输出结果为0,因为b对应的形参为s,fun执行后s=55,但s并没有回传给b。
可以用传指针方式实现形参的回传,但增加了函数的复杂性。
C语言中增加了引用型参数,参数名前需加上&,形参在执行后会将结果回传给对应的实参。上例采用C语言描述算法如下所示。
bool fun(int n,int &s) { if (n<0) return false; s=0; for (int i=1;i<=n;i++) s+=i; return true; }
当形参s改为引用类型的参数后,执行main函数的输出结果就输出55。
-
结论:在设计算法时,如果某个形参需要将执行结果回传给实参,需要将该形参设计为引用型参数。
算法和数据结构
既有联系又有区别
-
联系
数据结构是算法设计的基础。算法的操作对象是数据结构,在设计算法时,要构建适合这种算法的数据结构。数据结构设计主要是选择数据的存储方式,如确定求解问题中的数据采用数组存储还是采用链表存储等。算法设计就是在选定的存储结构上设计一个满足要求的好算法。
-
区别
数据结构关注的是数据的逻辑结构、存储结构以及基本操作,而算法更多的是关注如何在数据结构的基础上解决实际问题。算法是编程思想,数据结构则是这些思想的逻辑基础。
算法设计的基本步骤
graph TD a[分析求解问题]-->b[选择数据结构和算法设计策略]-->c[描述算法]-->d[证明算法正确性]-->e[算法分析]
算法分析
算法分析是分析算法占用计算机资源的情况。主要是分析算法的时间复杂度和空间复杂度。
算法时间复杂度分析
时间复杂度分析概述
算法是由控制结构(顺序、分支和循环3种)和原操作(指固有数据类型的操作)构成的,运行时间取决于两者的综合效果。
bool Solve(double a[][MAX],int m,int n,double &s)
{
int i; s=0; //顺序结构
if (m!=n) return false; //分支结构
for (i=0;i<m;i++) //循环结构
s+=a[i][i];
return true; //顺序结构
}
-
设n为算法中的问题规模,通常用大O、大和Θ等三种渐进符号表示算法的执行时间与n之间的一种增长关系。
-
分析算法时间复杂度的一般步骤:
graph TD 算法-->d["分析问题规模n,找出基本语句,求出其运行次数f(n)"]-->p["用o、Ω或Θ表示其阶"]
渐进符号()
-
定义1(),当且仅当存在正常量的上界。
如,因为当n≥2时,。
,因为当n≥2时,。
用来描述增长率的上界,表示$f(n)的增长最多像g(n) $增长的那样快,即当输入规模为n时,算法消耗时间的最大值。这个上界的阶越低,结果就越有价值,所以,对于$10n2+4n+2,O(n2)比O(n^4) $有价值。
一个算法的时间用大O符号表示时,总是采用最有价值的表示,称之为“紧凑上界”或“紧确上界”。
一般地,如果。
-
定义2(读作“,使当下界。
如
大Ω符号用来描述增长率的下界,表示f(n)的增长最少像g(n) 增长的那样快,也就是说,当输入规模为n时,算法消耗时间的最小值。 与大O符号对称,这个下界的阶越高,结果就越有价值,所以,对于$10n2+4n+2,Ω(n2)比Ω(n) Ωf(n)=a_mnm+a_{m-1}n{m-1}+…+a_1n+a_0,有f(n)=Ω(n^m)$。
-
定义3(大Θ符号), 。
。 。 大,的下界。
-
【例1.3】分析以下算法的时间复杂度:
void fun(int n) { int s=0,i,j,k; for (i=0;i<=n;i++) for (j=0;j<=i;j++) for (k=0;k<j;k++) s++; }
-
-
-
算法的最好、最坏和平均情况
-
定义4 设一个算法的输入规模为是所有输入的集合,任一输入出现的概率,有是算法在输入下所执行的基本语句次数,则该算法的平均执行时间为:。
-
算法的平均情况指各种特定输入下的基本语句执行次数的带权平均值。
-
算法的最好情况为:,是指算法在所有输入下所执行基本语句的最少次数。 算法的最坏情况为:,是指算法在所有输入下所执行基本语句的最大次数。
-
-
【例1.4】采用顺序查找方法,在长度为n的一维实型数组a[0…n-1]中查找值为x的元素。即从数组的第一个元素开始,逐个与被查值x进行比较。找到后返回1,否则返回0,对应的算法如下:
int Find(double a[],int n,double x) { int i=0; while (i<n) { if (a[i]==x) break; i++; } if (i<n) return 1; else return 0; }
-
回答以下问题: (1)分析该算法在等概率情况下成功查找到值为x的元素的最好、最坏和平均时间复杂度。 (2)假设被查值x在数组a中的概率是q,求算法的时间复杂度。
-
解:(1)算法的while循环中的if语句是基本语句。a数组中有n个元素,当第一个元素a[0]等于x,此时基本语句仅执行一次,此时呈现最好的情况,即G(n)=O(1)。
当a中最后一个元素a[n-1]等于x,此时基本语句执行n次,此时呈现最坏的情况,即W(n)=O(n)。
对于其他情况,假设查找每个元素的概率相同,则P(a[i])=1/n(0≤i≤n-1),而成功找到a[i]元素时基本语句正好执行i+1次,所以:
(2)当被查值x在数组a中的概率为q时,算法执行有n+1种情况,即n种成功查找和一种不成功查找。 对于成功查找,假设是等概率情况,则元素a[i]被查找到的概率,成功找到a[i]元素时基本语句正好执行i+1次。 对于不成功查找,其概率为1-q,不成功查找时基本语句正好执行n次。 所以:
非递归算法的时间复杂度分析
-
非递归算法,关键是求出代表算法执行时间的表达式。 基本语句的执行次数是一个关于问题规模n的表达式,用渐进符号来表示这个表达式即得到算法的时间复杂度。
-
【例1.6】给出以下算法的时间复杂度。
void func(int n) { int i=1,k=100; while (i<=n) { k++; i+=2; } }
- 解:基本语句是循环内的语句,设执行的次数为,最后取值为,有:
$f(n)=m≤\frac{(n-1)}{2}=O(n)$。 该算法的时间复杂度为$O(n)$。
递归算法的时间复杂度分析
递归算法是采用一种分而治之的方法,把一个“大问题”分解为若干个相似的“小问题”来求解。 关键是根据递归过程建立递推关系式,然后求解这个递推关系式,得到一个表示算法执行时间的表达式,最后用渐进符号来表示这个表达式即得到算法的时间复杂度。
-
【例1.7】有以下递归算法:
void mergesort(int a[],int i,int j) { int m; if (i!=j) { m=(i+j)/2; mergesort(a,i,m); mergesort(a,m+1,j); merge(a,i,j,m); } }
mergesort()用于数组a[0…n-1](设,这里的k为正整数)的归并排序,调用该算法的方式为: mergesort(a,0,n-1); 另外merge(a,i,j,m)用于两个有序子序列a[i…j]和a[j+1…m]的有序合并,是非递归函数,它的时间复杂度为O(n)(这里n=j-i+1)。分析上述调用的时间复杂度。
解:设调用mergesort(a,0,n-1)的执行时间为T(n),由其执行过程得到以下求执行时间的递归关系(递推关系式):
其中,O(n)为merge()所需的时间,设为cn(c为正常量)。因此:
-
【例1.8】求解梵塔问题的递归算法如下,分析其时间复杂度。
void Hanoi(int n,char x,char y,char z) { if (n==1) printf("将盘片%d从%c搬到%c\n",n,x,z); else { Hanoi(n-1,x,z,y); printf("将盘片%d从%c搬到%c\n",n,x,z); Hanoi(n-1,y,x,z); } }
解:设调用Hanoi(n,x,y,z)的执行时间为T(n),由其执行过程得到以下求执行时间的递归关系(递推关系式):
算法空间复杂度分析
一个算法的存储量包括形参所占空间和临时变量所占空间。在进行存储空间分析时,只考察临时变量所占空间。
-
例如,以下算法中临时空间为变量i、maxi占用的空间。所以,空间复杂度是对一个算法在运行过程中临时占用的存储空间大小的量度,一般也作为问题规模n的函数,以数量级形式给出,记作: S(n)=O(g(n))、Ω(g(n))或Θ(g(n)) 其中渐进符号的含义与时间复杂度中的含义相同。
int max(int a[],int n) { int i,maxi=0; for (i=1;i<n;i++) if (a[i]>a[maxi]) maxi=i; return a[maxi]; }
函数体内分配的变量空间为临时空间,不计形参占用的空间,这里的仅计i、maxi变量的空间,其空间复杂度为O(1)。
为什么算法占用的空间只考虑临时空间,而不必考虑形参的空间呢?这是因为形参的空间会在调用该算法的算法中考虑,例如,以下maxfun算法调用max算法:
void maxfun()
{ int b[]={1,2,3,4,5},n=5;
printf("Max=%d\n",max(b,n));
}
maxfun算法中为b数组分配了相应的内存空间,其空间复杂度为O(n),如果在max算法中再考虑形参a的空间,这样重复计算了占用的空间。
int max(int a[],int n)
{ int i,maxi=0;
for (i=1;i<n;i++)
if (a[i]>a[maxi])
maxi=i;
return a[maxi];
}
算法空间复杂度的分析方法与前面介绍的时间复杂度分析方法相似。
【例1.9】分析例1.6算法的空间复杂度。
void func(int n)
{ int i=1,k=100;
while (i<=n)
{ k++;
i+=2;
}
}
解:该算法是一个非递归算法,其中只临时分配了i、k两个变量的空间,它与问题规模n无关,所以其空间复杂度均为O(1),即该算法为原地工作算法。
【例1.10】有如下递归算法,分析调用maxelem(a,0,n-1)的空间复杂度。
int maxelem(int a[],int i,int j)
{ int mid=(i+j)/2,max1,max2;
if (i<j)
{ max1=maxelem(a,i,mid);
max2=maxelem(a,mid+1,j);
return (max1>max2)?max1:max2;
}
else return a[i];
}
解:执行该递归算法需要多次调用自身,每次调用只临时分配3个整型变量的空间(O(1))。设调用maxelem(a,0,n-1)的空间为S(n),有: