最大子数组和
此题解法仅为为个人理解,如有其他思路或解法欢迎来探讨! 基础篇链接:最大子数组和(连续子数组的最大和)—— 基础篇
题目:
已知一个整数数组 arr,找到子数组的最大和(子数组最少包含一个元素)并返回这个子数组。 子数组元素顺序不发生改变且需连续。
示例:
输入: arr = [-2,1,-3,4,-1,2,1,-5,4]
输出: sum = 6 ,nums = [4,-1,2,1]
分析:
首先这道题是我们老师给我们出的,要求我们使用分治算法来写。所以接下来的分析都是围绕着分治算法来进行解析:
在计算机科学中,分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。 比如:排序算法中的快速排序与归并排序,傅里叶变换等
这里就要来说明一下分治法适用的情况了:
- 该问题的规模缩小到一定的程度就可以容易地解决;
- 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质;
- 利用该问题分解出的子问题的解可以合并为该问题的解;
- 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;
第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;、
第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。
第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。
现在
将视线回到这道题上,按照分治算法,我们要将数组从中间拆分,使其成为更小的问题。在方法maxSubArray 中,我们需要的元素有——所输入的数组、指向左子组的下标left、指向右子组的下标right、指向中间分隔值的下标mid。用递归实现,当子数组中只剩一位时返回其本身if (left == right) return nums[left]; 。
这时,会出现三种情况,最大和出现在左子组、最大和出现在右子组、最大和出现在跨两个子组的位置。这里左子组的从left—mid、右子组是从mid+1—right、而跨左右数组的最大子数组和,则需要单独再写一个方法midSubArray 来计算了。
在跨左右数组的最大子数组和的计算中,我们要分左右进行计算和的计算,让指针从中值mid开始,向两边进行移动就能保证始终是以最大和来计算的,当和sum 加上指针所在的那一位时的所得值比之前的sum 更小,则不将此次的和存入leftSum(rightSum) 。这样将左右子组都计算完毕时,返回leftSum + rightSum 就是跨左右数组的最大子数组和。 注意:这里在定义leftSum 和rightSum 时要将值设置为极小值而不能为0,因为输入的数组为整数,要考虑子数组中出现负数的情况。且不论是最大和产生在左子组还是右子组,都会进这一步,所以不用再专门写左子组合右子组的求和。
int leftSum = Integer.MIN_VALUE;
int rightSum = Integer.MIN_VALUE;
在midSubArray 方法返回值回到maxSubArray 时,求出当前左子组最大和、右子组最大和、跨左右子组最大和,返回其中的最大值就可以了。
return Math.max(leftSum,Math.max(rightSum,midSum));
最后,在递归的一层层返回后,就得到了我们所求的最大子数组和。
什么? 你说没写完?还有个返回这个子数组的要求没有实现。 嗯嗯,是的。这个还没有写。上面的其实是最大子数组和—— 基础篇 最后分治算法的解析,而提高篇和基础篇的区别就在,提高篇要找到并输出哪个产生最大和的子数组。这在我第一次接触到这道题时可烦到我了,当时我对分治算法还不是特别熟悉,我找不到哪个子数组的下标。但如今做出来后再看起分治算法,便有了更通透的理解。这里我变分享一下我的写法:
由上面分治算法的说明我们可以知道,分治算法是将一个大问题分成若干个小问题,有点像二叉树那样子。那我们想找到哪个子数组的头尾下标,就要从小问题着手。 我们先定义两个静态变量来记录头尾位置下标(改成其他的会更好,在这道题我这样更方便看到罢了)
public static int startIndex;
public static int endIndex;
那我们在哪里给这两个变量赋值呢?嗯,对了,就是在midSubArray 方法中的每一次将所得最大和存入leftSum(rightSum) 时写入下标i ,因为每一次更新最大和,i 指到那个对应的数就刚好是我们所需要的那个边界上的数,在这存入就会一直保证每次更新最大和时都能更新到下标。 最后再按照下标输出就好咯。
结束!
代码:
先看上面的分析!自己理解思考一下,不要急着看代码!
分值算法:
public class DivideAndConquer {
public static int startIndex;
public static int endIndex;
@Test
public void test(){
int[] arr = {1, -2, 3, 10, -4, 7, 2, -5};
System.out.println("输入数组:" + Arrays.toString(arr));
System.out.println("和最大值为:" + maxSubArray(arr,0,arr.length-1));
printSubArray(arr,startIndex,endIndex,maxSubArray(arr,0,arr.length-1));
int[] arr1 = {-2, -1, -5, -9, -8};
System.out.println("输入数组:" + Arrays.toString(arr1));
System.out.println("和最大值为:" + maxSubArray(arr1,0,arr1.length-1));
printSubArray(arr1,startIndex,endIndex,maxSubArray(arr1,0,arr1.length-1));
}
public static void printSubArray(int[] nums , int startIndex , int endIndex , int sum){
if (sum < 0){
System.out.println("和最大子数组为:[" + sum + "]");
return;
}
System.out.print("和最大子数组为:[" + nums[startIndex]);
while (startIndex < endIndex){
System.out.print("," + nums[startIndex + 1]);
startIndex++;
}
System.out.println("]");
}
public static int maxSubArray(int[] nums , int left , int right){
if (left == right) return nums[left];
int mid = (left + right) / 2;
int leftSum = maxSubArray(nums , left , mid);
int rightSum = maxSubArray(nums , mid +1 , right);
int midSum = midSubArray(nums , left , mid , right);
return Math.max(leftSum,Math.max(rightSum,midSum));
}
public static int midSubArray(int[] nums , int left , int mid , int right){
int leftSum = Integer.MIN_VALUE;
int rightSum = Integer.MIN_VALUE;
int sum = 0;
for (int i = mid ; i >= left ; i--){
sum += nums[i];
if (sum > leftSum){
leftSum = sum;
startIndex = i;
}
}
sum = 0;
for (int i = mid + 1 ; i <= right ; i++){
sum += nums[i];
if (sum > rightSum){
rightSum = sum;
endIndex = i;
}
}
return leftSum + rightSum;
}
}
当然,这里还有一份代码,是我在写分治之前的一种写法,对我写分治算法有蛮大帮助的,也放在这里:
public class Solution {
@Test
public void test(){
int[] arr = {1, -2, 3, 10, -4, 7, 2, -5};
System.out.println("输入数组:" + Arrays.toString(arr));
System.out.println("和最大值为:" + maxSubArray(arr));
int[] arr1 = {-2, -1, -5, -9, -8};
System.out.println("输入数组:" + Arrays.toString(arr1));
System.out.println("和最大值为:" + maxSubArray(arr1));
}
public int maxSubArray(int[] nums) {
if (nums.length < 1) return 0;
else if (nums.length == 1) return nums[0];
int startIndex = 0;
int endIndex = 0;
int sum = 0;
int temp = 0;
int tempStartIndex = -1;
for (int i = 0 ; i < nums.length ; i++){
if (temp < 0)
{
temp = nums[i];
tempStartIndex = i;
}
else
{
temp += nums[i];
}
if (sum < temp)
{
sum = temp;
startIndex = tempStartIndex;
endIndex = i;
}
}
if (endIndex - startIndex == 0){
System.out.println("输入全为负数");
return Arrays.stream(nums).max().getAsInt();
}
System.out.print("和最大子数组为:[");
while (startIndex <= endIndex){
System.out.print(nums[startIndex] + ",");
startIndex++;
}
System.out.println("]");
return sum;
}
}
分治算法解释来自:
分治算法详解(超详细)
|