本章简述
通过前面第二章节的叙述,描述了算法设计对程序员的日常影响:算法上的灵机一动可以使程序更加简单。但是本章内容将会发现算法设计的一个不那么常见但更富于戏剧性的贡献:复杂深奥的算法有时可以极大地提高程序性能。
问题及简单算法
本章引入的问题来自一维的模式识别,问题的输入是具有n个浮点数的向量x,输出是输入向量的任何连续子向量中的最大和。
例如,如果输入向量包含以下 N = 10 个元素:
arr[N] = { 31, -41, 59, 26, -53, 58, 97, -93, -23, 84 }
那么该程序的输出为x[2…6]的总和,即187。
该问题分为以下几种情况:
- 当所有数都是正数时,此时最大子向量就是整个输入向量;
- 当所有数都是负数时,此时最大子向量就是空向量,总和为0;
- 当输入向量正负均有时,此时最大子向量需要特殊处理;
对该问题,本章讨论了4种算法,性能逐步提高。
- 算法1的时间复杂度为T(n)=O(n^3);
- 算法2及2b的时间复杂度为T(n)=O(n^2);
- 算法3的时间复杂度为T(n)=O(nlogn);
- 算法4的时间复杂度为T(n)=O(n);
下面将下面4个算法,详细给出程序实现。
立方算法
完成该问题的浅显程序对所有满足区间的(i,j)的整数对进行迭代。对每个整数对,程序都要计算x[i…j]的总和,并检验是否大于迄今为止的最大总和。算法程序实现如下:
/************************************************************************/
/*
* 《编程珠玑》第八章 算法设计技术
* 问题:输入具有n个浮点数的向量x ,输出向量中任何连续子向量中的最大和
* 算法一:T(n) = O(n^3)
*/
/************************************************************************/
#include <iostream>
#include <cstdlib>
using namespace std;
/************************************************************************/
/*
* 算法说明:
*(1)当所有输入都是正数时,最大子向量就是整个输入向量
*(2)当所有输入都是负数时,最大子向量是空向量,和为0
*(3)当前处理情况是,输入序列有正有负
*/
/************************************************************************/
float maxSubSum(const float *arr, const int N , int &begin , int &end)
{
float maxsofar = 0 ;
for (int i = 0; i < N; i++)
{
for (int j = i; j < N; j++)
{
float sum = 0;
for (int k = i; k <= j; k++)
{
sum += arr[k];
}
if (sum > maxsofar)
{
maxsofar = sum;
begin = i;
end = j;
}
}
}
return maxsofar;
}
const int N = 10;
const float arr[N] = { 31, -41, 59, 26, -53, 58, 97, -93, -23, 84 };
int main()
{
cout << "The input numbers are: " << endl;
for (int i = 0; i < N; i++)
cout << arr[i] << "\t";
cout << endl << "The max sum is : " ;
int begin = -1, end = -1;
cout << maxSubSum(arr, N , begin , end) <<endl;
cout << "The position is : " << begin << "\t" << end << endl;
system("pause");
return 0;
}
平方算法1
对于之前的立方算法性能不尽如人意,接下来的第一个平方算法,利用x[i…j]的总和与x[i…j-1]的总和之间的关系出发,简化算法使得时间复杂度降为O(n^2)
/************************************************************************/
/*
* 《编程珠玑》第八章 算法设计技术
* 问题:输入具有n个浮点数的向量x ,输出向量中任何连续子向量中的最大和
* 算法二:T(n) = O(n^2)
*/
/************************************************************************/
#include <iostream>
#include <cstdlib>
using namespace std;
/************************************************************************/
/*
* 算法说明:
*(1)当所有输入都是正数时,最大子向量就是整个输入向量
*(2)当所有输入都是负数时,最大子向量是空向量,和为0
*(3)当前处理情况是,输入序列有正有负
*/
/************************************************************************/
float maxSubSum(const float *arr, const int N, int &begin, int &end)
{
float maxsofar = 0;
for (int i = 0; i < N; i++)
{
float sum = 0;
for (int j = i; j < N; j++)
{
sum += arr[j];
if (sum > maxsofar)
{
maxsofar = sum;
begin = i;
end = j;
}
}
}
return maxsofar;
}
const int N = 10;
const float arr[N] = { 31, -41, 59, 26, -53, 58, 97, -93, -23, 84 };
int main()
{
cout << "The input numbers are: " << endl;
for (int i = 0; i < N; i++)
cout << arr[i] << "\t";
cout << endl << "The max sum is : ";
int begin = -1, end = -1;
cout << maxSubSum(arr, N, begin, end) << endl;
cout << "The position is : " << begin << "\t" << end << endl;
system("pause");
return 0;
}
在该算法中,第一个循环内的语句执行n次,第二个循环内的语句在每次执行外循环时至多执行n次,所以总的运行时间为O(n^2);
平方算法2
另一个平方算法是通过访问在外循环执行之前就已构建的数据结构的方式在内循环中计算总和,cumarr中的第i个元素包含x[0…i]中各个数的累加和,所以x[i…j]中各个数的总和可以通过计算cumarr[j]-cumarr[i-1]得到,该算法的程序实现如下:
/************************************************************************/
/*
* 《编程珠玑》第八章 算法设计技术
* 问题:输入具有n个浮点数的向量x ,输出向量中任何连续子向量中的最大和
* 算法2b:T(n) = O(n^2)
*/
/************************************************************************/
#include <iostream>
#include <cstdlib>
using namespace std;
const int N = 10;
/************************************************************************/
/*
* 算法说明:
*(1)当所有输入都是正数时,最大子向量就是整个输入向量
*(2)当所有输入都是负数时,最大子向量是空向量,和为0
*(3)当前处理情况是,输入序列有正有负
*/
/************************************************************************/
float maxSubSum(const float *arr, const int n, int &begin, int &end)
{
/*
* 下面3行代码解决 cumarr[-1] 的问题
*/
float realsum[N + 1] = { 0 };
float *cumarr = realsum + 1;
cumarr[-1] = 0;
for (int i = 0; i < n; i++)
{
cumarr[i] = cumarr[i - 1] + arr[i];
}
float maxsofar = 0;
for (int i = 0; i < n; i++)
{
float sum = 0;
for (int j = i; j < n; j++)
{
sum = cumarr[j] - cumarr[i-1];
if (sum > maxsofar)
{
maxsofar = sum;
begin = i;
end = j;
}
}
}
return maxsofar;
}
const float arr[N] = { 31, -41, 59, 26, -53, 58, 97, -93, -23, 84 };
int main()
{
cout << "The input numbers are: " << endl;
for (int i = 0; i < N; i++)
cout << arr[i] << "\t";
cout << endl << "The max sum is : ";
int begin = -1, end = -1;
cout << maxSubSum(arr, N, begin, end) << endl;
cout << "The position is : " << begin << "\t" << end << endl;
system("pause");
return 0;
}
该算法的时间复杂度也是O(n^2)其分析过程与上一个算法相同。
分治算法
分治算法思想:要解决规模为n的问题,可递归的解决两个规模近似为n/2的子问题,然后对他们的答案进行合并可以得到整个问题的答案。
在本例中,初始问题要处理大小为n的向量。所以将它划分为两个子问题的最自然的方法就是创建两个大小近似相等的子向量,分别称为a , b;
然后递归的找出a,b元素中总和最大的子向量;
此时分为三种情况:
- 最大子向量位于a中;
- 最大子向量位于b中;
- 最大子向量跨越a,b之间的边界,处于整个向量的中间;
分治算法解决该问题的程序实现如下:
/************************************************************************/
/*
* 《编程珠玑》第八章 算法设计技术
* 问题:输入具有n个浮点数的向量x ,输出向量中任何连续子向量中的最大和
* 算法3:T(n) = O(nlogn)
*/
/************************************************************************/
#include <iostream>
#include <cstdlib>
using namespace std;
const int N = 10;
float max(float a, float b)
{
return a > b ? a : b;
}
/************************************************************************/
/*
* 算法说明:
*(1)当所有输入都是正数时,最大子向量就是整个输入向量
*(2)当所有输入都是负数时,最大子向量是空向量,和为0
*(3)当前处理情况是,输入序列有正有负
*/
/************************************************************************/
float maxSubSum(const float *arr, int left , int right)
{
if (left > right)
return 0;
if (left == right)
return 0 > arr[left] ? 0 : arr[left];
int middle = (left + right) / 2;
float lmax = 0, sum = 0;
for (int i = middle; i >= left; i--)
{
sum += arr[i];
lmax = lmax > sum ? lmax : sum;
}
float rmax = 0;
sum = 0;
for (int j = middle + 1; j <= right; j++)
{
sum += arr[j];
rmax = rmax > sum ? rmax : sum;
}
return max(max((lmax + rmax), maxSubSum(arr , left, middle)), maxSubSum(arr , middle + 1, right));
}
const float arr[N] = { 31, -41, 59, 26, -53, 58, 97, -93, -23, 84 };
int main()
{
cout << "The input numbers are: " << endl;
for (int i = 0; i < N; i++)
cout << arr[i] << "\t";
cout << endl << "The max sum is : ";
cout << maxSubSum(arr, 0, N-1) << endl;
system("pause");
return 0;
}
该程序代码在O(nlogn)的时间内解决了该问题。
其时间复杂度满足:
T(n) = 2T(n/2) + O(n)
解此递归式可得T(n)=O(nlogn);
扫描算法
扫描算法是操作数据的最简单算法:从数组最左端(元素x[0])开始扫描,到元素最右端(元素x[n-1])为止,并记下所遇到的总和最大的子向量。
程序实现如下:
/************************************************************************/
/*
* 《编程珠玑》第八章 算法设计技术
* 问题:输入具有n个浮点数的向量x ,输出向量中任何连续子向量中的最大和
* 算法4:T(n) = O(n)
*/
/************************************************************************/
#include <iostream>
#include <cstdlib>
using namespace std;
const int N = 10;
float max(float a, float b)
{
return a > b ? a : b;
}
/************************************************************************/
/*
* 算法说明:
*(1)当所有输入都是正数时,最大子向量就是整个输入向量
*(2)当所有输入都是负数时,最大子向量是空向量,和为0
*(3)当前处理情况是,输入序列有正有负
*/
/************************************************************************/
float maxSubSum(const float *arr, const int n )
{
float maxSoFar = 0, maxEndInHere = 0;
//该问题的关键在于maxEndInHere变量,它是结束位置i-1之前的最大子向量之和
for (int i = 0; i < n; i++)
{
maxEndInHere = max(maxEndInHere + arr[i], 0);
maxSoFar = max(maxSoFar, maxEndInHere);
}
return maxSoFar;
}
const float arr[N] = { 31, -41, 59, 26, -53, 58, 97, -93, -23, 84 };
int main()
{
cout << "The input numbers are: " << endl;
for (int i = 0; i < N; i++)
cout << arr[i] << "\t";
cout << endl << "The max sum is : ";
cout << maxSubSum(arr,N) << endl;
system("pause");
return 0;
}
理解这个程序的关键在于变量maxEndInHere,在循环中的第一个赋值语句之前,该变量存储了结束位置i-1的最大子向量之和,赋值语句将其修改为结束位置为i的最大子向量的和。
这个算法理解起来有点困难,但是其具有最优的时间复杂度,T(n)=O(n)
原理
本章围绕求一维向量的最大子向量和的问题展开,讲解了解决该问题的4个算法,性能依次提升,这也就是循序渐进的展示了算法设计技术:
- 保存状态,避免重复计算
- 将信息预处理至数据结构中
- 分治算法
- 扫描算法
- 累加数组,如同平方算法2中,使用了累加表,表中第i个元素的值为x中前i个元素的值的总和,这一类表通常用于处理有范围限制的问题。
- 下界,只有在确定了自己的算法是所有可能的算法中最佳的之后,算法设计师才可能踏实睡觉,因此必须证明出某个相匹配的下界。