浙大陈越老师主编《数据结构》(第2版)学习笔记
目录
算法定义
算法是一个有限的指令集,接受一些输入(有些情况下没有输入),产生输出,并一定在有限步骤之后终止。
算法复杂度
衡量算法优劣的指标:
1、空间复杂度
根据算法写成的程序执行时占用的存储单元长度(与输入数据的规模n有关)
2、时间复杂度
根据算法写成的程序执行时花费时间的长度(与输入数据的规模n有关)
chapter 1.1 例1.2中printN函数的递归实现,在N比较大时非正常中断的原因是什么?
函数A调用函数B时,必须先保存A当前状态,当B调用完成后,再释放内存,恢复状态,继续执行。
例1.2中printN函数迭代实现中:执行printN(N)时调用printN(N - 1),需要保存printN(N)状态;执行printN(N - 1)时调用printN(N - 2),需要保存printN(N - 1)状态;……依次类推,直到执行完printN(0),才能开始逐级释放内存。
假设存储每个函数的状态占用1个单位内存空间,则执行printN(N)需要N个单位的内存空间,当N非常大时,内存不足导致非正常中断。
根据定义,例1.2中printN递归实现的空间复杂度,C是一个固定常数,n是要打印整数的个数。
chapter 1.1 例1.3中计算多项式的简单直接算法、秦九昭算法的时间复杂度分别是多少?
简单直接算法:执行了n+1次result += (a[i] * pow(x, i))语句——每次涉及i次乘法和1次加法,所以总共涉及n+1次加法和(1 + 2 +...+n)=(1 + n) * n / 2次乘法。,n是多项式的阶数。
秦九昭算法:执行了n次result = a[i -1] + (x * result)语句,总共涉及n次加法和n次乘法。,n是多项式的阶数。
两者比较,对于充分大的n,总会比大,即秦九昭算法比简单直接算法快,且n越大,快得越明显。
最坏情况复杂度
分析算法效率时,我们经常关注两种复杂度:
1、最坏情况复杂度
2、平均复杂度
例:用顺序查找法在一排混乱无序的书架上找一本书
最好情况:1次找到
最坏情况:找了n本书,没有找到
则,C是查看一本书的时间。
要得到平均查找次数,则略麻烦,要把每一种可能的情况都考虑到(可能查找两次,也可能需要三次),把所有情况下的查找次数加起来,除以情况个数。
显然,
渐进表示法
精确地比较程序执行的步数是没意义的,因为每步执行时间可能不同,比如递归调用的1步,实际上涉及系统堆栈的很多处理,比循环中的1步计算慢得多。
所以,比较算法优劣时,只考虑宏观渐近性质,即当输入规模n“充分大”时,观察不同算法复杂度的“增长趋势”。
计算多项式例子中,我们只要知道:当很大的时候,简单直接算法的时间复杂度,基本上就是 在起主要作用。而秦九昭算法,则是在起主要作用。当n充分大的时候,前者肯定比后者要慢。
于是就有了复杂度的渐进表示法:
表示存在常数, ,使得当 时,有,就表示是的某种上界.
表示存在常数, ,使得当 时,有,
就表示是的某种下界.
表示同时有和。对于这个里面的函数来说,和是同时成立的,也就是说,它既是上界也是下界。
计算多项式例子中,简单直接算法的时间复杂度是,秦九昭算法的时间复杂度是
通过下图可以直观地看到不同函数随着n的增长,它的增长速度:
面对复杂度的算法时,计算机科学的本能反应是将之优化为一个的算法,后者效率高很多。
对给定算法做渐近分析的窍门:
应用实例:最大子列和问题
分析:“子列”为原始序列中连续的一段数字,要找具有最大和的“子列”,并返回它的和;如果这个最大和是负数,则返回0。
算法1.1 穷举所有子列和,从中找到最大值
/* 穷举所有子列和,从中找到最大值 */
int max_subseq_sum(int List[], int N)
{
int i;
int j;
int k;
int max_sum = 0;
int sum;
if(N <= 0)
{
printf("param error. \n");
return -1;
}
/* i是子列左端位置,j是子列右端位置 */
for(i = 0; i < N; i++)
{
for(j = i; j < N; j++)
{
sum = 0;
for(k = i; k <= j; k++)
{
sum += List[k];
}
if(sum > max_sum)
{
max_sum = sum;
}
}
}
return max_sum;
}
调用:
int main()
{
int max_sum;
int List[] = {-2, 11, -4, 13, -5, -2};
int N = sizeof(List) / sizeof(List[0]);
max_sum = max_subseq_sum(List, N);
if(max_sum >= 0)
{
printf("max sub sequence sum is %d \n", max_sum);
}
return 0;
}
结果:
算法1.2 部分存储中间值的穷举
/* 部分存储中间值的穷举 */
int max_subseq_sum(int List[], int N)
{
int i;
int j;
int k;
int max_sum = 0;
int sum;
if(N <= 0)
{
printf("param error. \n");
return -1;
}
/* i是子列左端位置,j是子列右端位置 */
for(i = 0; i < N; i++)
{
sum = 0;
for(j = i; j < N; j++)
{
sum += List[j];
if(sum > max_sum)
{
max_sum = sum;
}
}
}
return max_sum;
}
算法1.3 分而治之
/* 返回3个数中最大的数 */
int max3(int A, int B, int C)
{
if(A > B)
{
if(A > C)
{
return A;
}
else
{
return C;
}
}
else
{
if(B > C)
{
return B;
}
else
{
return C;
}
}
}
/* 分而治之 */
int devide_and_conquer(int List[], int left, int right)
{
int middle = 0;
int left_max_sum = 0;
int right_max_sum = 0;
int middle_max_sum = 0;
/* 递归终止条件:子列只有一个数字 */
if(left == right)
{
if(List[left] < 0)
{
return 0;
}
else
{
return List[left];
}
}
/* 找到数列中间位置,将数列分为左右两部分 */
middle = (left + right) / 2;
/* 分别计算左、右两部分的最大子列和 */
left_max_sum = devide_and_conquer(List, left, middle);
right_max_sum = devide_and_conquer(List, middle + 1, right);
/* 计算跨越中间位置的子列的最大子列和 */
int i;
int tmp_left_sum = 0;
int tmp_right_sum = 0;
int tmp_left_max_sum = 0;
int tmp_right_max_sum = 0;
for(i = middle; i >= left; i--)
{
tmp_left_sum += List[i];
if(tmp_left_sum > tmp_left_max_sum)
{
tmp_left_max_sum = tmp_left_sum;
}
}
for(i = (middle + 1); i <= right; i++)
{
tmp_right_sum += List[i];
if(tmp_right_sum > tmp_right_max_sum)
{
tmp_right_max_sum = tmp_right_sum;
}
}
middle_max_sum = tmp_left_max_sum + tmp_right_max_sum;
/* 结果:左边部分、右边部分、跨越中间位置的最大子列和中的最大值 */
return max3(left_max_sum, right_max_sum, middle_max_sum);
}
/* 用分而治之的方法计算最大子列和 */
int max_subseq_sum(int List[], int N)
{
return devide_and_conquer(List, 0, N - 1);
}
算法1.1的时间复杂度由3层for循环嵌套决定,算法复杂度为
算法1.2的时间复杂度由2层for循环嵌套决定,算法复杂度为
算法1.3的时间复杂度分析略有难度:记整体时间复杂度为,则函数devide_and_conquer中递归进行“分”的复杂度为(我们解决了2个长度减半的子问题)。求跨分界线的最大子列和时,有2个for循环,所用步骤不超过N,所以在时间完成,其他步骤只需要时间。
综上分析:
不断对分直到,即时,得到
此算法比算法1.2又快一些,但仍然不是最快的算法。
算法1.4 在线处理
/* 在线处理方法计算最大子列和 */
int max_subseq_sum(int List[], int N)
{
int i;
int max_sum = 0;
int sum = 0;
for(i = 0; i < N; i++)
{
sum += List[i];
// printf("sum: %d \n", sum);
if(sum > max_sum)
{
max_sum = sum;
// printf("max sum: %d \n", max_sum);
}
else if(sum < 0)
{
/* 当前子列和为负,则不可能使后面的部分和增大,抛弃 */
sum = 0;
}
}
return max_sum;
}
该算法复杂度只有
此算法中,无论我们停在中间哪一步,返回值都是最大子列和的正确解。
解决同一个问题,不同的算法会有很大的差别。
提高效率的窍门之一:让计算机“记住”一些关键的中间结果,避免重复计算。