浙大数据结构算法效率笔记补充,算法复杂度 分治法 在线处理

前言

      本文撰写基于b站上陈越老师的数据结构课程,笔者将致力于详尽的记录下其中的基础知识点,以及对于一些抽象的概念进行自己的尝试与思考。

复杂度的渐进表示法

7bccf03681b64d4ba34d02f5577b3882.png这种形式表示f(n)为T(n)的某种上界

 

1325d8217aeb4f80a546ea9178e24fdb.png同样的,这种形式表示g(n)为T(n)的某种下界

 

ce6ba10e779b498eae73be4adfb530f2.png

这种形式表示h(n)同时为T(n)的上界和下界,即f(n)和T(n)基本等价。

注意d34aa33f2eb543239fe993c99693a4d8.png这里取最小上界,7250dc0e14f5477ea28bd6d8122abc42.png这里取最大下界,否则,上界可以无穷大,下界可以无穷小,就没有意义了。

补充说明:其实再实际想想,如果数据量很小,就不用太去关注算法的效率,因为都很快。只有当数据量很大时不同算法效率不同带来了明显的时间参差,我们就会想着如何去提高算法效率,所以复杂度的渐进表示法实际也是讨论在n足够大的情况。

应用实例:最大子列和问题

题目

4e7723face6946139b57345956af19bf.png

算法1 

int MaxSubseqSum1(int A[], int N)
{
	int ThisSum=0, MaxSum = 0;
	int i, j = 0, 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;//则更新结果 
		}//j循环结束 
	}//i循环结束
	return MaxSum;
}

理解:

可以看出,此算法使用了三层循环,分别第一层第二层指出了所有序列和可能包括的首(i)尾(j)序号,然后再使用k来从首到尾遍历。

时间复杂度:

三重循环,每重循环都可能进行n次,所以复杂度为O(n^3) 

 

算法2

int MaxSubseqSum2(int A[], int N)
{
	int ThisSum=0, MaxSum = 0;
	int i = 0, j = 0;
	for (i = 0; i< N; i++)//i是子列左端位置 
	{
		ThisSum = 0;//ThisSum是从A[i]到A[j]的子列和 
		for (j = i; j < N; j++)//j是子列右端位置 
		{
			ThisSum += A[j];
			if (ThisSum > MaxSum)//如果刚得到的这个子列和更大 
				MaxSum = ThisSum;//则更新结果 
		}//j循环结束 
	}//i循环结束
	return MaxSum;
}

理解: 

没有将求和与序列左右端的序号那么割裂开来而是内层讨论i恒定情况,随j++和的逐次变化,再在外层变化i,内层中若加上了新的A[j]使ThisSum超过MaxSum,则代替,相较于第一种算法,规避了类似A[0]+A[1]+A[2],A[0]+A[1]+A[2]+A[3]中两个明明有重复部分却都独立的从头计算的问题,在算法2中算后者结果可以直接在前者上+A[3]。

时间复杂度:

双重循环,每重循环都可能进行n次,所以复杂度为O(n^2)

 

算法3 分而治之

int MaxSubseqSum3(int* A, int size)
{
	int max(int a, int b);
	if (size <= 1)
	{
		if (*A > 0)
			return (*A);
		else
			return 0;
	}
	int mid;
	mid = size / 2;//二分 
	int* left = A;//标记那一段的最左边 
	int* right = (A+mid);//标记那一段的最右边 
	int i;
	int maxSubseqSum;
	int ans;
	maxSubseqSum = max(MaxSubseqSum3(left, mid), MaxSubseqSum3(right, size - mid));//递归的思路找出左右边各自的连续子列和最大值 
	int sum = 0, lmax = *(A + mid), rmax = *(A + mid+1);//跨过中间界限的情况 
	for ( i = mid; i >=0; i--)//从中间往左连续的相加找出过分界线左边的连续序列最大值 
	{
		sum += *(A + i);
		if (sum > lmax)
			lmax = sum;
	}
	sum = 0;
	for ( i = mid + 1; i <=size; i++) {//从中间往右连续的相加找出过分界线右边的连续序列最大值 
		sum += *(A + i);
		if (sum > rmax)
			rmax = sum;
	}
	ans = rmax + lmax;//左右相加即得出跨过中间界限的连续序列最大值 
	maxSubseqSum = max(maxSubseqSum, ans);//中间的和两边最大值一比较,得出这一段的最大值 
	return maxSubseqSum;
    }

 笔者这里将传入的数组变成了传入数组的指针,为的是可以更加灵活的在不断的二分中分好区间,若是创建数组来储存区间,则可能导致一些编译错误,比如形式参数的值不能当作常量这种。

理解

分而治之呢就是不断的将大区间用小区间替代,递归到区间只有一两个,再逐渐返回值到较大区间。

本题的一个小核心呢就是区间内的最大子序列和可以通过两边二分区间各自的最大序列和与跨过分界线的最大序列和比较,从而得出整个区间内最大的序列和。

如图

bdff74e846f3493ba564d7f3c1a979d7.png

时间复杂度:

nlogn

解析:

由上述代码及理解可看出本算法的运算核心是两个

1.二分递归

2.从中界遍历整个区间

所以得出时间复杂度关于数组大小N的一个递推公式

c58aed519f30483db4b74d7f8c10c961.png

N替换为N/2

T(N/2)=2T((N/2)/2)+cN/2

T(N/2)代入T(N)式子中

则得

95eefc76a74b4b018d580ac1cd63dc31.png

一直展开,直到第k步

db9c4e3a3dda443c9038ed3ecd6a3b48.png

k=log₂N

可以看出前半部分是一个常数,当N无穷大的时候可以忽略掉,后半部分为c(log₂N)N,根据复杂度定义,则复杂度为nlogn。

 

算法4在线处理

int MaxSubseqSum4(int A[], int N)
{
	int ThisSum=0,MaxSum=0;
	int i;
	for(i=0;i<N;i++)
	{
	  ThisSum+=A[i];//向右累加 
	  if(ThisSum>MaxSum)//发现有更大的则更新当前结果 
	  MaxSum=ThisSum; 
	  else if(ThisSum<0)//如果当前子列和为负
	  ThisSum=0;//则不可能使后面的部分和增大,则弃之 	
	}
	return MaxSum;
}

理解:

在线即在任意时终止都能得到当前的最好的解,比如在程序读到整个序列的第十个数时终止,那么此时返回的就是前十个数的结果,所以其实是可以动态的输入序列的,而前三种算法的内在逻辑就决定了一旦开始运算,整个都得固定,没结束前中断给出的结果都是零散的,不知道结果是在哪个区间的最大子序列和。

此外,感觉第四个算法更新的特点很明显,不断的累加像是将一个集体不断扩大,当发现累加到负,知道前面的相对于后面未知的数只能让和变小了,就像是这个集团已然无用(负的话叫有害可能更好),所以果断 断舍离,从0继续累加,让的ThisSum迭代的效率更高,出现的次数更少,进而加快程序进程。

时间复杂度:

该算法只循环遍历了一次数组,故算法复杂度为n。

 

比较

使用上节所学的时钟打点函数clock,如不熟悉可去查看上篇博文

#include<stdio.h>
#include<stdlib.h>
#include<time.h>
clock_t start,stop;//因为时钟打点的返回值变量类型
double duration;//记录被测函数运行时间,以秒为单位 
int max(int a, int b)
{
	if (a > b)
		return a;
	else
		return b;
}
int MaxSubseqSum1(int A[], int N)
{
	int ThisSum=0, MaxSum = 0;
	int i, j = 0, 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;//则更新结果 
		}//j循环结束 
	}//i循环结束
	return MaxSum;
}
int MaxSubseqSum2(int A[], int N)
{
	int ThisSum=0, MaxSum = 0;
	int i = 0, j = 0;
	for (i = 0; i< N; i++)//i是子列左端位置 
	{
		ThisSum = 0;//ThisSum是从A[i]到A[j]的子列和 
		for (j = i; j < N; j++)//j是子列右端位置 
		{
			ThisSum += A[j];
			if (ThisSum > MaxSum)//如果刚得到的这个子列和更大 
				MaxSum = ThisSum;//则更新结果 
		}//j循环结束 
	}//i循环结束
	return MaxSum;
}
int MaxSubseqSum3(int* A, int size)
{
	int max(int a, int b);
	if (size <= 1)
	{
		if (*A > 0)
			return (*A);
		else
			return 0;
	}
	int mid;
	mid = size / 2;//二分 
	int* left = A;//标记那一段的最左边 
	int* right = (A+mid);//标记那一段的最右边 
	int i;
	int maxSubseqSum;
	int ans;
	maxSubseqSum = max(MaxSubseqSum3(left, mid), MaxSubseqSum3(right, size - mid));//递归的思路找出左右边各自的连续子列和最大值 
	int sum = 0, lmax = *(A + mid), rmax = *(A + mid+1);//跨过中间界限的情况 
	for ( i = mid; i >=0; i--)//从中间往左连续的相加找出过分界线左边的连续序列最大值 
	{
		sum += *(A + i);
		if (sum > lmax)
			lmax = sum;
	}
	sum = 0;
	for ( i = mid + 1; i <=size; i++) {//从中间往右连续的相加找出过分界线右边的连续序列最大值 
		sum += *(A + i);
		if (sum > rmax)
			rmax = sum;
	}
	ans = rmax + lmax;//左右相加即得出跨过中间界限的连续序列最大值 
	maxSubseqSum = max(maxSubseqSum, ans);//中间的和两边最大值一比较,得出这一段的最大值 
	return maxSubseqSum;
    }
int MaxSubseqSum4(int A[], int N)
{
	int ThisSum=0,MaxSum=0;
	int i;
	for(i=0;i<N;i++)
	{
	  ThisSum+=A[i];//向右累加 
	  if(ThisSum>MaxSum)//发现有更大的则更新当前结果 
	  MaxSum=ThisSum; 
	  else if(ThisSum<0)//如果当前子列和为负
	  ThisSum=0;//则不可能使后面的部分和增大,则弃之 	
	}
	return MaxSum;
}
int main()
{   
    double duration;
    int MaxSubseqSum1(int A[], int N);
    int MaxSubseqSum2(int A[], int N);
    int MaxSubseqSum3(int* A, int size);
    int MaxSubseqSum4(int A[], int N); 
	int shuzu[21];
	int i,a1,a2,a3,a4;
	for(i=0;i<21;i++)
	scanf("%d",&shuzu[i]);
	
	start=clock();//开始计时
    for(i=1;i<1e5;i++)//让被测函数重复运行多次 
	a1=MaxSubseqSum1(shuzu,21);
    stop=clock();//停止计时 
    duration=(double)(stop-start)/CLK_TCK;//其他不在测试范围内的处理写在后面,
    printf("%e\n",duration/1e5);//例如输出duration的值 
   
    start=clock();//开始计时
    for(i=1;i<1e5;i++)//让被测函数重复运行多次 
	a2=MaxSubseqSum2(shuzu,21);
	stop=clock();//停止计时 
    duration=(double)(stop-start)/CLK_TCK;//其他不在测试范围内的处理写在后面,
    printf("%e\n",duration/1e5);//例如输出duration的值 
    
    start=clock();//开始计时
    for(i=1;i<1e5;i++)//让被测函数重复运行多次 
	a3=MaxSubseqSum3(shuzu,21);
	stop=clock();//停止计时 
    duration=(double)(stop-start)/CLK_TCK;//其他不在测试范围内的处理写在后面,
    printf("%e\n",duration/1e5);//例如输出duration的值 
        
    start=clock();//开始计时
    for(i=1;i<1e5;i++)//让被测函数重复运行多次 
	a4=MaxSubseqSum4(shuzu,21);
	stop=clock();//停止计时 
    duration=(double)(stop-start)/CLK_TCK;//其他不在测试范围内的处理写在后面,
    printf("%e\n",duration/1e5);//例如输出duration的值 
        
	printf("%d\n%d\n%d\n%d\n",a1,a2,a3,a4);
	return 0; 

}

当序列长度为21时,算法1234所用时间依次如下

6cf3b764f88b4669ac2d34806d2602bc.png

所以算法效率是算法4>算法3>算法2>算法1,与前面分析得出的时间复杂度是对应的。

总结:

本节主要还是算法效率,不过更详尽的介绍了时间复杂度的计算方法,此外,经过一个例题四种算法的对比,分治法和在线处理法的表现尤为突出,二分的思想也在慢慢渗透进课程。笔者接触数据结构时间不久,所以在课程中了解了分治法的逻辑后并不能直接写出相应的代码表示,而是在查阅分治法的代码后,思考良久,方有所得。

笔者拙见,如有错误,请多指正。

指个路,课程在这

【浙江大学数据结构 陈越】https://www.bilibili.com/video/BV1H4411N7oD?p=3&vd_source=6534e1822cc7bda8e7766166eb8f307b

要是直接点链接跳转太麻烦了就在b站用本文标题关键词搜吧。

 

  • 35
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值