算法引入——定义及复杂度

前言

学习数据结构,算法是基础,本篇主要介绍算法及其复杂度,附例子及代码。

一、什么是算法

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:内容是听完网课后自己整合的,代码非原创。

猜你喜欢

转载自blog.csdn.net/qq_51308373/article/details/113993305