关于最大子序列和问题的四种算法(Java)
求解最大子序列和问题,即求解当前序列中某一子序列的最大值,例如当前序列为【-6,7,4,-3,1】,最大子序列和则为11。
第一种算法
利用穷举法暴力,一个序列的子序列一定是有穷的,寻找出所有的子序列和,通过它们不断的比较,不断的取最大值,最后一次比较的结果一定是当前序列的最大子序列和。下面贴出代码:
public class Way_1 {
public static int way(int [] arr) {
int maxSum = 0;
for (int i = 0; i < arr.length; i++) {
for (int j = i; j < arr.length; j++) {
//thisSum在第二重循环时归零
int thisSum = 0;
for (int k = i; k <j; k++) {
thisSum += arr[k];
}
if(maxSum < thisSum)
maxSum = thisSum;
}
}
return maxSum;
}
}
这个算法一定会成功,并且不需要过多解释。由三重for循环实现穷举比较出最大值,显然它的时间复杂度为O(n^3)。这种暴力方式显示过分的消耗了时间。可以再此思路上进行优化。
第二种算法
第二种算法同样是穷举法,是对第一种算法的优化,节约了一定的时间开销。通过对第一种算法的分析,发现第三重for循环可以省略,通过对thisSum归零策略的调整,可以在第二重循环时,实现对thisSum的计算。下面贴出代码:
public class Way_2 {
public static int way(int [] arr) {
int maxSum = 0;
for (int i = 0; i < arr.length; i++) {
//thisSum在第二重循环时累加
int thisSum = 0;
for (int j = i; j < arr.length; j++) {
thisSum += arr[j];
if(maxSum < thisSum)
maxSum = thisSum;
}
}
return maxSum;
}
}
显然第二种穷举法的时间复杂度为O(n^2)。
第三种算法
第三种算法是递归和分治的结合,相对复杂但更加节约时间的的O(NlogN)解法。而且这种算法极好地体现了递归的威力,当然,如果不出现O(N)这种线性解法的话。
这种算法的策略是将序列分割成两个大致相等的序列,暂且称之为左序列和右序列,那么最大子序列一定在左序列的最大子序列,右序列的最大子序列或者左序列包含分割点的最大子序列与右序列包含分割点的最大子序列之和中。如果我们将左序列和右序列继续以这种策略分治,就会得到无数个规模越来越小的子问题。然后递归的求解这些子问题。直到,有一层递归无法继续分割序列了,因为当前序列中只有一个元素,这时我们发现我们找到了递归的基准,也就是说解决了最小的子问题,也可以说我们解决了整个问题。之后补充一下递归的知识,先贴代码:
public class Way_3 {
public static int way(int [] arr,int left,int right) {
//这是递归的基准
if (left == right) {
if(arr [left] > 0)
return arr[left];
else
return 0;
}
//分割序列
int center = (left + right) / 2;
//递归处理子序列
int maxLeftSum = way(arr, left, center);
int maxRightSum = way(arr, center+1, right);
//处理左序列,border边界,英语不好,把单词记下来23333333
int maxLeftBorderSum = 0;
int leftBorderSum = 0;
for (int i = center; i >= left; i--) {
leftBorderSum += arr[i];
if (leftBorderSum > maxLeftBorderSum) {
maxLeftBorderSum = leftBorderSum;
}
}
//处理右序列
int maxRigrhtBorderSum = 0;
int rigrhtBorderSum = 0;
for (int i = center+1; i <= right; i++) {
rigrhtBorderSum += arr[i];
if (rigrhtBorderSum > maxRigrhtBorderSum) {
maxRigrhtBorderSum = rigrhtBorderSum;
}
}
//返回最大子序列和
return Max(maxLeftBorderSum+maxRigrhtBorderSum,maxLeftSum,maxRightSum);
}
//三元运算求最值
public static int Max(int a,int b, int c) {
return a > b? (a > c ? a : c ): (b > c ? b : c);
}
}
就代码量来看,这种算法显然要付出相对于前两种算法更多的编程努力,也算是为了节约时间开销付出的代价了。但是,代码的复杂并不一定对应着时间的节约,有时只需要更好的思路,就像第四种算法。
第四种算法
这种算法较之第三种算法更加简单有效。其策略是:若某个子序列是最大子序列,那么,负数一定不可能是该序列的前缀。类似的,和为负数的子序列一定不可能是此序列前缀。下面贴出代码:
public class Way_4 {
public static int way(int [] arr) {
int maxSum = 0;
int thisSum = 0;
for (int i = 0; i < arr.length; i++) {
thisSum += arr[i];
if (thisSum > maxSum) {
maxSum = thisSum;
} else if (thisSum < 0){
thisSum = 0;
}
}
return maxSum;
}
}
分析下策略,当循环检测到arr[i]到arr[j]的序列和是负的,我们就可以推进i。而且不仅仅是将i推进到i+1,而是推进到j+1。重要的是,这种推进是安全的,我们不会错过最优解。
这种算法的运行时间是明显的。它附带的一个优点是,只需要对数据进行一次扫描,当arr[i]被读入并处理后,就不需要在进行记忆。不仅如此,该算法在任意时刻,都能给出已输入序列的最优解,其他算法不具有这样的特性。定义:具有这种特性的算法叫做联机算法。仅需要常量空间并以线性时间运行的联机算法几乎是完美算法。
##对递归的补充
递归的四大基本原则:
- 基准情形:必须有某种情形,无需递归即可求解。
- 不断推进:对于需要递归求解的清醒,每一次递归调用必须要使状况向某一种基本情形推进。
- 设计原则:假设所有的递归调用都能进行。
- 合成效益法则:在求解一个问题的同一实例是,切勿在不同的递归中做重复的工作。 前两条原则很好理解,就不做赘述。 第三条原则意味着设计递归程序时,一般没有必要了解递归的细节,不必要追踪大量的递归调用。追踪具体的递归调用常常是非常复杂的,而这对于计算机来讲,你懂的。这也正是递归优势的体现。 前三条意味着能不能使用递归,而第四条意味着要不要使用递归。例如·,对于斐波那契数列(1,1,2,3,5,8,13…)来讲,递归不是一个好主意。下面贴出递归实现的代码:
public static long fib(long n) {
if (n <= 1) {
return 1;
} else {
return fib(n - 1) + fib(n - 2);
}
}
- 初看来,该方法似乎对递归的使用非常聪明,但是当n的值在40左右时,它的效率低的吓人。它的运行时间随着n的增大,呈指数型增长。这个程序之所以缓慢是因为它做了大量重复的工作,这个方法在计算fib(n-1)时,实际已经在某处计算了fib(n-2),但是它将这个数据抛弃了,又重新计算了fib(n-2),这就导致了巨大的运行时间。分享一句话,计算任何事情计量不要超过一次。顺便贴出main,以便运行:
• public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("输入序列长度:n = ");
System.out.println();
int n = sc.nextInt();
int [] arr = new int [n];
System.out.print(“输入序列:”);
for (int i = 0; i < arr.length; i++) {
arr[i] = sc.nextInt();
}
//System.out.println("最大子序列和为:"+Way_1.way(arr));
//System.out.println("最大子序列和为:"+Way_2.way(arr));
//System.out.println("最大子序列和为:"+Way_3.way(arr, 0, arr.length-1));
//System.out.println("最大子序列和为:"+Way_4.way(arr));
}
本篇结束,哈哈。