前言
学习数据结构,算法是基础,本篇主要介绍算法及其复杂度,附例子及代码。
一、什么是算法
1.定义:
算法:
一个有限指令集
接受一些输入(有些情况下不需要输入)
产生至少一个输出
一定在有限步骤之后终止
每一条指令必须:
(1)有充分明确的目标,不可以有歧义
(2)在计算机能处理的范围之内
(3)描述要抽象,不应该依赖于任何一种计算机语言以及具体的实现手段
2.算法的描述工具:
1.自然语言
2.程序设计语言
3.流程图(框图)
4.伪码语言:
包括 高级程序设计语言的三种基本结构(顺序、选择、循环)和 自然语言成分
5.类c语言:
介于伪码语言和程序设计语言之间的一种表现形式。保留了C语言的精华,不拘泥于C语言的语法细节,同时添加了一些C++的成分
特点:便于理解、阅读;能方便的转换成C语言
3.算法的主题:
<1>函数:用以表示算法
函数类型 函数名 (函数参数表)
{ //算法说明
语句序列
}//函数名
注:(1)算法说明应包括功能说明,输入、输出;
(2)为提高算法可读性,关键位置加以说明;
(3)明确函数实参和形参的匹配规则,以便能正确使用算法函数
<2> 其他说明可附在函数定义后
4.举例
(1)选择排序算法的伪码描述:
其中有两处抽象:
1.list到底是数组还是链表
2.swap用函数还是宏去实现
void SelectionSort (int List[], int N)
{
//将 N 个整数进行递增排序
for (i=0;i<N;i++)
{
//从List[i]到List[N-1]中找最小元,并将其位置赋给MinPosition
MinPosition = ScanFormin( List i,N-1);
//将未排序部分的最小元换到有序部分的最后位置
Swap(List[i],List[Minposition]);
}
}
(2)算法描述举例
问题:设一维数组 a[0…n-1]中有n个整数,其中n为常数,试设计算法:求数组中所有元素最大值
用伪码来表示算法的主要思想:
1.maxai = a[0];
2.i = 1;
3.若i <= n-1,则:
3.1 若a[i] > maxai,则 maxai = a[i];
3.2 i++;
3.3 转3
4.maxai为最大值
*以下为代码实现*
int a_maxint(int a[],int n)
{
int j,maxai = a[0];
for (j=1;j<=n-1;j++)
if(a[j]>maxai)
maxai = a[j];
printf("maxai=&d\n",maxai);
return maxai
}
二、什么是好的算法
1.多项式问题引入
以多项式为例,比较同一问题不同方法的算法优劣。使大家对好坏算法的衡量有初步的判断与感受。
多项式的表示:
例:一元多项式及其运算
一元多项式: f(x) = a0 + a1*x + … + a(n-1)x^(n-1) + an(x^n)
主要运算:多项式相加、相减、相乘等
分析:如何表示多项式
多项式的关键数据:
1.多项式项数 n
2.各项系数 ai 及指数 i
(1)方法1:(最简)顺序存储的直接表示
数组各分量对应多项式各项:
a[i]:项 x^i的系数 ai
下标 i:x的指数
两个多项式相加:两个数组对应分量相加
PS:存在问题,当 x次数很高,要用一个很大的数组
(2)方法2:顺序存储结构表示非零项,节省空间,同样也方便运算
①每一项按照指数大小有序存储,下面例子中指数大的排在前面,指数小的排在后面
②每个非零项涉及两个信息:系数 ai 和指数 i,可将一个多项式看成是一个(ai,i)二元组的集合
③用结构数组表示:数组分量是由系数 ai,指数 i组成的结构,对应一个非零项
eg:P1(x)=9x^12 +15*x^8 + 3x^2
P2(x)=26x^19 - 4x^8 - 13x^6 + 82
下标i 0 1 2 …
系数 ai 9 15 3 …
指数 i 12 8 2 …
//相加过程:从头开始,比较两个多项式当前对应项的指数
P1:(9,12),(15,8),(3,2)
P2:(26,19),(-4,8),(-13,6),(82,0)
如先比较p1第一项和p2第一项指数,指数高的则输出,再比较p1第一项与p2第二项,以此类推
指数一样则系数相加减
结果:P3:(26,19) (9,12) (11,8) (-13,6) (3,2) (82,0)
(3)方法3:链表结构存储非零项
链表中每个结点存储多项式中的一个非零项,包括系数和指数两个数据域及一个指针域
系数:coef 指数: expon
指针域:link
typedef struct PolyNode *Polynomial;
struct PolyNode
{
int coef;
int expon;
Polynomial link;
};
2. 如何衡量算法
(1)空间复杂度 S(n):根据算法写成的程序,在执行时占用存储单元的长度。
注:这个长度往往与输入数据的规模有关。空间复杂度过高的算法可能导致使用的内存超限,造成程序非正常中断
(2)时间复杂度 T(n): 根据算法写成的程序,在执行时耗费时间的长度。
注:这个长度往往与输入数据的规模有关。时间复杂度过高的低效算法可能导致在有生之年都等不到运行结果
时间复杂度曲线(递增)
O(1)<O(log2 n)< O(n)< O(nlog2 n)< O(n^2)< O(n^3) <O(2^n)
分析一般算法效率,经常关注:
1.最坏情况复杂度 Tworst(n)
2.平均复杂度 Tavg(n)
后者<=前者
分析前者较多
*以下举几个例子计算两种复杂度
eg1:递归打印整数 :s(N) = C·N
注:若使用循环,不管 N 多大,始终占用一个固定的空间,空间复杂度更低
#include <stdio.h>
void PrintN (int N)
{
if (N){
PrintN (N-1);
printf ("%d\n",N);
}
return ;
}
int main()
{
int a=10;
PrintN(a);
return 0;
}
eg2.1:多项式的例子,计算T(n),计算做了多少次乘除法即可,加减法可忽略
T(n) = C1n^2+C2n
double f (int n,double a[],double x)
{
int i;
double p=a[0];
for (i=1;i<=n;i++)
p += (a[i] * pow(x,i));// 一个循环内做 i 次,共做了(n*n+n)/2次乘法
return p;
}
eg2.2:T(n) = C*n
double f (int n,double a[],double x)
{
int i;
double p=a[0];
for (i=n;i>0;i--)
p = a[i-1] + x*p;//一个循环内1次乘法,共n次
return p;
}
3.4个例子细说复杂度
(1)eg1:O(1)称为:常量阶/常量数量级
{
int s;
scanf("%d",&s);
s++;
printf("&d",s);
}
其中:语句频度为 f(n)=f(1)=3
时间复杂度 T(n)=O(f(n))=O(3)=O(1)
(2)O(n)称为线性阶/线性数量级
void sum (int a[],int n)
{
int s = 0,i;//1次
for (i=0;i<n;i++)//n次
s = s+a[i];//n次
printf ("&d",s);//1次
}
语句频度:f(n)=1+n+n+1
时间复杂度 T(n)=O(f(n))=O(2n+2)=O(n)
(3)eg3:O(n^2)称为平方阶/平方数量级
void sum (int m,int n)
{
int i,j,s = 0;// 1次
for (i=1li<=m;i++)//m次
{
for (j=1;j<=n;j++)//m*n次
s++;//m*n次
printf("&d",s);//m次
}
}
f(m,n)=1+m+2mn+m=2mn+2m+1
当m=n,f(n)=2n^2+2n+1
T(n)=O(f(n))=O(2n+1+2n^2)=O(n*n)
(4)eg4:冒泡排序
void bubble1 (int a[],int n)
{
int i,j,temp;
for (i=1;i<n;i++)//n-1次
for (j=0;j<n-i;j++)//n(n-1)/2次
if(a[j]>a[j+1])//n(n-1)/2次
{
temp = a[j];//n(n-1)/2或0 次
a[j] = a[j+1];//n(n-1)/2或0次
a[j+1] = temp; //n(n-1)/2或0次
}
for (i=0;i<n;i++)//n
printf("&d",a[i]);//n
}
最坏情况:每次比较都发生数据交换,n^2+2n-1
最好情况:每次比较都不发生数据交换,5n^2/2 + n/(2-1)
T最好=T最坏=O(n^2)
4.复杂度的渐进表示法
(1) **上下界有很多,找最贴合的上下界**
T(n)=O(f(n)) 表示存在常数 C>0,n0>0 使得当 n>=n0时有 T(n)<=C*f(n),即 f(n) 是 T(n)的上界
T(n) = Ω(g(n)) 表示存在常数 C>0,n0>0 使得当 n>=n0时有 T(n)>=C*f(n),即 g(n) 是 T(n)的下界
T(n) = θ(h(n)) 表示同时有 T(n)=O(h(n))和 T(n)=Ω(h(n))
(2)另附:复杂度分析小窍门:
1.若两段算法分别有复杂度 T1(n)=O(f1(n))和 T2(n)=O(f2(n)) ,则
T1(n) + T2(n) = max(Of1(n),Of2(n))
T1(n) * T2(n) = O(f1(n)* f2(n))
2.若 T(n)是关于 n 的 k 阶多项式,那么 T(n)= θ(n^k)
3.一个for循环的时间复杂度=循环次数*循环体代码复杂度
4.if-else结构的复杂度取决于if的条件判断复杂度和两个分支的复杂度总体复杂度取三者中最大
三.应用实例——最大子列和问题
这里以最大子列和问题,提出4种不同的算法,对算法的优劣比较进行更深层剖析。
题目:给定 N个整数的序列 {A1,A2,…,An},函数 f(i,j)=max{0,求和:从 Ai到Aj} ,求函数的最大值。若和为负数,则返回0
ps:以下代码均为函数
(1)算法1:把所有的连续子列和全部算出来,从中找最大的那一个
复杂度 T(n)=O(n^3) ,三重循环嵌套
int MaxSubseqSum1 (int A[],int N)
{
int ThisSum,MaxSum = 0;
int i,j,k;
for (i=0;i<N;i++)//i为子列左端位置
{
for (j=i;j<N;j++)//j为子列右端位置
{
ThisSum = 0;//ThisSum是从A[i]到A[j]的子列和
for (k=i;k<=j;k++)
{
ThisSum += A[k];
}
if (ThisSum > MaxSum) //若刚得到的子列和更大,则更新结果
MaxSum = ThisSum;
}
}
return MaxSum;
}
(2)算法2:把所有的连续子列和全部算出来,从中找最大的那一个,k循环作废
复杂度 T(n)=O(n^2) ,两重循环嵌套
int MaxSubseqSum2 (int A[],int N)
{
int ThisSum,MaxSum = 0;
int i,j,k;
for (i=0;i<N;i++)//i为子列左端位置
{
ThisSum = 0;//ThisSum是从A[i]到A[j]的子列和
for (j=i;j<N;j++)//j为子列右端位置
{
ThisSum += A[j];//对于相同的i,不同的j,只要在前一次循环的基础上累加1项即可
if (ThisSum > MaxSum) //若刚得到的子列和更大,则更新结果
MaxSum = ThisSum;
}
}
return MaxSum;
}
(3)算法3:分而治之
运算过程:复杂度 T(n)=左边复杂度+右边复杂度+跨越中间复杂度 =2T(n/2)+cn
把 T(n/2)展开得,左边=4T(n/4)+2cn ,继续展开得
左边=+ckn+ 2^kO(1)
其中n/(2^k)=1
则左边=nO(1)+cn*log2 n ,n=1时是常数
相加时取较大那一项,即 O(NlogN)
递归:从中间开始不停的对半分,算出分界线两边的最大子列和,再算出跨越分界线的最大子列和,即可得出结论
int Max3(int A,int B,int C)//返回3个整数中的最大值
{
return A > B ? A > C ?A : C : B > C ? B : C;
}
int DivideAndConquer (int List[],int left,int right)//分治法求List[left]到List[right]的最大子列和
{
int MaxLeftSum,MaxRightSum;//存放左右子问题的解
int MaxLeftBorderSum,MaxRightBorder;//存放跨分界线的结果
int LeftBorderSum,RightBorder;
int center,i;
if (left==right)//递归终止条件,子列仅1个数字
{
if (List[left]>0)
return List[left];
else return 0;
}
//以下为“分”
center = (left + right)/2;//找到中分点,递归求两边子列最大和
MaxLeftSum = DivideAndConquer (List,left,center);
MaxRightSum = DivideAndConquer (List,center,right);
//下面求跨分界线的最大子列和
MaxLeftBorderSum = 0;
LeftBorderSum = 0;
for (i=center;i>=left;i--)//从中线向左扫描
{
LeftBorderSum += List[i];
if (LeftBorderSum>MaxLeftBorderSum)
MaxLeftBorderSum = LeftBorderSum;
}//左边扫描结束
MaxRightBorderSum = 0;
RightBorderSum = 0;
for (i=center+1;i<=Right;i++)//从中线向右扫描
{
RightBorderSum += List[i];
if (RightBorderSum>MaxRightBorderSum)
MaxRightBorderSum = RightBorderSum;
}//右边扫描结束
//下面返回“治”的结果
return Max3 (MaxLeftSum,MaxRight,MaxLeftBorderSum+MaxRightBorderSum);
}
int MaxUbseqSum3 (int List[],int N)//保持与前两种算法相同的函数接口
{
return DivideAndConquer (List,0,N-1);//调用函数
}
(4)算法4:在线处理
在线:每输入一个数据就能进行即时处理,在任何一个地方中止输入,算法都能正确给出当前的解
复杂度 T(n)=O(n),线性,副作用:难以理解
int MaxSubseqSum4 (int A[],int N)
{
int ThisSum,MaxSum;
int i;
ThisSum = MaxSum = 0;
for (i=0;i<N;i++)//i为子列左端位置
{
ThisSum += A[i];//向右累加
if (ThisSum > MaxSum) //若刚得到的子列和更大,则更新结果
MaxSum = ThisSum;
else if (ThisSum<0)//若当前子列和为负数,则不可能使后面的部分和增大,抛弃
ThisSum = 0;
}
return MaxSum;
}
总结
以上简要介绍算法复杂度,通过几个例子进行比较,会更详细一些。感兴趣可以去找找复杂度的函数图形,会更加直接一些。
如有错误,欢迎指正。
ps:内容是听完网课后自己整合的,代码非原创。