数据结构和算法(一):算法分析

数学基础

四个定义

  1. 定义:如果存在正常数c和n0使得当N>=n0 时T(N)<=cf(N),则记为T(N)=O(f(N));

  2. 定义:如果存在正常数c和n0使得当N>=n0时T(N)>=cg(N),则记为T(N)=Ω(f(N));

  3. 定义:当且仅当T(N)=O(f(N))且T(N)=Ω(f(N)),则记为T(N)=θ(h(N));

  4. 定义:T(N)=O((p(N))且T(N)≠θ((p(N)),则记为T(N)=o((p(N))。

    通俗来说:
    定义1:大于等于
    定义2:小于等于
    定义3:等于
    定义4:小于

三个法则

在这里插入图片描述典型的增长率
在这里插入图片描述

模型

我们为了在一个框架当中分析算法,假设了一个非常理想化的框架:一台标准的计算机,它里面的指令都被顺序执行,在做任何一件简单工作的时候都会是消耗相同的时间的,有固定范围的整数,并且有无限的内存。

要分析的问题

要分析的最重要资源就是时间,在这里只分析两个主要因素:使用的算法以及算法的输入。一般需要考虑的是平均的情况Tavg(N)和最坏的情况Tworst(N)。

最大子序列问题

题目:给定整数A1,A2,…AN (可能有负数),求 ∑ k = 1 j A k \sum_{k=1}^{j} A_k k=1jAk的最大值(为了方便起见,如果所有整数均为负数,则最大子序列和为0)。
例如:输入-2,11,-4,13,-5,-2时,答案为20(从A2到A4)。
给出四种算法
在这里插入图片描述运行时间的增长率
在这里插入图片描述在这里插入图片描述

运行时间的计算

计算的是运行的上界,也就是大O的运行时间,这就需要抛弃一些低阶项。

简单举例

计算 ∑ i = 1 N i 3 \sum_{i=1}^{N} i^3 i=1Ni3的一个简单片段为:

int Sum(int N)
{
	int i,	Sum;
	Sum=0;				//第1行
	for(i=1;i<=N;i++)	//第2行
		Sum+=i*i*i;		//第3行
		return Sum;		//第4行
}

分析:

  1. 声明不计时间;
  2. 第1行和第4行各占一个时间单元;
  3. 第3行每执行一次占用4个时间单元,即两次乘法、一次加法和一次赋值),一共执行N次;
  4. 第2行所有的语句初始化的时候花费的是1个时间单元,所有的测试语句(i<=N)花费的是N+1个时间单元,所有的自增运算(i++)花费的是N个时间单元,共2N+2个时间单元;
  5. 忽略调用和返回值的开销,得到的总量是6N+4;
  6. 因此说该函数是O(N)。

四个一般法则

在这里插入图片描述

分析的基本策略是从内部(或最深部分)向外展开的;
如果有函数调用,那么这些调用要首先分析;
如果有递归过程,那么存在几种选择(以后会讨论)。

递归的例子

long int Factorial(int N)
{
	if(N<=1)
		return 1;
	else
		return N*Factorial(N-1);
}

上面这个递归的例子实际上只是一个简单的for循环,从而运行时间为O(N)。

递归转化成一个简单的循环结构是相当困难的,分析将涉及到求解一个递推关系。为了观察这种能发生的情形,考虑下列程序,实际上这个程序效率极其低下。

long int Fib(int N)
{
	if(N<=1)					 //第一行时间需求为1
		return 1;				 //第二行时间需求为1
	else
		return Fib(N-1)+Fib(N-2);//第三行时间需求为T(N)=T(N-1)+T(N-2)+2
}

T(N)=T(N-1)+T(N-2)+2中的“2”指的是第一行上的工作加上第三行的加法。
这个程序中Fib(N)=Fib(N-1)+Fib(N-2),因此由归纳法可求得T(N)>=Fib(N)>=(3/2)N。可以看出这个运行时间呈指数增长,因此效率低下。

深入探讨:
这个程序缓慢的原因是,做了很多多余的工作,违反了合成效益法则[^1],在第三行的第一次调用即Fib(N-1)实际上计算了Fib(N-2),而这个信息被抛弃而在第三行上的第二次调用时又重新计算了一遍。抛弃的信息量递归地合成起来并导致巨大的运行时间。

但有一说一,递归也可以有出色的应用,会在以后讨论。

最大子序列求和问题

现在我们将要叙述四个算法来求解早先提出的最大子序列问题。

算法1:穷举所有可能

int MaxSubsequenceSum(const int A[],int N)//传入一个总长和一个数组
{
	int ThisSum,MaxSum,i,j,k;
	MaxSum=0;
	for(i=0;i<N;i++)//设置起点位置,从i开始加
		for(j=i;j<N;j++)//设置终点位置,加到j结束
		{
			ThisSum=0;
			for(k=i;k<=j;k++)//设置变量,让k从i到j,逐渐相加
				ThisSum+=A[k];
			if(ThisSum>MaxSum)
				MaxSum=ThisSum;
		}
		return MaxSum;
}

三个for循环里面, ∑ i = 0 N − 1 ∑ j = i N − 1 ∑ k = i j 1 \sum_{i=0}^{N-1} \sum_{j=i}^{N-1}\sum_{k=i}^{j}1 i=0N1j=iN1k=ij1可以算出ThisSum+=A[k]被执行的次数。
在这里插入图片描述该算法O(N3)。

算法2:分而治之

int MaxSubsequenceSum(const int A[],int N)
{
	int ThisSum,MaxSum,i,j;
		MaxSum=0;
	for(i=0;i<N;i++)
	{
		ThisSum=0;
		for(j=i;j<N;j++)
		{
			ThisSum+=A[j];
			if(ThisSum>MaxSum)
				MaxSum=ThisSum;
		}
	}
	return MaxSum;
}

该算法O(N2)

算法3:递归调用

static int MaxSubSum(const int A[],int Left,int Right)
{
	int MaxLeftSum,MaxRightSum;
	int MaxLeftBorderSum,MaxRightBorderSum;
	int LeftBorderSum,RightBorderSum;
	int Center,i;

	if(Left==Right)//左右界相等,说明只有一个元素
		if(A[Left]>0)
			return A[Left];
		else
			return 0;

	Center=(Left+Right)/2;//求中间部分
	MaxLeftSum=MaxSubSum(A,Left,Center);
	MaxRightSum=MaxSubSum(A,Center+1,Right);
	/*这两个递归不断进入这个函数知道Left=Right*/

	MaxLeftBorderSum=0;
	LeftBorderSum=0;
	for(i=Center;i>=Left;i--)
	{
		LeftBorderSum+=A[i];
		if(LeftBorderSum>MaxLeftBorderSum)
			MaxLeftBorderSum=LeftBorderSum;
	}
	/*左边子序列求和*/

	MaxRightBorderSum=0;
	RightBorderSum=0;
	for(i=Center+1;i<=Right;i++)
	{
		RightBorderSum+=A[i];
		if(RightBorderSum>MaxRightBorderSum)
			MaxRightBorderSum=RightBorderSum;
	}
	/*右边子序列求和*/

	return Max(MaxLeftSum,MaxRightSum,MaxLeftBorderSum+MaxRightBorderSum);
	/*Max是求三个中最大值的函数*/
}

int MaxSubsequenceSum(const int A[],int N)
/*求最大数列和的函数*/
{
	return MaxSubSum(A,0,N-1);
}

算法分析:
令T(N)是求解大小为N的最大子序列和问题所花费的时间。如果N=1,则算法执行的是

if(Left==Right)//左右界相等,说明只有一个元素
	if(A[Left]>0)
		return A[Left];
	else
		return 0;

花费的时间是一个常量,我们称之为一个时间单元,T(1)=1;

对于下面这段

for(i=Center;i>=Left;i--)
	{
		LeftBorderSum+=A[i];
		if(LeftBorderSum>MaxLeftBorderSum)
			MaxLeftBorderSum=LeftBorderSum;
	}
	/*左边子序列求和*/

	MaxRightBorderSum=0;
	RightBorderSum=0;
	for(i=Center+1;i<=Right;i++)
	{
		RightBorderSum+=A[i];
		if(RightBorderSum>MaxRightBorderSum)
			MaxRightBorderSum=RightBorderSum;
	}
	/*右边子序列求和*/

花费的时间为O(N)

对于

    Center=(Left+Right)/2;//求中间部分
	MaxLeftSum=MaxSubSum(A,Left,Center);
	MaxRightSum=MaxSubSum(A,Center+1,Right);
	/*这两个递归不断进入这个函数知道Left=Right*/

这两行求解大小为N/2的子序列问题(假设N是偶数)。因此,这两行每行花费的T(N/2)个时间单位,共花费2T(N/2)个时间单元。

因此算法花费的总时间是2T(N/2)+O(N)
由递归推导可以发现规律O(N logN),推导过程如下
在这里插入图片描述

算法4:简单有效的算法

int MaxSubsquenceSum(const int A[],int N)
{
	int ThisSum,MaxSum,j;
	ThisSum=MaxSum=0;
	for(j=0;j<N;j++)
	{
		ThisSum+=A[j];
		if(ThisSum>MaxSum)
			MaxSum=ThisSum;
		else if(ThisSum<0)
			ThisSum=0;
	}
	return MaxSum;
}

该算法花费的时间是O(N)

运行时间中的对数

对数最常出现的规律可以概括为以下法则

如果一个算法用常数时间(O(1))将问题的大小削减为其一部分(通常是1/2),那么该算法就是O(logN)。另一方面,如果使用常数时间只是把问题减少一个常数(如将问题减少1),那么这种运算就是O(N)的。

下面例举具有对数特点的三个例子:

对分查找

给定一个整数A0 ,A1 ,……,AN , 后者已经预先排序并在内存中,求使得Ai=X的下标i,如果X不在数据中,则返回i=-1。

int BinarySearch(const ElementType A[],ElementType X,int N)
{
	int Low,Mid,High;
	Low=0;High=N-1;
	while(Low<=High)
	{
		Mid=(Low+High)/2;
		if(A[Mid]<X)
			Low=Mid+1;
		else
			if(A[Mid]>X)
				High=Mid-1;
			else
				return Mid;
	}
	return NotFound;
}

算法分析:
每次迭代在循环内所有的工作花费为O(1),因此需要确定循环的次数,循环从High-Low=N-1开始并在High-Low>=-1结束。每次循环后High-Low的值至少将该次循环前的值折半;于是循环的次数最多为[log(N-1)]+2。

欧几里得算法

计算最大公因式,也就是辗转相除法。

unsigned int
Gcd(unsigned int M,unsigned int N)
{
	unsigned int Rem;
	while(N>0)
	{
		Rem=M%N;
		M=N;
		N=Rem;
	}
	return M;
}

该算法花费的时间是O(logN)。

具体推导可根据下列定理:
如果M>N,则M mod N<M/2。

幂运算

计算XN,采用递归算法。如果N是偶数,我们有XN/2·XN/2,如果N是奇数,则XN=X(N-1)/2·X(N-1)/2·X

long int Pow(long int X,unsigned int N)
{
	if(N==0)
		return 1;
	if(N==1)
		return X;
	if(IsEven(N)
			return Pow(X*X,N/2);
	else 
		return Pow(X*X,N/2)*X;
}

程序花费的时间为O(logN).

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值