一、数学基础
1.定义
2.定义的解释
3.级别的判断方法
4.结论
二、以“最大的子序列和”为例
1.运行时间的计算规定
为了简化分析,因此忽略前导常数(leading constants),同时抛弃低阶项(低阶项对于算法的整体运行时间来说影响较小)。
前导常数是指在进行运行时间计算时,忽略的常数因子。具体来说,当我们分析算法的运行时间时,我们通常只关注算法的增长率,而不关注具体的常数值。比如:
执行一个固定数量的操作,例如执行1000次循环,那么前导常数是1000。
执行一次操作需要1秒钟,那么前导常数是1。
进行一次数据读取需要10毫秒,那么前导常数是10。
声明不计时间。赋值、加减乘除、初始化、测试条件、return都记1个单位时间。
2.运行时间的计算法则
①for循环:
for (i = 1; i <= N; i++)
p += i * i * i;
②嵌套for循环:
在一组嵌套循环内部的一条语句总的运行时间为该语句的运行时间乘以该组所有的for循环的大小的乘积。
for (i = 0; i < N; i++)
for (j = 0; j < N; j++)
k++;
③顺序语句:
将各个语句的运行时间求和即可(其中的最大值就是所得的运行时间)
for (i = 0; i < N; i++)
A[i] = 0;
for (i = 0; i < N; i++)
for (j = 0; j < N; j++)
A[i] += A[j] + i + j;
④if-else语句:
运行时间取if/else中运行时间较长的。
⑤递归:
简单的递归可以化成for循环:
int Factorial(int N)
{
if (N <= 1)
return 1;
else
return N * Factorial(N - 1);
}
int Fib(int N)
{
if (N <= 1)
return 1;
else
return Fib(N - 1) + Fib(N - 2);
}
3.四种算法的具体实现与对比
方法① 三重嵌套循环
#include <stdio.h>
int MaxSubsequenceSum(const int A[], int N)
{
int ThisSum, MaxSum, i, j, k;
MaxSum = 0;
for (i = 0; i < N; i++)
{
for (j = i; j < N; j++)
{
ThisSum = 0;
for (k = i; k <= j; k++)
ThisSum += A[k];
if (ThisSum > MaxSum)
MaxSum = ThisSum;
}
}
return MaxSum;
}
int main()
{
int n,maxsum=0;
int arr[] = { -2,11,-4,13,-5,-2 };
n = sizeof(arr) / sizeof(arr[0]);
maxsum = MaxSubsequenceSum(arr, n);
printf("%d", maxsum);
return 0;
}
使用const int A[]的原因是为了指明函数MaxSubsequenceSum中的参数A是一个常量数组,即在函数内部不会改变数组中的元素的值。这有助于编译器进行优化,并避免无意中修改传入的数组。这样做是为了确保函数不会意外地修改调用者传入的数组内容,增强代码的安全性和可维护性。
方法② 两层嵌套循环
int MaxSubsequenceSum(const int A[], int N)
{
int ThisSum, MaxSum, i, j, k;
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;
}
方法①设置了两层起始点,i为j的起始点,然后再让k遍历完一个整体,而j又会再次遍历,这其中就有重复的,这是不必要的。方法②中仅设置一个起始点i,j直接开始遍历,减少了一个for循环。
方法③ 递归
#include <stdio.h>
int Max3(int a, int b, int c)
{
return (a > b) ? (a > c ? a : c) : (c > b ? c : b);
}
static int MaxSubSum(const int A[], int left, int right)
{
int maxleftsum, maxrightsum, maxleftbordersum = 0, maxrightbordersum = 0; // 左/右子数组的最大和,左/右边界子数组的最大和
int leftbordersum = 0, rightbordersum = 0, center, i; // 当前左/右边界子数组的和,中间元素的位置,循环变量
if (left == right) // 基准情形
{
if (A[left] > 0)
return A[left]; // 如果只有一个元素,非负时就返回当前的值
else
return 0; // 否则返回0
}
center = (left + right) / 2; // 中心
maxleftsum = MaxSubSum(A, left, center); // 左边递归,从left到center
maxrightsum = MaxSubSum(A, center + 1, right); // 右边递归,从center+1到right
for (i = center; i >= left; i--) // 计算从center开始,向左的最大和
{
leftbordersum += A[i];
if (leftbordersum > maxleftbordersum)
maxleftbordersum = leftbordersum; // 只要向左求和还在变大,就将其和leftbordersum重新赋值给maxleftbordersum
}
for (i = center + 1; i <= right; i++) // 计算从center开始,向右的最大和
{
rightbordersum += A[i];
if (rightbordersum > maxrightbordersum)
maxrightbordersum = rightbordersum; // 只要向右求和还在变大,就将其和rightbordersum重新赋值给maxrightbordersum
}
return Max3(maxleftsum, maxrightsum, maxleftbordersum + maxrightbordersum);
}
int MaxSubsequenceSum(const int A[], int N)
{
return MaxSubSum(A, 0, N - 1);
}
整体思想是:最大子序列只可能在三处位置出现:①全在左半部分②全在右半部分③跨越中心。(分而治之)
对于①②情况,可以直接递归求解。
对于③情况,因为必有中心两侧的数字在最大子序列中(否则变成①②情况),既然已知这两个数在最大子序列中,可以向前/后求得前半/后部分的最大和,加起来从而得到整体的最大和。
依旧以-2,11,-4,13,-5,-2为例。
左侧-2,11,-4最大为-2+11=9
右侧13,-5,-2最大为13
跨越中心的为-4,13。从-4向左,最大为11-4=7。从13向右,最大的是13。从而7+13=20。
比较9,13,20,三者最大的是20。
方法④ 动态规划
int MaxSubsequenceSum(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;
}
方法⑤ 数学分析
int MaxSubsequenceSum(const int A[], int N)
{
int MinSum, MaxSum, Sum, i;
MaxSum = A[0];
MinSum = Sum = 0;
for (i = 0; i < N; i++)
{
Sum += A[i];
MaxSum = MaxSum > (Sum - MinSum) ? MaxSum : (Sum - MinSum);
MinSum = MinSum < Sum ? MinSum : Sum;
}
return MaxSum;
}